summaryrefslogtreecommitdiffstats
path: root/dom/serviceworkers
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 09:22:09 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 09:22:09 +0000
commit43a97878ce14b72f0981164f87f2e35e14151312 (patch)
tree620249daf56c0258faa40cbdcf9cfba06de2a846 /dom/serviceworkers
parentInitial commit. (diff)
downloadfirefox-43a97878ce14b72f0981164f87f2e35e14151312.tar.xz
firefox-43a97878ce14b72f0981164f87f2e35e14151312.zip
Adding upstream version 110.0.1.upstream/110.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'dom/serviceworkers')
-rw-r--r--dom/serviceworkers/FetchEventOpChild.cpp600
-rw-r--r--dom/serviceworkers/FetchEventOpChild.h92
-rw-r--r--dom/serviceworkers/FetchEventOpParent.cpp88
-rw-r--r--dom/serviceworkers/FetchEventOpParent.h72
-rw-r--r--dom/serviceworkers/FetchEventOpProxyChild.cpp256
-rw-r--r--dom/serviceworkers/FetchEventOpProxyChild.h71
-rw-r--r--dom/serviceworkers/FetchEventOpProxyParent.cpp227
-rw-r--r--dom/serviceworkers/FetchEventOpProxyParent.h68
-rw-r--r--dom/serviceworkers/IPCNavigationPreloadState.ipdlh16
-rw-r--r--dom/serviceworkers/IPCServiceWorkerDescriptor.ipdlh30
-rw-r--r--dom/serviceworkers/IPCServiceWorkerRegistrationDescriptor.ipdlh58
-rw-r--r--dom/serviceworkers/NavigationPreloadManager.cpp139
-rw-r--r--dom/serviceworkers/NavigationPreloadManager.h65
-rw-r--r--dom/serviceworkers/PFetchEventOp.ipdl33
-rw-r--r--dom/serviceworkers/PFetchEventOpProxy.ipdl32
-rw-r--r--dom/serviceworkers/PServiceWorker.ipdl28
-rw-r--r--dom/serviceworkers/PServiceWorkerContainer.ipdl41
-rw-r--r--dom/serviceworkers/PServiceWorkerManager.ipdl31
-rw-r--r--dom/serviceworkers/PServiceWorkerRegistration.ipdl40
-rw-r--r--dom/serviceworkers/ServiceWorker.cpp379
-rw-r--r--dom/serviceworkers/ServiceWorker.h98
-rw-r--r--dom/serviceworkers/ServiceWorkerActors.cpp37
-rw-r--r--dom/serviceworkers/ServiceWorkerActors.h37
-rw-r--r--dom/serviceworkers/ServiceWorkerChild.cpp69
-rw-r--r--dom/serviceworkers/ServiceWorkerChild.h44
-rw-r--r--dom/serviceworkers/ServiceWorkerCloneData.cpp80
-rw-r--r--dom/serviceworkers/ServiceWorkerCloneData.h71
-rw-r--r--dom/serviceworkers/ServiceWorkerContainer.cpp893
-rw-r--r--dom/serviceworkers/ServiceWorkerContainer.h143
-rw-r--r--dom/serviceworkers/ServiceWorkerContainerChild.cpp70
-rw-r--r--dom/serviceworkers/ServiceWorkerContainerChild.h47
-rw-r--r--dom/serviceworkers/ServiceWorkerContainerParent.cpp129
-rw-r--r--dom/serviceworkers/ServiceWorkerContainerParent.h55
-rw-r--r--dom/serviceworkers/ServiceWorkerContainerProxy.cpp153
-rw-r--r--dom/serviceworkers/ServiceWorkerContainerProxy.h47
-rw-r--r--dom/serviceworkers/ServiceWorkerDescriptor.cpp141
-rw-r--r--dom/serviceworkers/ServiceWorkerDescriptor.h100
-rw-r--r--dom/serviceworkers/ServiceWorkerEvents.cpp1278
-rw-r--r--dom/serviceworkers/ServiceWorkerEvents.h309
-rw-r--r--dom/serviceworkers/ServiceWorkerIPCUtils.h35
-rw-r--r--dom/serviceworkers/ServiceWorkerInfo.cpp286
-rw-r--r--dom/serviceworkers/ServiceWorkerInfo.h183
-rw-r--r--dom/serviceworkers/ServiceWorkerInterceptController.cpp173
-rw-r--r--dom/serviceworkers/ServiceWorkerInterceptController.h25
-rw-r--r--dom/serviceworkers/ServiceWorkerJob.cpp220
-rw-r--r--dom/serviceworkers/ServiceWorkerJob.h126
-rw-r--r--dom/serviceworkers/ServiceWorkerJobQueue.cpp120
-rw-r--r--dom/serviceworkers/ServiceWorkerJobQueue.h40
-rw-r--r--dom/serviceworkers/ServiceWorkerManager.cpp3364
-rw-r--r--dom/serviceworkers/ServiceWorkerManager.h439
-rw-r--r--dom/serviceworkers/ServiceWorkerManagerChild.h42
-rw-r--r--dom/serviceworkers/ServiceWorkerManagerParent.cpp106
-rw-r--r--dom/serviceworkers/ServiceWorkerManagerParent.h48
-rw-r--r--dom/serviceworkers/ServiceWorkerOp.cpp1913
-rw-r--r--dom/serviceworkers/ServiceWorkerOp.h195
-rw-r--r--dom/serviceworkers/ServiceWorkerOpArgs.ipdlh189
-rw-r--r--dom/serviceworkers/ServiceWorkerOpPromise.h48
-rw-r--r--dom/serviceworkers/ServiceWorkerParent.cpp62
-rw-r--r--dom/serviceworkers/ServiceWorkerParent.h44
-rw-r--r--dom/serviceworkers/ServiceWorkerPrivate.cpp1678
-rw-r--r--dom/serviceworkers/ServiceWorkerPrivate.h380
-rw-r--r--dom/serviceworkers/ServiceWorkerProxy.cpp122
-rw-r--r--dom/serviceworkers/ServiceWorkerProxy.h61
-rw-r--r--dom/serviceworkers/ServiceWorkerQuotaUtils.cpp327
-rw-r--r--dom/serviceworkers/ServiceWorkerQuotaUtils.h23
-rw-r--r--dom/serviceworkers/ServiceWorkerRegisterJob.cpp57
-rw-r--r--dom/serviceworkers/ServiceWorkerRegisterJob.h33
-rw-r--r--dom/serviceworkers/ServiceWorkerRegistrar.cpp1458
-rw-r--r--dom/serviceworkers/ServiceWorkerRegistrar.h119
-rw-r--r--dom/serviceworkers/ServiceWorkerRegistrarTypes.ipdlh33
-rw-r--r--dom/serviceworkers/ServiceWorkerRegistration.cpp688
-rw-r--r--dom/serviceworkers/ServiceWorkerRegistration.h162
-rw-r--r--dom/serviceworkers/ServiceWorkerRegistrationChild.cpp91
-rw-r--r--dom/serviceworkers/ServiceWorkerRegistrationChild.h52
-rw-r--r--dom/serviceworkers/ServiceWorkerRegistrationDescriptor.cpp274
-rw-r--r--dom/serviceworkers/ServiceWorkerRegistrationDescriptor.h103
-rw-r--r--dom/serviceworkers/ServiceWorkerRegistrationInfo.cpp907
-rw-r--r--dom/serviceworkers/ServiceWorkerRegistrationInfo.h268
-rw-r--r--dom/serviceworkers/ServiceWorkerRegistrationListener.h35
-rw-r--r--dom/serviceworkers/ServiceWorkerRegistrationParent.cpp152
-rw-r--r--dom/serviceworkers/ServiceWorkerRegistrationParent.h58
-rw-r--r--dom/serviceworkers/ServiceWorkerRegistrationProxy.cpp490
-rw-r--r--dom/serviceworkers/ServiceWorkerRegistrationProxy.h92
-rw-r--r--dom/serviceworkers/ServiceWorkerScriptCache.cpp1503
-rw-r--r--dom/serviceworkers/ServiceWorkerScriptCache.h54
-rw-r--r--dom/serviceworkers/ServiceWorkerShutdownBlocker.cpp291
-rw-r--r--dom/serviceworkers/ServiceWorkerShutdownBlocker.h157
-rw-r--r--dom/serviceworkers/ServiceWorkerShutdownState.cpp165
-rw-r--r--dom/serviceworkers/ServiceWorkerShutdownState.h61
-rw-r--r--dom/serviceworkers/ServiceWorkerUnregisterCallback.cpp35
-rw-r--r--dom/serviceworkers/ServiceWorkerUnregisterCallback.h41
-rw-r--r--dom/serviceworkers/ServiceWorkerUnregisterJob.cpp139
-rw-r--r--dom/serviceworkers/ServiceWorkerUnregisterJob.h36
-rw-r--r--dom/serviceworkers/ServiceWorkerUpdateJob.cpp541
-rw-r--r--dom/serviceworkers/ServiceWorkerUpdateJob.h97
-rw-r--r--dom/serviceworkers/ServiceWorkerUtils.cpp146
-rw-r--r--dom/serviceworkers/ServiceWorkerUtils.h61
-rw-r--r--dom/serviceworkers/docs/telemetry.md42
-rw-r--r--dom/serviceworkers/moz.build130
-rw-r--r--dom/serviceworkers/test/ForceRefreshChild.sys.mjs12
-rw-r--r--dom/serviceworkers/test/ForceRefreshParent.sys.mjs77
-rw-r--r--dom/serviceworkers/test/abrupt_completion_worker.js18
-rw-r--r--dom/serviceworkers/test/activate_event_error_worker.js4
-rw-r--r--dom/serviceworkers/test/async_waituntil_worker.js53
-rw-r--r--dom/serviceworkers/test/blocking_install_event_worker.js22
-rw-r--r--dom/serviceworkers/test/browser-common.ini52
-rw-r--r--dom/serviceworkers/test/browser-dFPI.ini7
-rw-r--r--dom/serviceworkers/test/browser.ini4
-rw-r--r--dom/serviceworkers/test/browser_antitracking.js104
-rw-r--r--dom/serviceworkers/test/browser_antitracking_subiframes.js105
-rw-r--r--dom/serviceworkers/test/browser_base_force_refresh.html27
-rw-r--r--dom/serviceworkers/test/browser_cached_force_refresh.html59
-rw-r--r--dom/serviceworkers/test/browser_devtools_serviceworker_interception.js264
-rw-r--r--dom/serviceworkers/test/browser_download.js97
-rw-r--r--dom/serviceworkers/test/browser_download_canceled.js176
-rw-r--r--dom/serviceworkers/test/browser_force_refresh.js87
-rw-r--r--dom/serviceworkers/test/browser_head.js319
-rw-r--r--dom/serviceworkers/test/browser_intercepted_channel_process_swap.js110
-rw-r--r--dom/serviceworkers/test/browser_intercepted_worker_script.js102
-rw-r--r--dom/serviceworkers/test/browser_navigationPreload_read_after_respondWith.js115
-rw-r--r--dom/serviceworkers/test/browser_navigation_fetch_fault_handling.js272
-rw-r--r--dom/serviceworkers/test/browser_remote_type_process_swap.js138
-rw-r--r--dom/serviceworkers/test/browser_storage_permission.js297
-rw-r--r--dom/serviceworkers/test/browser_storage_recovery.js152
-rw-r--r--dom/serviceworkers/test/browser_unregister_with_containers.js153
-rw-r--r--dom/serviceworkers/test/browser_userContextId_openWindow.js163
-rw-r--r--dom/serviceworkers/test/bug1151916_driver.html53
-rw-r--r--dom/serviceworkers/test/bug1151916_worker.js15
-rw-r--r--dom/serviceworkers/test/bug1240436_worker.js2
-rw-r--r--dom/serviceworkers/test/chrome-common.ini21
-rw-r--r--dom/serviceworkers/test/chrome-dFPI.ini7
-rw-r--r--dom/serviceworkers/test/chrome.ini4
-rw-r--r--dom/serviceworkers/test/chrome_helpers.js71
-rw-r--r--dom/serviceworkers/test/claim_clients/client.html43
-rw-r--r--dom/serviceworkers/test/claim_oninstall_worker.js7
-rw-r--r--dom/serviceworkers/test/claim_worker_1.js32
-rw-r--r--dom/serviceworkers/test/claim_worker_2.js34
-rw-r--r--dom/serviceworkers/test/close_test.js22
-rw-r--r--dom/serviceworkers/test/console_monitor.js44
-rw-r--r--dom/serviceworkers/test/controller/index.html72
-rw-r--r--dom/serviceworkers/test/create_another_sharedWorker.html6
-rw-r--r--dom/serviceworkers/test/download/window.html46
-rw-r--r--dom/serviceworkers/test/download/worker.js34
-rw-r--r--dom/serviceworkers/test/download_canceled/page_download_canceled.html58
-rw-r--r--dom/serviceworkers/test/download_canceled/server-stream-download.sjs133
-rw-r--r--dom/serviceworkers/test/download_canceled/sw_download_canceled.js150
-rw-r--r--dom/serviceworkers/test/empty.html0
-rw-r--r--dom/serviceworkers/test/empty.js0
-rw-r--r--dom/serviceworkers/test/empty_with_utils.html13
-rw-r--r--dom/serviceworkers/test/error_reporting_helpers.js73
-rw-r--r--dom/serviceworkers/test/eval_worker.js1
-rw-r--r--dom/serviceworkers/test/eventsource/eventsource.resource22
-rw-r--r--dom/serviceworkers/test/eventsource/eventsource.resource^headers^3
-rw-r--r--dom/serviceworkers/test/eventsource/eventsource_cors_response.html75
-rw-r--r--dom/serviceworkers/test/eventsource/eventsource_cors_response_intercept_worker.js30
-rw-r--r--dom/serviceworkers/test/eventsource/eventsource_mixed_content_cors_response.html75
-rw-r--r--dom/serviceworkers/test/eventsource/eventsource_mixed_content_cors_response_intercept_worker.js29
-rw-r--r--dom/serviceworkers/test/eventsource/eventsource_opaque_response.html75
-rw-r--r--dom/serviceworkers/test/eventsource/eventsource_opaque_response_intercept_worker.js30
-rw-r--r--dom/serviceworkers/test/eventsource/eventsource_register_worker.html27
-rw-r--r--dom/serviceworkers/test/eventsource/eventsource_synthetic_response.html75
-rw-r--r--dom/serviceworkers/test/eventsource/eventsource_synthetic_response_intercept_worker.js27
-rw-r--r--dom/serviceworkers/test/eventsource/eventsource_worker_helper.js17
-rw-r--r--dom/serviceworkers/test/fetch.js33
-rw-r--r--dom/serviceworkers/test/fetch/cookie/cookie_test.js11
-rw-r--r--dom/serviceworkers/test/fetch/cookie/register.html19
-rw-r--r--dom/serviceworkers/test/fetch/cookie/unregister.html12
-rw-r--r--dom/serviceworkers/test/fetch/deliver-gzip.sjs23
-rw-r--r--dom/serviceworkers/test/fetch/fetch_tests.js716
-rw-r--r--dom/serviceworkers/test/fetch/fetch_worker_script.js28
-rw-r--r--dom/serviceworkers/test/fetch/hsts/embedder.html7
-rw-r--r--dom/serviceworkers/test/fetch/hsts/hsts_test.js11
-rw-r--r--dom/serviceworkers/test/fetch/hsts/image-20px.pngbin0 -> 87 bytes
-rw-r--r--dom/serviceworkers/test/fetch/hsts/image-40px.pngbin0 -> 123 bytes
-rw-r--r--dom/serviceworkers/test/fetch/hsts/image.html13
-rw-r--r--dom/serviceworkers/test/fetch/hsts/realindex.html8
-rw-r--r--dom/serviceworkers/test/fetch/hsts/register.html14
-rw-r--r--dom/serviceworkers/test/fetch/hsts/register.html^headers^2
-rw-r--r--dom/serviceworkers/test/fetch/hsts/unregister.html12
-rw-r--r--dom/serviceworkers/test/fetch/https/clonedresponse/https_test.js19
-rw-r--r--dom/serviceworkers/test/fetch/https/clonedresponse/index.html4
-rw-r--r--dom/serviceworkers/test/fetch/https/clonedresponse/register.html14
-rw-r--r--dom/serviceworkers/test/fetch/https/clonedresponse/unregister.html12
-rw-r--r--dom/serviceworkers/test/fetch/https/https_test.js31
-rw-r--r--dom/serviceworkers/test/fetch/https/index.html4
-rw-r--r--dom/serviceworkers/test/fetch/https/register.html20
-rw-r--r--dom/serviceworkers/test/fetch/https/unregister.html12
-rw-r--r--dom/serviceworkers/test/fetch/imagecache-maxage/image-20px.pngbin0 -> 87 bytes
-rw-r--r--dom/serviceworkers/test/fetch/imagecache-maxage/image-40px.pngbin0 -> 123 bytes
-rw-r--r--dom/serviceworkers/test/fetch/imagecache-maxage/index.html29
-rw-r--r--dom/serviceworkers/test/fetch/imagecache-maxage/maxage_test.js45
-rw-r--r--dom/serviceworkers/test/fetch/imagecache-maxage/register.html14
-rw-r--r--dom/serviceworkers/test/fetch/imagecache-maxage/unregister.html12
-rw-r--r--dom/serviceworkers/test/fetch/imagecache/image-20px.pngbin0 -> 87 bytes
-rw-r--r--dom/serviceworkers/test/fetch/imagecache/image-40px.pngbin0 -> 123 bytes
-rw-r--r--dom/serviceworkers/test/fetch/imagecache/imagecache_test.js15
-rw-r--r--dom/serviceworkers/test/fetch/imagecache/index.html20
-rw-r--r--dom/serviceworkers/test/fetch/imagecache/postmortem.html9
-rw-r--r--dom/serviceworkers/test/fetch/imagecache/register.html16
-rw-r--r--dom/serviceworkers/test/fetch/imagecache/unregister.html12
-rw-r--r--dom/serviceworkers/test/fetch/importscript-mixedcontent/https_test.js31
-rw-r--r--dom/serviceworkers/test/fetch/importscript-mixedcontent/register.html14
-rw-r--r--dom/serviceworkers/test/fetch/importscript-mixedcontent/unregister.html12
-rw-r--r--dom/serviceworkers/test/fetch/index.html191
-rw-r--r--dom/serviceworkers/test/fetch/interrupt.sjs20
-rw-r--r--dom/serviceworkers/test/fetch/origin/https/index-https.sjs8
-rw-r--r--dom/serviceworkers/test/fetch/origin/https/origin_test.js29
-rw-r--r--dom/serviceworkers/test/fetch/origin/https/realindex.html6
-rw-r--r--dom/serviceworkers/test/fetch/origin/https/realindex.html^headers^1
-rw-r--r--dom/serviceworkers/test/fetch/origin/https/register.html14
-rw-r--r--dom/serviceworkers/test/fetch/origin/https/unregister.html12
-rw-r--r--dom/serviceworkers/test/fetch/origin/index-to-https.sjs8
-rw-r--r--dom/serviceworkers/test/fetch/origin/index.sjs8
-rw-r--r--dom/serviceworkers/test/fetch/origin/origin_test.js38
-rw-r--r--dom/serviceworkers/test/fetch/origin/realindex.html6
-rw-r--r--dom/serviceworkers/test/fetch/origin/realindex.html^headers^1
-rw-r--r--dom/serviceworkers/test/fetch/origin/register.html14
-rw-r--r--dom/serviceworkers/test/fetch/origin/unregister.html12
-rw-r--r--dom/serviceworkers/test/fetch/plugin/plugins.html43
-rw-r--r--dom/serviceworkers/test/fetch/plugin/worker.js15
-rw-r--r--dom/serviceworkers/test/fetch/real-file.txt1
-rw-r--r--dom/serviceworkers/test/fetch/redirect.sjs4
-rw-r--r--dom/serviceworkers/test/fetch/requesturl/index.html7
-rw-r--r--dom/serviceworkers/test/fetch/requesturl/redirect.sjs8
-rw-r--r--dom/serviceworkers/test/fetch/requesturl/redirector.html2
-rw-r--r--dom/serviceworkers/test/fetch/requesturl/register.html14
-rw-r--r--dom/serviceworkers/test/fetch/requesturl/requesturl_test.js21
-rw-r--r--dom/serviceworkers/test/fetch/requesturl/secret.html5
-rw-r--r--dom/serviceworkers/test/fetch/requesturl/unregister.html12
-rw-r--r--dom/serviceworkers/test/fetch/sandbox/index.html5
-rw-r--r--dom/serviceworkers/test/fetch/sandbox/intercepted_index.html5
-rw-r--r--dom/serviceworkers/test/fetch/sandbox/register.html14
-rw-r--r--dom/serviceworkers/test/fetch/sandbox/sandbox_test.js5
-rw-r--r--dom/serviceworkers/test/fetch/sandbox/unregister.html12
-rw-r--r--dom/serviceworkers/test/fetch/upgrade-insecure/embedder.html10
-rw-r--r--dom/serviceworkers/test/fetch/upgrade-insecure/embedder.html^headers^1
-rw-r--r--dom/serviceworkers/test/fetch/upgrade-insecure/image-20px.pngbin0 -> 87 bytes
-rw-r--r--dom/serviceworkers/test/fetch/upgrade-insecure/image-40px.pngbin0 -> 123 bytes
-rw-r--r--dom/serviceworkers/test/fetch/upgrade-insecure/image.html13
-rw-r--r--dom/serviceworkers/test/fetch/upgrade-insecure/realindex.html4
-rw-r--r--dom/serviceworkers/test/fetch/upgrade-insecure/register.html14
-rw-r--r--dom/serviceworkers/test/fetch/upgrade-insecure/unregister.html12
-rw-r--r--dom/serviceworkers/test/fetch/upgrade-insecure/upgrade-insecure_test.js11
-rw-r--r--dom/serviceworkers/test/fetch_event_worker.js364
-rw-r--r--dom/serviceworkers/test/file_blob_response_worker.js39
-rw-r--r--dom/serviceworkers/test/file_js_cache.html10
-rw-r--r--dom/serviceworkers/test/file_js_cache.js5
-rw-r--r--dom/serviceworkers/test/file_js_cache_cleanup.js16
-rw-r--r--dom/serviceworkers/test/file_js_cache_save_after_load.html10
-rw-r--r--dom/serviceworkers/test/file_js_cache_save_after_load.js15
-rw-r--r--dom/serviceworkers/test/file_js_cache_syntax_error.html10
-rw-r--r--dom/serviceworkers/test/file_js_cache_syntax_error.js1
-rw-r--r--dom/serviceworkers/test/file_js_cache_with_sri.html12
-rw-r--r--dom/serviceworkers/test/file_notification_openWindow.html26
-rw-r--r--dom/serviceworkers/test/file_userContextId_openWindow.js3
-rw-r--r--dom/serviceworkers/test/force_refresh_browser_worker.js42
-rw-r--r--dom/serviceworkers/test/force_refresh_worker.js43
-rw-r--r--dom/serviceworkers/test/gtest/TestReadWrite.cpp952
-rw-r--r--dom/serviceworkers/test/gtest/moz.build13
-rw-r--r--dom/serviceworkers/test/gzip_redirect_worker.js15
-rw-r--r--dom/serviceworkers/test/header_checker.sjs9
-rw-r--r--dom/serviceworkers/test/hello.html9
-rw-r--r--dom/serviceworkers/test/importscript.sjs11
-rw-r--r--dom/serviceworkers/test/importscript_worker.js46
-rw-r--r--dom/serviceworkers/test/install_event_error_worker.js9
-rw-r--r--dom/serviceworkers/test/install_event_worker.js3
-rw-r--r--dom/serviceworkers/test/intercepted_channel_process_swap_worker.js7
-rw-r--r--dom/serviceworkers/test/isolated/README.md19
-rw-r--r--dom/serviceworkers/test/isolated/multi-e10s-update/browser.ini7
-rw-r--r--dom/serviceworkers/test/isolated/multi-e10s-update/browser_multie10s_update.js143
-rw-r--r--dom/serviceworkers/test/isolated/multi-e10s-update/file_multie10s_update.html40
-rw-r--r--dom/serviceworkers/test/isolated/multi-e10s-update/server_multie10s_update.sjs100
-rw-r--r--dom/serviceworkers/test/lazy_worker.js8
-rw-r--r--dom/serviceworkers/test/lorem_script.js8
-rw-r--r--dom/serviceworkers/test/match_all_advanced_worker.js5
-rw-r--r--dom/serviceworkers/test/match_all_client/match_all_client_id.html31
-rw-r--r--dom/serviceworkers/test/match_all_client_id_worker.js28
-rw-r--r--dom/serviceworkers/test/match_all_clients/match_all_controlled.html83
-rw-r--r--dom/serviceworkers/test/match_all_properties_worker.js28
-rw-r--r--dom/serviceworkers/test/match_all_worker.js10
-rw-r--r--dom/serviceworkers/test/message_posting_worker.js8
-rw-r--r--dom/serviceworkers/test/message_receiver.html6
-rw-r--r--dom/serviceworkers/test/mochitest-common.ini347
-rw-r--r--dom/serviceworkers/test/mochitest-dFPI.ini11
-rw-r--r--dom/serviceworkers/test/mochitest.ini35
-rw-r--r--dom/serviceworkers/test/navigationPreload_page.html1
-rw-r--r--dom/serviceworkers/test/network_with_utils.html14
-rw-r--r--dom/serviceworkers/test/nofetch_handler_worker.js14
-rw-r--r--dom/serviceworkers/test/notification/listener.html27
-rw-r--r--dom/serviceworkers/test/notification/register.html11
-rw-r--r--dom/serviceworkers/test/notification/unregister.html12
-rw-r--r--dom/serviceworkers/test/notification_alt/register.html11
-rw-r--r--dom/serviceworkers/test/notification_alt/unregister.html12
-rw-r--r--dom/serviceworkers/test/notification_constructor_error.js1
-rw-r--r--dom/serviceworkers/test/notification_get_sw.js55
-rw-r--r--dom/serviceworkers/test/notification_openWindow_worker.js25
-rw-r--r--dom/serviceworkers/test/notificationclick-otherwindow.html30
-rw-r--r--dom/serviceworkers/test/notificationclick.html27
-rw-r--r--dom/serviceworkers/test/notificationclick.js23
-rw-r--r--dom/serviceworkers/test/notificationclick_focus.html28
-rw-r--r--dom/serviceworkers/test/notificationclick_focus.js49
-rw-r--r--dom/serviceworkers/test/notificationclose.html37
-rw-r--r--dom/serviceworkers/test/notificationclose.js31
-rw-r--r--dom/serviceworkers/test/notify_loaded.js1
-rw-r--r--dom/serviceworkers/test/onmessageerror_worker.js54
-rw-r--r--dom/serviceworkers/test/opaque_intercept_worker.js40
-rw-r--r--dom/serviceworkers/test/openWindow_worker.js178
-rw-r--r--dom/serviceworkers/test/open_window/client.sjs69
-rw-r--r--dom/serviceworkers/test/page_post_controlled.html27
-rw-r--r--dom/serviceworkers/test/parse_error_worker.js2
-rw-r--r--dom/serviceworkers/test/redirect.sjs4
-rw-r--r--dom/serviceworkers/test/redirect_post.sjs39
-rw-r--r--dom/serviceworkers/test/redirect_serviceworker.sjs7
-rw-r--r--dom/serviceworkers/test/register_https.html15
-rw-r--r--dom/serviceworkers/test/sanitize/example_check_and_unregister.html22
-rw-r--r--dom/serviceworkers/test/sanitize/frame.html11
-rw-r--r--dom/serviceworkers/test/sanitize/register.html9
-rw-r--r--dom/serviceworkers/test/sanitize_worker.js5
-rw-r--r--dom/serviceworkers/test/scope/scope_worker.js2
-rw-r--r--dom/serviceworkers/test/script_file_upload.js15
-rw-r--r--dom/serviceworkers/test/self_update_worker.sjs42
-rw-r--r--dom/serviceworkers/test/server_file_upload.sjs22
-rw-r--r--dom/serviceworkers/test/service_worker.js9
-rw-r--r--dom/serviceworkers/test/service_worker_client.html28
-rw-r--r--dom/serviceworkers/test/serviceworker.html12
-rw-r--r--dom/serviceworkers/test/serviceworker_not_sharedworker.js20
-rw-r--r--dom/serviceworkers/test/serviceworker_wrapper.js92
-rw-r--r--dom/serviceworkers/test/serviceworkerinfo_iframe.html27
-rw-r--r--dom/serviceworkers/test/serviceworkermanager_iframe.html34
-rw-r--r--dom/serviceworkers/test/serviceworkerregistrationinfo_iframe.html30
-rw-r--r--dom/serviceworkers/test/sharedWorker_fetch.js30
-rw-r--r--dom/serviceworkers/test/simple_fetch_worker.js18
-rw-r--r--dom/serviceworkers/test/simpleregister/index.html51
-rw-r--r--dom/serviceworkers/test/simpleregister/ready.html14
-rw-r--r--dom/serviceworkers/test/skip_waiting_installed_worker.js6
-rw-r--r--dom/serviceworkers/test/skip_waiting_scope/index.html33
-rw-r--r--dom/serviceworkers/test/source_message_posting_worker.js16
-rw-r--r--dom/serviceworkers/test/storage_recovery_worker.sjs23
-rw-r--r--dom/serviceworkers/test/streamfilter_server.sjs9
-rw-r--r--dom/serviceworkers/test/streamfilter_worker.js9
-rw-r--r--dom/serviceworkers/test/strict_mode_warning.js4
-rw-r--r--dom/serviceworkers/test/sw_bad_mime_type.js1
-rw-r--r--dom/serviceworkers/test/sw_bad_mime_type.js^headers^1
-rw-r--r--dom/serviceworkers/test/sw_clients/file_blob_upload_frame.html76
-rw-r--r--dom/serviceworkers/test/sw_clients/navigator.html34
-rw-r--r--dom/serviceworkers/test/sw_clients/refresher.html38
-rw-r--r--dom/serviceworkers/test/sw_clients/refresher_cached.html37
-rw-r--r--dom/serviceworkers/test/sw_clients/refresher_cached_compressed.htmlbin0 -> 560 bytes
-rw-r--r--dom/serviceworkers/test/sw_clients/refresher_cached_compressed.html^headers^2
-rw-r--r--dom/serviceworkers/test/sw_clients/refresher_compressed.htmlbin0 -> 609 bytes
-rw-r--r--dom/serviceworkers/test/sw_clients/refresher_compressed.html^headers^2
-rw-r--r--dom/serviceworkers/test/sw_clients/service_worker_controlled.html38
-rw-r--r--dom/serviceworkers/test/sw_clients/simple.html29
-rw-r--r--dom/serviceworkers/test/sw_file_upload.js16
-rw-r--r--dom/serviceworkers/test/sw_respondwith_serviceworker.js24
-rw-r--r--dom/serviceworkers/test/sw_storage_not_allow.js33
-rw-r--r--dom/serviceworkers/test/sw_with_navigationPreload.js28
-rw-r--r--dom/serviceworkers/test/swa/worker_scope_different.js0
-rw-r--r--dom/serviceworkers/test/swa/worker_scope_different.js^headers^1
-rw-r--r--dom/serviceworkers/test/swa/worker_scope_different2.js0
-rw-r--r--dom/serviceworkers/test/swa/worker_scope_different2.js^headers^1
-rw-r--r--dom/serviceworkers/test/swa/worker_scope_precise.js0
-rw-r--r--dom/serviceworkers/test/swa/worker_scope_precise.js^headers^1
-rw-r--r--dom/serviceworkers/test/swa/worker_scope_too_deep.js0
-rw-r--r--dom/serviceworkers/test/swa/worker_scope_too_deep.js^headers^1
-rw-r--r--dom/serviceworkers/test/swa/worker_scope_too_narrow.js0
-rw-r--r--dom/serviceworkers/test/swa/worker_scope_too_narrow.js^headers^1
-rw-r--r--dom/serviceworkers/test/test_abrupt_completion.html144
-rw-r--r--dom/serviceworkers/test/test_async_waituntil.html91
-rw-r--r--dom/serviceworkers/test/test_bad_script_cache.html96
-rw-r--r--dom/serviceworkers/test/test_bug1151916.html104
-rw-r--r--dom/serviceworkers/test/test_bug1240436.html34
-rw-r--r--dom/serviceworkers/test/test_bug1408734.html52
-rw-r--r--dom/serviceworkers/test/test_claim.html171
-rw-r--r--dom/serviceworkers/test/test_claim_oninstall.html77
-rw-r--r--dom/serviceworkers/test/test_controller.html83
-rw-r--r--dom/serviceworkers/test/test_cookie_fetch.html64
-rw-r--r--dom/serviceworkers/test/test_cross_origin_url_after_redirect.html50
-rw-r--r--dom/serviceworkers/test/test_csp_upgrade-insecure_intercept.html55
-rw-r--r--dom/serviceworkers/test/test_devtools_bypass_serviceworker.html107
-rw-r--r--dom/serviceworkers/test/test_devtools_track_serviceworker_time.html236
-rw-r--r--dom/serviceworkers/test/test_empty_serviceworker.html46
-rw-r--r--dom/serviceworkers/test/test_error_reporting.html241
-rw-r--r--dom/serviceworkers/test/test_escapedSlashes.html102
-rw-r--r--dom/serviceworkers/test/test_eval_allowed.html51
-rw-r--r--dom/serviceworkers/test/test_eval_allowed.html^headers^1
-rw-r--r--dom/serviceworkers/test/test_event_listener_leaks.html63
-rw-r--r--dom/serviceworkers/test/test_eventsource_intercept.html103
-rw-r--r--dom/serviceworkers/test/test_fetch_event.html75
-rw-r--r--dom/serviceworkers/test/test_fetch_event_with_thirdpartypref.html90
-rw-r--r--dom/serviceworkers/test/test_fetch_integrity.html228
-rw-r--r--dom/serviceworkers/test/test_file_blob_response.html78
-rw-r--r--dom/serviceworkers/test/test_file_blob_upload.html146
-rw-r--r--dom/serviceworkers/test/test_file_upload.html68
-rw-r--r--dom/serviceworkers/test/test_force_refresh.html105
-rw-r--r--dom/serviceworkers/test/test_gzip_redirect.html88
-rw-r--r--dom/serviceworkers/test/test_hsts_upgrade_intercept.html66
-rw-r--r--dom/serviceworkers/test/test_https_fetch.html62
-rw-r--r--dom/serviceworkers/test/test_https_fetch_cloned_response.html56
-rw-r--r--dom/serviceworkers/test/test_https_origin_after_redirect.html57
-rw-r--r--dom/serviceworkers/test/test_https_origin_after_redirect_cached.html57
-rw-r--r--dom/serviceworkers/test/test_https_synth_fetch_from_cached_sw.html69
-rw-r--r--dom/serviceworkers/test/test_imagecache.html55
-rw-r--r--dom/serviceworkers/test/test_imagecache_max_age.html71
-rw-r--r--dom/serviceworkers/test/test_importscript.html74
-rw-r--r--dom/serviceworkers/test/test_importscript_mixedcontent.html53
-rw-r--r--dom/serviceworkers/test/test_install_event.html143
-rw-r--r--dom/serviceworkers/test/test_install_event_gc.html121
-rw-r--r--dom/serviceworkers/test/test_installation_simple.html208
-rw-r--r--dom/serviceworkers/test/test_match_all.html83
-rw-r--r--dom/serviceworkers/test/test_match_all_advanced.html102
-rw-r--r--dom/serviceworkers/test/test_match_all_client_id.html95
-rw-r--r--dom/serviceworkers/test/test_match_all_client_properties.html101
-rw-r--r--dom/serviceworkers/test/test_navigationPreload_disable_crash.html52
-rw-r--r--dom/serviceworkers/test/test_navigator.html40
-rw-r--r--dom/serviceworkers/test/test_nofetch_handler.html57
-rw-r--r--dom/serviceworkers/test/test_not_intercept_plugin.html75
-rw-r--r--dom/serviceworkers/test/test_notification_constructor_error.html51
-rw-r--r--dom/serviceworkers/test/test_notification_get.html213
-rw-r--r--dom/serviceworkers/test/test_notification_openWindow.html91
-rw-r--r--dom/serviceworkers/test/test_notificationclick-otherwindow.html65
-rw-r--r--dom/serviceworkers/test/test_notificationclick.html66
-rw-r--r--dom/serviceworkers/test/test_notificationclick_focus.html66
-rw-r--r--dom/serviceworkers/test/test_notificationclose.html67
-rw-r--r--dom/serviceworkers/test/test_onmessageerror.html128
-rw-r--r--dom/serviceworkers/test/test_opaque_intercept.html93
-rw-r--r--dom/serviceworkers/test/test_openWindow.html112
-rw-r--r--dom/serviceworkers/test/test_origin_after_redirect.html58
-rw-r--r--dom/serviceworkers/test/test_origin_after_redirect_cached.html58
-rw-r--r--dom/serviceworkers/test/test_origin_after_redirect_to_https.html57
-rw-r--r--dom/serviceworkers/test/test_origin_after_redirect_to_https_cached.html57
-rw-r--r--dom/serviceworkers/test/test_post_message.html80
-rw-r--r--dom/serviceworkers/test/test_post_message_advanced.html109
-rw-r--r--dom/serviceworkers/test/test_post_message_source.html66
-rw-r--r--dom/serviceworkers/test/test_privateBrowsing.html103
-rw-r--r--dom/serviceworkers/test/test_register_base.html34
-rw-r--r--dom/serviceworkers/test/test_register_https_in_http.html45
-rw-r--r--dom/serviceworkers/test/test_sandbox_intercept.html56
-rw-r--r--dom/serviceworkers/test/test_sanitize.html86
-rw-r--r--dom/serviceworkers/test/test_sanitize_domain.html89
-rw-r--r--dom/serviceworkers/test/test_scopes.html143
-rw-r--r--dom/serviceworkers/test/test_script_loader_intercepted_js_cache.html224
-rw-r--r--dom/serviceworkers/test/test_self_update_worker.html136
-rw-r--r--dom/serviceworkers/test/test_service_worker_allowed.html74
-rw-r--r--dom/serviceworkers/test/test_serviceworker.html79
-rw-r--r--dom/serviceworkers/test/test_serviceworker_header.html41
-rw-r--r--dom/serviceworkers/test/test_serviceworker_interfaces.html116
-rw-r--r--dom/serviceworkers/test/test_serviceworker_interfaces.js549
-rw-r--r--dom/serviceworkers/test/test_serviceworker_not_sharedworker.html66
-rw-r--r--dom/serviceworkers/test/test_serviceworkerinfo.xhtml114
-rw-r--r--dom/serviceworkers/test/test_serviceworkermanager.xhtml79
-rw-r--r--dom/serviceworkers/test/test_serviceworkerregistrationinfo.xhtml155
-rw-r--r--dom/serviceworkers/test/test_skip_waiting.html86
-rw-r--r--dom/serviceworkers/test/test_streamfilter.html207
-rw-r--r--dom/serviceworkers/test/test_strict_mode_warning.html42
-rw-r--r--dom/serviceworkers/test/test_third_party_iframes.html263
-rw-r--r--dom/serviceworkers/test/test_unregister.html137
-rw-r--r--dom/serviceworkers/test/test_unresolved_fetch_interception.html95
-rw-r--r--dom/serviceworkers/test/test_workerUnregister.html81
-rw-r--r--dom/serviceworkers/test/test_workerUpdate.html63
-rw-r--r--dom/serviceworkers/test/test_worker_reference_gc_timeout.html76
-rw-r--r--dom/serviceworkers/test/test_workerupdatefoundevent.html91
-rw-r--r--dom/serviceworkers/test/test_xslt.html117
-rw-r--r--dom/serviceworkers/test/thirdparty/iframe1.html42
-rw-r--r--dom/serviceworkers/test/thirdparty/iframe2.html14
-rw-r--r--dom/serviceworkers/test/thirdparty/register.html29
-rw-r--r--dom/serviceworkers/test/thirdparty/sw.js33
-rw-r--r--dom/serviceworkers/test/thirdparty/unregister.html19
-rw-r--r--dom/serviceworkers/test/thirdparty/worker.js1
-rw-r--r--dom/serviceworkers/test/unregister/index.html26
-rw-r--r--dom/serviceworkers/test/unregister/unregister.html21
-rw-r--r--dom/serviceworkers/test/unresolved_fetch_worker.js18
-rw-r--r--dom/serviceworkers/test/update_worker.sjs12
-rw-r--r--dom/serviceworkers/test/updatefoundevent.html13
-rw-r--r--dom/serviceworkers/test/utils.js136
-rw-r--r--dom/serviceworkers/test/window_party_iframes.html18
-rw-r--r--dom/serviceworkers/test/worker.js1
-rw-r--r--dom/serviceworkers/test/worker2.js1
-rw-r--r--dom/serviceworkers/test/worker3.js1
-rw-r--r--dom/serviceworkers/test/workerUpdate/update.html23
-rw-r--r--dom/serviceworkers/test/worker_unregister.js22
-rw-r--r--dom/serviceworkers/test/worker_update.js25
-rw-r--r--dom/serviceworkers/test/worker_updatefoundevent.js20
-rw-r--r--dom/serviceworkers/test/worker_updatefoundevent2.js1
-rw-r--r--dom/serviceworkers/test/xslt/test.xml6
-rw-r--r--dom/serviceworkers/test/xslt/xslt.sjs12
-rw-r--r--dom/serviceworkers/test/xslt_worker.js58
487 files changed, 45632 insertions, 0 deletions
diff --git a/dom/serviceworkers/FetchEventOpChild.cpp b/dom/serviceworkers/FetchEventOpChild.cpp
new file mode 100644
index 0000000000..e7e068a864
--- /dev/null
+++ b/dom/serviceworkers/FetchEventOpChild.cpp
@@ -0,0 +1,600 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "FetchEventOpChild.h"
+
+#include <utility>
+
+#include "MainThreadUtils.h"
+#include "nsContentPolicyUtils.h"
+#include "nsContentUtils.h"
+#include "nsDebug.h"
+#include "nsError.h"
+#include "nsIChannel.h"
+#include "nsIConsoleReportCollector.h"
+#include "nsIContentPolicy.h"
+#include "nsIInputStream.h"
+#include "nsILoadInfo.h"
+#include "nsINetworkInterceptController.h"
+#include "nsIObserverService.h"
+#include "nsIScriptError.h"
+#include "nsISupportsImpl.h"
+#include "nsIURI.h"
+#include "nsNetUtil.h"
+#include "nsProxyRelease.h"
+#include "nsTArray.h"
+#include "nsThreadUtils.h"
+
+#include "ServiceWorkerPrivate.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/LoadInfo.h"
+#include "mozilla/Services.h"
+#include "mozilla/StaticPrefs_dom.h"
+#include "mozilla/Telemetry.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/Unused.h"
+#include "mozilla/ipc/BackgroundChild.h"
+#include "mozilla/dom/FetchService.h"
+#include "mozilla/dom/InternalHeaders.h"
+#include "mozilla/dom/InternalResponse.h"
+#include "mozilla/dom/PRemoteWorkerControllerChild.h"
+#include "mozilla/dom/ServiceWorkerRegistrationInfo.h"
+#include "mozilla/net/NeckoChannelParams.h"
+
+namespace mozilla::dom {
+
+namespace {
+
+bool CSPPermitsResponse(nsILoadInfo* aLoadInfo,
+ SafeRefPtr<InternalResponse> aResponse,
+ const nsACString& aWorkerScriptSpec) {
+ AssertIsOnMainThread();
+ MOZ_ASSERT(aLoadInfo);
+
+ nsCString url = aResponse->GetUnfilteredURL();
+ if (url.IsEmpty()) {
+ // Synthetic response.
+ url = aWorkerScriptSpec;
+ }
+
+ nsCOMPtr<nsIURI> uri;
+ nsresult rv = NS_NewURI(getter_AddRefs(uri), url, nullptr, nullptr);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return false;
+ }
+
+ int16_t decision = nsIContentPolicy::ACCEPT;
+ rv = NS_CheckContentLoadPolicy(uri, aLoadInfo, ""_ns, &decision);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return false;
+ }
+
+ return decision == nsIContentPolicy::ACCEPT;
+}
+
+void AsyncLog(nsIInterceptedChannel* aChannel, const nsACString& aScriptSpec,
+ uint32_t aLineNumber, uint32_t aColumnNumber,
+ const nsACString& aMessageName, nsTArray<nsString>&& aParams) {
+ AssertIsOnMainThread();
+ MOZ_ASSERT(aChannel);
+
+ nsCOMPtr<nsIConsoleReportCollector> reporter =
+ aChannel->GetConsoleReportCollector();
+
+ if (reporter) {
+ // NOTE: is appears that `const nsTArray<nsString>&` is required for
+ // nsIConsoleReportCollector::AddConsoleReport to resolve to the correct
+ // overload.
+ const nsTArray<nsString> params = std::move(aParams);
+
+ reporter->AddConsoleReport(
+ nsIScriptError::errorFlag, "Service Worker Interception"_ns,
+ nsContentUtils::eDOM_PROPERTIES, aScriptSpec, aLineNumber,
+ aColumnNumber, aMessageName, params);
+ }
+}
+
+class SynthesizeResponseWatcher final : public nsIInterceptedBodyCallback {
+ public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+
+ SynthesizeResponseWatcher(
+ const nsMainThreadPtrHandle<nsIInterceptedChannel>& aInterceptedChannel,
+ const nsMainThreadPtrHandle<ServiceWorkerRegistrationInfo>& aRegistration,
+ const bool aIsNonSubresourceRequest,
+ FetchEventRespondWithClosure&& aClosure, nsAString&& aRequestURL)
+ : mInterceptedChannel(aInterceptedChannel),
+ mRegistration(aRegistration),
+ mIsNonSubresourceRequest(aIsNonSubresourceRequest),
+ mClosure(std::move(aClosure)),
+ mRequestURL(std::move(aRequestURL)) {
+ AssertIsOnMainThread();
+ MOZ_ASSERT(mInterceptedChannel);
+ MOZ_ASSERT(mRegistration);
+ }
+
+ NS_IMETHOD
+ BodyComplete(nsresult aRv) override {
+ AssertIsOnMainThread();
+ MOZ_ASSERT(mInterceptedChannel);
+
+ if (NS_WARN_IF(NS_FAILED(aRv))) {
+ AsyncLog(mInterceptedChannel, mClosure.respondWithScriptSpec(),
+ mClosure.respondWithLineNumber(),
+ mClosure.respondWithColumnNumber(),
+ "InterceptionFailedWithURL"_ns, {mRequestURL});
+
+ CancelInterception(NS_ERROR_INTERCEPTION_FAILED);
+
+ return NS_OK;
+ }
+
+ nsresult rv = mInterceptedChannel->FinishSynthesizedResponse();
+
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ CancelInterception(rv);
+ }
+
+ mInterceptedChannel = nullptr;
+
+ return NS_OK;
+ }
+
+ // See FetchEventOpChild::MaybeScheduleRegistrationUpdate() for comments.
+ void CancelInterception(nsresult aStatus) {
+ AssertIsOnMainThread();
+ MOZ_ASSERT(mInterceptedChannel);
+ MOZ_ASSERT(mRegistration);
+
+ mInterceptedChannel->CancelInterception(aStatus);
+
+ if (mIsNonSubresourceRequest) {
+ mRegistration->MaybeScheduleUpdate();
+ } else {
+ mRegistration->MaybeScheduleTimeCheckAndUpdate();
+ }
+
+ mInterceptedChannel = nullptr;
+ mRegistration = nullptr;
+ }
+
+ private:
+ ~SynthesizeResponseWatcher() {
+ if (NS_WARN_IF(mInterceptedChannel)) {
+ CancelInterception(NS_ERROR_DOM_ABORT_ERR);
+ }
+ }
+
+ nsMainThreadPtrHandle<nsIInterceptedChannel> mInterceptedChannel;
+ nsMainThreadPtrHandle<ServiceWorkerRegistrationInfo> mRegistration;
+ const bool mIsNonSubresourceRequest;
+ const FetchEventRespondWithClosure mClosure;
+ const nsString mRequestURL;
+};
+
+NS_IMPL_ISUPPORTS(SynthesizeResponseWatcher, nsIInterceptedBodyCallback)
+
+} // anonymous namespace
+
+/* static */ RefPtr<GenericPromise> FetchEventOpChild::SendFetchEvent(
+ PRemoteWorkerControllerChild* aManager,
+ ParentToParentServiceWorkerFetchEventOpArgs&& aArgs,
+ nsCOMPtr<nsIInterceptedChannel> aInterceptedChannel,
+ RefPtr<ServiceWorkerRegistrationInfo> aRegistration,
+ RefPtr<FetchServicePromises>&& aPreloadResponseReadyPromises,
+ RefPtr<KeepAliveToken>&& aKeepAliveToken) {
+ AssertIsOnMainThread();
+ MOZ_ASSERT(aManager);
+ MOZ_ASSERT(aInterceptedChannel);
+ MOZ_ASSERT(aKeepAliveToken);
+
+ FetchEventOpChild* actor = new FetchEventOpChild(
+ std::move(aArgs), std::move(aInterceptedChannel),
+ std::move(aRegistration), std::move(aPreloadResponseReadyPromises),
+ std::move(aKeepAliveToken));
+
+ actor->mWasSent = true;
+ Unused << aManager->SendPFetchEventOpConstructor(actor, actor->mArgs);
+
+ return actor->mPromiseHolder.Ensure(__func__);
+}
+
+FetchEventOpChild::~FetchEventOpChild() {
+ AssertIsOnMainThread();
+ MOZ_ASSERT(mInterceptedChannelHandled);
+ MOZ_DIAGNOSTIC_ASSERT(mPromiseHolder.IsEmpty());
+}
+
+FetchEventOpChild::FetchEventOpChild(
+ ParentToParentServiceWorkerFetchEventOpArgs&& aArgs,
+ nsCOMPtr<nsIInterceptedChannel>&& aInterceptedChannel,
+ RefPtr<ServiceWorkerRegistrationInfo>&& aRegistration,
+ RefPtr<FetchServicePromises>&& aPreloadResponseReadyPromises,
+ RefPtr<KeepAliveToken>&& aKeepAliveToken)
+ : mArgs(std::move(aArgs)),
+ mInterceptedChannel(std::move(aInterceptedChannel)),
+ mRegistration(std::move(aRegistration)),
+ mKeepAliveToken(std::move(aKeepAliveToken)),
+ mPreloadResponseReadyPromises(std::move(aPreloadResponseReadyPromises)) {
+ if (mPreloadResponseReadyPromises) {
+ // This promise should be configured to use synchronous dispatch, so if it's
+ // already resolved when we run this code then the callback will be called
+ // synchronously and pass the preload response with the constructor message.
+ //
+ // Note that it's fine to capture the this pointer in the callbacks because
+ // we disconnect the request in Recv__delete__().
+ mPreloadResponseReadyPromises->GetResponseAvailablePromise()
+ ->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [this](FetchServiceResponse&& aResponse) {
+ if (!mWasSent) {
+ // The actor wasn't sent yet, we can still send the preload
+ // response with it.
+ mArgs.preloadResponse() =
+ Some(aResponse->ToParentToParentInternalResponse());
+ } else {
+ // It's too late to send the preload response with the actor, we
+ // have to send it in a separate message.
+ SendPreloadResponse(
+ aResponse->ToParentToParentInternalResponse());
+ }
+ mPreloadResponseAvailablePromiseRequestHolder.Complete();
+ },
+ [this](const CopyableErrorResult&) {
+ mPreloadResponseAvailablePromiseRequestHolder.Complete();
+ })
+ ->Track(mPreloadResponseAvailablePromiseRequestHolder);
+
+ mPreloadResponseReadyPromises->GetResponseEndPromise()
+ ->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [this](ResponseEndArgs&& aResponse) {
+ if (!mWasSent) {
+ // The actor wasn't sent yet, we can still send the preload
+ // response end args with it.
+ mArgs.preloadResponseEndArgs() = Some(std::move(aResponse));
+ } else {
+ // It's too late to send the preload response end with the
+ // actor, we have to send it in a separate message.
+ SendPreloadResponseEnd(aResponse);
+ }
+ mPreloadResponseReadyPromises = nullptr;
+ mPreloadResponseEndPromiseRequestHolder.Complete();
+ },
+ [this](const CopyableErrorResult&) {
+ mPreloadResponseReadyPromises = nullptr;
+ mPreloadResponseEndPromiseRequestHolder.Complete();
+ })
+ ->Track(mPreloadResponseEndPromiseRequestHolder);
+ }
+}
+
+mozilla::ipc::IPCResult FetchEventOpChild::RecvAsyncLog(
+ const nsCString& aScriptSpec, const uint32_t& aLineNumber,
+ const uint32_t& aColumnNumber, const nsCString& aMessageName,
+ nsTArray<nsString>&& aParams) {
+ AssertIsOnMainThread();
+ MOZ_ASSERT(mInterceptedChannel);
+
+ AsyncLog(mInterceptedChannel, aScriptSpec, aLineNumber, aColumnNumber,
+ aMessageName, std::move(aParams));
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult FetchEventOpChild::RecvRespondWith(
+ ParentToParentFetchEventRespondWithResult&& aResult) {
+ AssertIsOnMainThread();
+
+ switch (aResult.type()) {
+ case ParentToParentFetchEventRespondWithResult::
+ TParentToParentSynthesizeResponseArgs:
+ mInterceptedChannel->SetFetchHandlerStart(
+ aResult.get_ParentToParentSynthesizeResponseArgs()
+ .timeStamps()
+ .fetchHandlerStart());
+ mInterceptedChannel->SetFetchHandlerFinish(
+ aResult.get_ParentToParentSynthesizeResponseArgs()
+ .timeStamps()
+ .fetchHandlerFinish());
+ SynthesizeResponse(
+ std::move(aResult.get_ParentToParentSynthesizeResponseArgs()));
+ break;
+ case ParentToParentFetchEventRespondWithResult::TResetInterceptionArgs:
+ mInterceptedChannel->SetFetchHandlerStart(
+ aResult.get_ResetInterceptionArgs().timeStamps().fetchHandlerStart());
+ mInterceptedChannel->SetFetchHandlerFinish(
+ aResult.get_ResetInterceptionArgs()
+ .timeStamps()
+ .fetchHandlerFinish());
+ ResetInterception(false);
+ break;
+ case ParentToParentFetchEventRespondWithResult::TCancelInterceptionArgs:
+ mInterceptedChannel->SetFetchHandlerStart(
+ aResult.get_CancelInterceptionArgs()
+ .timeStamps()
+ .fetchHandlerStart());
+ mInterceptedChannel->SetFetchHandlerFinish(
+ aResult.get_CancelInterceptionArgs()
+ .timeStamps()
+ .fetchHandlerFinish());
+ CancelInterception(aResult.get_CancelInterceptionArgs().status());
+ break;
+ default:
+ MOZ_CRASH("Unknown IPCFetchEventRespondWithResult type!");
+ break;
+ }
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult FetchEventOpChild::Recv__delete__(
+ const ServiceWorkerFetchEventOpResult& aResult) {
+ AssertIsOnMainThread();
+ MOZ_ASSERT(mRegistration);
+
+ if (NS_WARN_IF(!mInterceptedChannelHandled)) {
+ MOZ_ASSERT(NS_FAILED(aResult.rv()));
+ NS_WARNING(
+ "Failed to handle intercepted network request; canceling "
+ "interception!");
+
+ CancelInterception(aResult.rv());
+ }
+
+ mPromiseHolder.ResolveIfExists(true, __func__);
+
+ // FetchEvent is completed.
+ // Disconnect preload response related promises and cancel the preload.
+ mPreloadResponseAvailablePromiseRequestHolder.DisconnectIfExists();
+ mPreloadResponseEndPromiseRequestHolder.DisconnectIfExists();
+ if (mPreloadResponseReadyPromises) {
+ RefPtr<FetchService> fetchService = FetchService::GetInstance();
+ fetchService->CancelFetch(std::move(mPreloadResponseReadyPromises));
+ }
+
+ /**
+ * This corresponds to the "Fire Functional Event" algorithm's step 9:
+ *
+ * "If the time difference in seconds calculated by the current time minus
+ * registration's last update check time is greater than 84600, invoke Soft
+ * Update algorithm with registration."
+ *
+ * TODO: this is probably being called later than it should be; it should be
+ * called ASAP after dispatching the FetchEvent.
+ */
+ mRegistration->MaybeScheduleTimeCheckAndUpdate();
+
+ return IPC_OK();
+}
+
+void FetchEventOpChild::ActorDestroy(ActorDestroyReason) {
+ AssertIsOnMainThread();
+
+ // If `Recv__delete__` was called, it would have resolved the promise already.
+ mPromiseHolder.RejectIfExists(NS_ERROR_DOM_ABORT_ERR, __func__);
+
+ if (NS_WARN_IF(!mInterceptedChannelHandled)) {
+ Unused << Recv__delete__(NS_ERROR_DOM_ABORT_ERR);
+ }
+}
+
+nsresult FetchEventOpChild::StartSynthesizedResponse(
+ ParentToParentSynthesizeResponseArgs&& aArgs) {
+ AssertIsOnMainThread();
+ MOZ_ASSERT(mInterceptedChannel);
+ MOZ_ASSERT(!mInterceptedChannelHandled);
+ MOZ_ASSERT(mRegistration);
+
+ /**
+ * TODO: moving the IPCInternalResponse won't do anything right now because
+ * there isn't a prefect-forwarding or rvalue-ref-parameter overload of
+ * `InternalResponse::FromIPC().`
+ */
+ SafeRefPtr<InternalResponse> response =
+ InternalResponse::FromIPC(aArgs.internalResponse());
+ if (NS_WARN_IF(!response)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsCOMPtr<nsIChannel> underlyingChannel;
+ nsresult rv =
+ mInterceptedChannel->GetChannel(getter_AddRefs(underlyingChannel));
+ if (NS_WARN_IF(NS_FAILED(rv)) || NS_WARN_IF(!underlyingChannel)) {
+ return NS_FAILED(rv) ? rv : NS_ERROR_FAILURE;
+ }
+
+ nsCOMPtr<nsILoadInfo> loadInfo = underlyingChannel->LoadInfo();
+ if (!CSPPermitsResponse(loadInfo, response.clonePtr(),
+ mArgs.common().workerScriptSpec())) {
+ return NS_ERROR_CONTENT_BLOCKED;
+ }
+
+ MOZ_ASSERT(response->GetChannelInfo().IsInitialized());
+ ChannelInfo channelInfo = response->GetChannelInfo();
+ rv = mInterceptedChannel->SetChannelInfo(&channelInfo);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return NS_ERROR_INTERCEPTION_FAILED;
+ }
+
+ rv = mInterceptedChannel->SynthesizeStatus(
+ response->GetUnfilteredStatus(), response->GetUnfilteredStatusText());
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ AutoTArray<InternalHeaders::Entry, 5> entries;
+ response->UnfilteredHeaders()->GetEntries(entries);
+ for (auto& entry : entries) {
+ mInterceptedChannel->SynthesizeHeader(entry.mName, entry.mValue);
+ }
+
+ auto castLoadInfo = static_cast<mozilla::net::LoadInfo*>(loadInfo.get());
+ castLoadInfo->SynthesizeServiceWorkerTainting(response->GetTainting());
+
+ // Get the preferred alternative data type of the outer channel
+ nsAutoCString preferredAltDataType(""_ns);
+ nsCOMPtr<nsICacheInfoChannel> outerChannel =
+ do_QueryInterface(underlyingChannel);
+ if (outerChannel &&
+ !outerChannel->PreferredAlternativeDataTypes().IsEmpty()) {
+ preferredAltDataType.Assign(
+ outerChannel->PreferredAlternativeDataTypes()[0].type());
+ }
+
+ nsCOMPtr<nsIInputStream> body;
+ if (preferredAltDataType.Equals(response->GetAlternativeDataType())) {
+ body = response->TakeAlternativeBody();
+ }
+ if (!body) {
+ response->GetUnfilteredBody(getter_AddRefs(body));
+ } else {
+ Telemetry::ScalarAdd(Telemetry::ScalarID::SW_ALTERNATIVE_BODY_USED_COUNT,
+ 1);
+ }
+
+ // Propagate the URL to the content if the request mode is not "navigate".
+ // Note that, we only reflect the final URL if the response.redirected is
+ // false. We propagate all the URLs if the response.redirected is true.
+ const IPCInternalRequest& request = mArgs.common().internalRequest();
+ nsAutoCString responseURL;
+ if (request.requestMode() != RequestMode::Navigate) {
+ responseURL = response->GetUnfilteredURL();
+
+ // Similar to how we apply the request fragment to redirects automatically
+ // we also want to apply it automatically when propagating the response
+ // URL from a service worker interception. Currently response.url strips
+ // the fragment, so this will never conflict with an existing fragment
+ // on the response. In the future we will have to check for a response
+ // fragment and avoid overriding in that case.
+ if (!request.fragment().IsEmpty() && !responseURL.IsEmpty()) {
+ MOZ_ASSERT(!responseURL.Contains('#'));
+ responseURL.AppendLiteral("#");
+ responseURL.Append(request.fragment());
+ }
+ }
+
+ nsMainThreadPtrHandle<nsIInterceptedChannel> interceptedChannel(
+ new nsMainThreadPtrHolder<nsIInterceptedChannel>(
+ "nsIInterceptedChannel", mInterceptedChannel, false));
+
+ nsMainThreadPtrHandle<ServiceWorkerRegistrationInfo> registration(
+ new nsMainThreadPtrHolder<ServiceWorkerRegistrationInfo>(
+ "ServiceWorkerRegistrationInfo", mRegistration, false));
+
+ nsCString requestURL = request.urlList().LastElement();
+ if (!request.fragment().IsEmpty()) {
+ requestURL.AppendLiteral("#");
+ requestURL.Append(request.fragment());
+ }
+
+ RefPtr<SynthesizeResponseWatcher> watcher = new SynthesizeResponseWatcher(
+ interceptedChannel, registration,
+ mArgs.common().isNonSubresourceRequest(), std::move(aArgs.closure()),
+ NS_ConvertUTF8toUTF16(responseURL));
+
+ rv = mInterceptedChannel->StartSynthesizedResponse(
+ body, watcher, nullptr /* TODO */, responseURL, response->IsRedirected());
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ nsCOMPtr<nsIObserverService> obsService = services::GetObserverService();
+ if (obsService) {
+ obsService->NotifyObservers(underlyingChannel,
+ "service-worker-synthesized-response", nullptr);
+ }
+
+ return rv;
+}
+
+void FetchEventOpChild::SynthesizeResponse(
+ ParentToParentSynthesizeResponseArgs&& aArgs) {
+ AssertIsOnMainThread();
+ MOZ_ASSERT(mInterceptedChannel);
+ MOZ_ASSERT(!mInterceptedChannelHandled);
+
+ nsresult rv = StartSynthesizedResponse(std::move(aArgs));
+
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ NS_WARNING("Failed to synthesize response!");
+
+ mInterceptedChannel->CancelInterception(rv);
+ }
+
+ mInterceptedChannelHandled = true;
+
+ MaybeScheduleRegistrationUpdate();
+}
+
+void FetchEventOpChild::ResetInterception(bool aBypass) {
+ AssertIsOnMainThread();
+ MOZ_ASSERT(mInterceptedChannel);
+ MOZ_ASSERT(!mInterceptedChannelHandled);
+
+ nsresult rv = mInterceptedChannel->ResetInterception(aBypass);
+
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ NS_WARNING("Failed to resume intercepted network request!");
+
+ mInterceptedChannel->CancelInterception(rv);
+ }
+
+ mInterceptedChannelHandled = true;
+
+ MaybeScheduleRegistrationUpdate();
+}
+
+void FetchEventOpChild::CancelInterception(nsresult aStatus) {
+ AssertIsOnMainThread();
+ MOZ_ASSERT(mInterceptedChannel);
+ MOZ_ASSERT(!mInterceptedChannelHandled);
+ MOZ_ASSERT(NS_FAILED(aStatus));
+
+ // Report a navigation fault if this is a navigation (and we have an active
+ // worker, which should be the case in non-shutdown/content-process-crash
+ // situations).
+ RefPtr<ServiceWorkerInfo> mActive = mRegistration->GetActive();
+ if (mActive && mArgs.common().isNonSubresourceRequest()) {
+ mActive->ReportNavigationFault();
+ // Additional mitigations such as unregistering the registration are handled
+ // in ServiceWorkerRegistrationInfo::MaybeScheduleUpdate which will be
+ // called by MaybeScheduleRegistrationUpdate which gets called by our call
+ // to ResetInterception.
+ if (StaticPrefs::dom_serviceWorkers_mitigations_bypass_on_fault()) {
+ ResetInterception(true);
+ return;
+ }
+ }
+
+ mInterceptedChannel->CancelInterception(aStatus);
+ mInterceptedChannelHandled = true;
+
+ MaybeScheduleRegistrationUpdate();
+}
+
+/**
+ * This corresponds to the "Handle Fetch" algorithm's steps 20.3, 21.2, and
+ * 22.2:
+ *
+ * "If request is a non-subresource request, or request is a subresource
+ * request and the time difference in seconds calculated by the current time
+ * minus registration's last update check time is greater than 86400, invoke
+ * Soft Update algorithm with registration."
+ */
+void FetchEventOpChild::MaybeScheduleRegistrationUpdate() const {
+ AssertIsOnMainThread();
+ MOZ_ASSERT(mRegistration);
+ MOZ_ASSERT(mInterceptedChannelHandled);
+
+ if (mArgs.common().isNonSubresourceRequest()) {
+ mRegistration->MaybeScheduleUpdate();
+ } else {
+ mRegistration->MaybeScheduleTimeCheckAndUpdate();
+ }
+}
+
+} // namespace mozilla::dom
diff --git a/dom/serviceworkers/FetchEventOpChild.h b/dom/serviceworkers/FetchEventOpChild.h
new file mode 100644
index 0000000000..13eecd3415
--- /dev/null
+++ b/dom/serviceworkers/FetchEventOpChild.h
@@ -0,0 +1,92 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_fetcheventopchild_h__
+#define mozilla_dom_fetcheventopchild_h__
+
+#include "nsCOMPtr.h"
+
+#include "mozilla/MozPromise.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/dom/FetchService.h"
+#include "mozilla/dom/PFetchEventOpChild.h"
+#include "mozilla/dom/ServiceWorkerOpArgs.h"
+
+class nsIInterceptedChannel;
+
+namespace mozilla::dom {
+
+class KeepAliveToken;
+class PRemoteWorkerControllerChild;
+class ServiceWorkerRegistrationInfo;
+
+/**
+ * FetchEventOpChild represents an in-flight FetchEvent operation.
+ */
+class FetchEventOpChild final : public PFetchEventOpChild {
+ friend class PFetchEventOpChild;
+
+ public:
+ static RefPtr<GenericPromise> SendFetchEvent(
+ PRemoteWorkerControllerChild* aManager,
+ ParentToParentServiceWorkerFetchEventOpArgs&& aArgs,
+ nsCOMPtr<nsIInterceptedChannel> aInterceptedChannel,
+ RefPtr<ServiceWorkerRegistrationInfo> aRegistrationInfo,
+ RefPtr<FetchServicePromises>&& aPreloadResponseReadyPromises,
+ RefPtr<KeepAliveToken>&& aKeepAliveToken);
+
+ ~FetchEventOpChild();
+
+ private:
+ FetchEventOpChild(
+ ParentToParentServiceWorkerFetchEventOpArgs&& aArgs,
+ nsCOMPtr<nsIInterceptedChannel>&& aInterceptedChannel,
+ RefPtr<ServiceWorkerRegistrationInfo>&& aRegistrationInfo,
+ RefPtr<FetchServicePromises>&& aPreloadResponseReadyPromises,
+ RefPtr<KeepAliveToken>&& aKeepAliveToken);
+
+ mozilla::ipc::IPCResult RecvAsyncLog(const nsCString& aScriptSpec,
+ const uint32_t& aLineNumber,
+ const uint32_t& aColumnNumber,
+ const nsCString& aMessageName,
+ nsTArray<nsString>&& aParams);
+
+ mozilla::ipc::IPCResult RecvRespondWith(
+ ParentToParentFetchEventRespondWithResult&& aResult);
+
+ mozilla::ipc::IPCResult Recv__delete__(
+ const ServiceWorkerFetchEventOpResult& aResult);
+
+ void ActorDestroy(ActorDestroyReason) override;
+
+ nsresult StartSynthesizedResponse(
+ ParentToParentSynthesizeResponseArgs&& aArgs);
+
+ void SynthesizeResponse(ParentToParentSynthesizeResponseArgs&& aArgs);
+
+ void ResetInterception(bool aBypass);
+
+ void CancelInterception(nsresult aStatus);
+
+ void MaybeScheduleRegistrationUpdate() const;
+
+ ParentToParentServiceWorkerFetchEventOpArgs mArgs;
+ nsCOMPtr<nsIInterceptedChannel> mInterceptedChannel;
+ RefPtr<ServiceWorkerRegistrationInfo> mRegistration;
+ RefPtr<KeepAliveToken> mKeepAliveToken;
+ bool mInterceptedChannelHandled = false;
+ MozPromiseHolder<GenericPromise> mPromiseHolder;
+ bool mWasSent = false;
+ MozPromiseRequestHolder<FetchServiceResponseAvailablePromise>
+ mPreloadResponseAvailablePromiseRequestHolder;
+ MozPromiseRequestHolder<FetchServiceResponseEndPromise>
+ mPreloadResponseEndPromiseRequestHolder;
+ RefPtr<FetchServicePromises> mPreloadResponseReadyPromises;
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_fetcheventopchild_h__
diff --git a/dom/serviceworkers/FetchEventOpParent.cpp b/dom/serviceworkers/FetchEventOpParent.cpp
new file mode 100644
index 0000000000..41dc3d77d0
--- /dev/null
+++ b/dom/serviceworkers/FetchEventOpParent.cpp
@@ -0,0 +1,88 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "FetchEventOpParent.h"
+
+#include "mozilla/dom/FetchTypes.h"
+#include "nsDebug.h"
+
+#include "mozilla/Assertions.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/Unused.h"
+#include "mozilla/dom/FetchEventOpProxyParent.h"
+#include "mozilla/dom/FetchStreamUtils.h"
+#include "mozilla/dom/InternalResponse.h"
+#include "mozilla/dom/RemoteWorkerControllerParent.h"
+#include "mozilla/dom/RemoteWorkerParent.h"
+#include "mozilla/ipc/BackgroundParent.h"
+
+namespace mozilla {
+
+using namespace ipc;
+
+namespace dom {
+
+Tuple<Maybe<ParentToParentInternalResponse>, Maybe<ResponseEndArgs>>
+FetchEventOpParent::OnStart(
+ MovingNotNull<RefPtr<FetchEventOpProxyParent>> aFetchEventOpProxyParent) {
+ Maybe<ParentToParentInternalResponse> preloadResponse =
+ std::move(mState.as<Pending>().mPreloadResponse);
+ Maybe<ResponseEndArgs> preloadResponseEndArgs =
+ std::move(mState.as<Pending>().mEndArgs);
+ mState = AsVariant(Started{std::move(aFetchEventOpProxyParent)});
+ return MakeTuple(preloadResponse, preloadResponseEndArgs);
+}
+
+void FetchEventOpParent::OnFinish() {
+ MOZ_ASSERT(mState.is<Started>());
+ mState = AsVariant(Finished());
+}
+
+mozilla::ipc::IPCResult FetchEventOpParent::RecvPreloadResponse(
+ ParentToParentInternalResponse&& aResponse) {
+ AssertIsOnBackgroundThread();
+
+ mState.match(
+ [&aResponse](Pending& aPending) {
+ MOZ_ASSERT(aPending.mPreloadResponse.isNothing());
+ aPending.mPreloadResponse = Some(std::move(aResponse));
+ },
+ [&aResponse](Started& aStarted) {
+ auto backgroundParent = WrapNotNull(
+ WrapNotNull(aStarted.mFetchEventOpProxyParent->Manager())
+ ->Manager());
+ Unused << aStarted.mFetchEventOpProxyParent->SendPreloadResponse(
+ ToParentToChild(aResponse, backgroundParent));
+ },
+ [](const Finished&) {});
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult FetchEventOpParent::RecvPreloadResponseEnd(
+ ResponseEndArgs&& aArgs) {
+ AssertIsOnBackgroundThread();
+
+ mState.match(
+ [&aArgs](Pending& aPending) {
+ MOZ_ASSERT(aPending.mEndArgs.isNothing());
+ aPending.mEndArgs = Some(std::move(aArgs));
+ },
+ [&aArgs](Started& aStarted) {
+ Unused << aStarted.mFetchEventOpProxyParent->SendPreloadResponseEnd(
+ std::move(aArgs));
+ },
+ [](const Finished&) {});
+
+ return IPC_OK();
+}
+
+void FetchEventOpParent::ActorDestroy(ActorDestroyReason) {
+ AssertIsOnBackgroundThread();
+}
+
+} // namespace dom
+} // namespace mozilla
diff --git a/dom/serviceworkers/FetchEventOpParent.h b/dom/serviceworkers/FetchEventOpParent.h
new file mode 100644
index 0000000000..ebac93bc10
--- /dev/null
+++ b/dom/serviceworkers/FetchEventOpParent.h
@@ -0,0 +1,72 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_fetcheventopparent_h__
+#define mozilla_dom_fetcheventopparent_h__
+
+#include "nsISupports.h"
+
+#include "mozilla/Tuple.h"
+#include "mozilla/dom/FetchEventOpProxyParent.h"
+#include "mozilla/dom/PFetchEventOpParent.h"
+
+namespace mozilla::dom {
+
+class FetchEventOpParent final : public PFetchEventOpParent {
+ friend class PFetchEventOpParent;
+
+ public:
+ NS_INLINE_DECL_REFCOUNTING(FetchEventOpParent)
+
+ FetchEventOpParent() = default;
+
+ // Transition from the Pending state to the Started state. Returns the preload
+ // response and response end args, if it has already arrived.
+ Tuple<Maybe<ParentToParentInternalResponse>, Maybe<ResponseEndArgs>> OnStart(
+ MovingNotNull<RefPtr<FetchEventOpProxyParent>> aFetchEventOpProxyParent);
+
+ // Transition from the Started state to the Finished state.
+ void OnFinish();
+
+ private:
+ ~FetchEventOpParent() = default;
+
+ // IPDL methods
+
+ mozilla::ipc::IPCResult RecvPreloadResponse(
+ ParentToParentInternalResponse&& aResponse);
+
+ mozilla::ipc::IPCResult RecvPreloadResponseEnd(ResponseEndArgs&& aArgs);
+
+ void ActorDestroy(ActorDestroyReason) override;
+
+ struct Pending {
+ Maybe<ParentToParentInternalResponse> mPreloadResponse;
+ Maybe<ResponseEndArgs> mEndArgs;
+ };
+
+ struct Started {
+ NotNull<RefPtr<FetchEventOpProxyParent>> mFetchEventOpProxyParent;
+ };
+
+ struct Finished {};
+
+ using State = Variant<Pending, Started, Finished>;
+
+ // Tracks the state of the fetch event.
+ //
+ // Pending: the fetch event is waiting in RemoteWorkerController::mPendingOps
+ // and if the preload response arrives, we have to save it.
+ // Started: the FetchEventOpProxyParent has been created, and if the preload
+ // response arrives then we should forward it.
+ // Finished: the response has been propagated to the parent process, if the
+ // preload response arrives now then we simply drop it.
+ State mState = AsVariant(Pending());
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_fetcheventopparent_h__
diff --git a/dom/serviceworkers/FetchEventOpProxyChild.cpp b/dom/serviceworkers/FetchEventOpProxyChild.cpp
new file mode 100644
index 0000000000..94bdbaa4bb
--- /dev/null
+++ b/dom/serviceworkers/FetchEventOpProxyChild.cpp
@@ -0,0 +1,256 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "FetchEventOpProxyChild.h"
+
+#include <utility>
+
+#include "mozilla/dom/FetchTypes.h"
+#include "mozilla/dom/ServiceWorkerOpPromise.h"
+#include "nsCOMPtr.h"
+#include "nsDebug.h"
+#include "nsThreadUtils.h"
+
+#include "mozilla/Assertions.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/Unused.h"
+#include "mozilla/dom/InternalRequest.h"
+#include "mozilla/dom/InternalResponse.h"
+#include "mozilla/dom/RemoteWorkerChild.h"
+#include "mozilla/dom/RemoteWorkerService.h"
+#include "mozilla/dom/ServiceWorkerOp.h"
+#include "mozilla/dom/WorkerCommon.h"
+#include "mozilla/ipc/BackgroundChild.h"
+#include "mozilla/ipc/IPCStreamUtils.h"
+
+namespace mozilla {
+
+using namespace ipc;
+
+namespace dom {
+
+namespace {
+
+nsresult GetIPCSynthesizeResponseArgs(
+ ChildToParentSynthesizeResponseArgs* aIPCArgs,
+ SynthesizeResponseArgs&& aArgs) {
+ MOZ_ASSERT(RemoteWorkerService::Thread()->IsOnCurrentThread());
+
+ SafeRefPtr<InternalResponse> internalResponse;
+ FetchEventRespondWithClosure closure;
+ FetchEventTimeStamps timeStamps;
+ Tie(internalResponse, closure, timeStamps) = std::move(aArgs);
+
+ aIPCArgs->closure() = std::move(closure);
+ aIPCArgs->timeStamps() = std::move(timeStamps);
+
+ PBackgroundChild* bgChild = BackgroundChild::GetOrCreateForCurrentThread();
+
+ if (NS_WARN_IF(!bgChild)) {
+ return NS_ERROR_DOM_INVALID_STATE_ERR;
+ }
+
+ internalResponse->ToChildToParentInternalResponse(
+ &aIPCArgs->internalResponse(), bgChild);
+ return NS_OK;
+}
+
+} // anonymous namespace
+
+void FetchEventOpProxyChild::Initialize(
+ const ParentToChildServiceWorkerFetchEventOpArgs& aArgs) {
+ MOZ_ASSERT(RemoteWorkerService::Thread()->IsOnCurrentThread());
+ MOZ_ASSERT(!mOp);
+
+ mInternalRequest =
+ MakeSafeRefPtr<InternalRequest>(aArgs.common().internalRequest());
+
+ if (aArgs.common().preloadNavigation()) {
+ // We use synchronous task dispatch here to make sure that if the preload
+ // response arrived before we dispatch the fetch event, then the JS preload
+ // response promise will get resolved immediately.
+ mPreloadResponseAvailablePromise =
+ MakeRefPtr<FetchEventPreloadResponseAvailablePromise::Private>(
+ __func__);
+ mPreloadResponseAvailablePromise->UseSynchronousTaskDispatch(__func__);
+
+ if (aArgs.preloadResponse().isSome()) {
+ mPreloadResponseAvailablePromise->Resolve(
+ InternalResponse::FromIPC(aArgs.preloadResponse().ref()), __func__);
+ }
+ mPreloadResponseEndPromise =
+ MakeRefPtr<FetchEventPreloadResponseEndPromise::Private>(__func__);
+ mPreloadResponseEndPromise->UseSynchronousTaskDispatch(__func__);
+ if (aArgs.preloadResponseEndArgs().isSome()) {
+ mPreloadResponseEndPromise->Resolve(aArgs.preloadResponseEndArgs().ref(),
+ __func__);
+ }
+ }
+
+ RemoteWorkerChild* manager = static_cast<RemoteWorkerChild*>(Manager());
+ MOZ_ASSERT(manager);
+
+ RefPtr<FetchEventOpProxyChild> self = this;
+
+ auto callback = [self](const ServiceWorkerOpResult& aResult) {
+ // FetchEventOp could finish before NavigationPreload fetch finishes.
+ // If NavigationPreload is available in FetchEvent, caching FetchEventOp
+ // result until RecvPreloadResponseEnd is called, such that the response's
+ // ResourceTiming could be recorded in worker's performanceStorage.
+ if (self->mPreloadResponseEndPromise &&
+ !self->mPreloadResponseEndPromise->IsResolved() &&
+ self->mPreloadResponseAvailablePromise->IsResolved()) {
+ self->mCachedOpResult = Some(aResult);
+ return;
+ }
+ if (!self->CanSend()) {
+ return;
+ }
+
+ if (NS_WARN_IF(aResult.type() == ServiceWorkerOpResult::Tnsresult)) {
+ Unused << self->Send__delete__(self, aResult.get_nsresult());
+ return;
+ }
+
+ MOZ_ASSERT(aResult.type() ==
+ ServiceWorkerOpResult::TServiceWorkerFetchEventOpResult);
+
+ Unused << self->Send__delete__(self, aResult);
+ };
+
+ RefPtr<FetchEventOp> op = ServiceWorkerOp::Create(aArgs, std::move(callback))
+ .template downcast<FetchEventOp>();
+
+ MOZ_ASSERT(op);
+
+ op->SetActor(this);
+ mOp = op;
+
+ op->GetRespondWithPromise()
+ ->Then(GetCurrentSerialEventTarget(), __func__,
+ [self = std::move(self)](
+ FetchEventRespondWithPromise::ResolveOrRejectValue&& aResult) {
+ self->mRespondWithPromiseRequestHolder.Complete();
+
+ if (NS_WARN_IF(aResult.IsReject())) {
+ MOZ_ASSERT(NS_FAILED(aResult.RejectValue().status()));
+
+ Unused << self->SendRespondWith(aResult.RejectValue());
+ return;
+ }
+
+ auto& result = aResult.ResolveValue();
+
+ if (result.is<SynthesizeResponseArgs>()) {
+ ChildToParentSynthesizeResponseArgs ipcArgs;
+ nsresult rv = GetIPCSynthesizeResponseArgs(
+ &ipcArgs, result.extract<SynthesizeResponseArgs>());
+
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ Unused << self->SendRespondWith(
+ CancelInterceptionArgs(rv, ipcArgs.timeStamps()));
+ return;
+ }
+
+ Unused << self->SendRespondWith(ipcArgs);
+ } else if (result.is<ResetInterceptionArgs>()) {
+ Unused << self->SendRespondWith(
+ result.extract<ResetInterceptionArgs>());
+ } else {
+ Unused << self->SendRespondWith(
+ result.extract<CancelInterceptionArgs>());
+ }
+ })
+ ->Track(mRespondWithPromiseRequestHolder);
+
+ manager->MaybeStartOp(std::move(op));
+}
+
+SafeRefPtr<InternalRequest> FetchEventOpProxyChild::ExtractInternalRequest() {
+ MOZ_ASSERT(IsCurrentThreadRunningWorker());
+ MOZ_ASSERT(mInternalRequest);
+
+ return std::move(mInternalRequest);
+}
+
+RefPtr<FetchEventPreloadResponseAvailablePromise>
+FetchEventOpProxyChild::GetPreloadResponseAvailablePromise() {
+ return mPreloadResponseAvailablePromise;
+}
+
+RefPtr<FetchEventPreloadResponseEndPromise>
+FetchEventOpProxyChild::GetPreloadResponseEndPromise() {
+ return mPreloadResponseEndPromise;
+}
+
+mozilla::ipc::IPCResult FetchEventOpProxyChild::RecvPreloadResponse(
+ ParentToChildInternalResponse&& aResponse) {
+ // Receiving this message implies that navigation preload is enabled, so
+ // Initialize() should have created this promise.
+ MOZ_ASSERT(mPreloadResponseAvailablePromise);
+
+ mPreloadResponseAvailablePromise->Resolve(
+ InternalResponse::FromIPC(aResponse), __func__);
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult FetchEventOpProxyChild::RecvPreloadResponseEnd(
+ ResponseEndArgs&& aArgs) {
+ // Receiving this message implies that navigation preload is enabled, so
+ // Initialize() should have created this promise.
+ MOZ_ASSERT(mPreloadResponseEndPromise);
+
+ mPreloadResponseEndPromise->Resolve(std::move(aArgs), __func__);
+ // If mCachedOpResult is not nothing, it means FetchEventOp had already done
+ // and the operation result is cached. Continue closing IPC here.
+ if (mCachedOpResult.isNothing()) {
+ return IPC_OK();
+ }
+
+ if (!CanSend()) {
+ return IPC_OK();
+ }
+
+ if (NS_WARN_IF(mCachedOpResult.ref().type() ==
+ ServiceWorkerOpResult::Tnsresult)) {
+ Unused << Send__delete__(this, mCachedOpResult.ref().get_nsresult());
+ return IPC_OK();
+ }
+
+ MOZ_ASSERT(mCachedOpResult.ref().type() ==
+ ServiceWorkerOpResult::TServiceWorkerFetchEventOpResult);
+
+ Unused << Send__delete__(this, mCachedOpResult.ref());
+
+ return IPC_OK();
+}
+
+void FetchEventOpProxyChild::ActorDestroy(ActorDestroyReason) {
+ Unused << NS_WARN_IF(mRespondWithPromiseRequestHolder.Exists());
+ mRespondWithPromiseRequestHolder.DisconnectIfExists();
+
+ // If mPreloadResponseAvailablePromise exists, navigation preloading response
+ // will not be valid anymore since it is too late to respond to the
+ // FetchEvent. Resolve the preload response promise with
+ // NS_ERROR_DOM_ABORT_ERR.
+ if (mPreloadResponseAvailablePromise) {
+ mPreloadResponseAvailablePromise->Resolve(
+ InternalResponse::NetworkError(NS_ERROR_DOM_ABORT_ERR), __func__);
+ }
+
+ if (mPreloadResponseEndPromise) {
+ ResponseEndArgs args(FetchDriverObserver::eAborted, Nothing());
+ mPreloadResponseEndPromise->Resolve(args, __func__);
+ }
+
+ mOp->RevokeActor(this);
+ mOp = nullptr;
+}
+
+} // namespace dom
+} // namespace mozilla
diff --git a/dom/serviceworkers/FetchEventOpProxyChild.h b/dom/serviceworkers/FetchEventOpProxyChild.h
new file mode 100644
index 0000000000..ed888069d8
--- /dev/null
+++ b/dom/serviceworkers/FetchEventOpProxyChild.h
@@ -0,0 +1,71 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_fetcheventopproxychild_h__
+#define mozilla_dom_fetcheventopproxychild_h__
+
+#include "nsISupportsImpl.h"
+
+#include "ServiceWorkerOp.h"
+#include "ServiceWorkerOpPromise.h"
+#include "mozilla/MozPromise.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/dom/InternalRequest.h"
+#include "mozilla/dom/PFetchEventOpProxyChild.h"
+
+namespace mozilla::dom {
+
+class InternalRequest;
+class InternalResponse;
+class ParentToChildServiceWorkerFetchEventOpArgs;
+
+class FetchEventOpProxyChild final : public PFetchEventOpProxyChild {
+ friend class PFetchEventOpProxyChild;
+
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(FetchEventOpProxyChild, override);
+
+ FetchEventOpProxyChild() = default;
+
+ void Initialize(const ParentToChildServiceWorkerFetchEventOpArgs& aArgs);
+
+ // Must only be called once and on a worker thread.
+ SafeRefPtr<InternalRequest> ExtractInternalRequest();
+
+ RefPtr<FetchEventPreloadResponseAvailablePromise>
+ GetPreloadResponseAvailablePromise();
+
+ RefPtr<FetchEventPreloadResponseEndPromise> GetPreloadResponseEndPromise();
+
+ private:
+ ~FetchEventOpProxyChild() = default;
+
+ mozilla::ipc::IPCResult RecvPreloadResponse(
+ ParentToChildInternalResponse&& aResponse);
+
+ mozilla::ipc::IPCResult RecvPreloadResponseEnd(ResponseEndArgs&& aArgs);
+
+ void ActorDestroy(ActorDestroyReason) override;
+
+ MozPromiseRequestHolder<FetchEventRespondWithPromise>
+ mRespondWithPromiseRequestHolder;
+
+ RefPtr<FetchEventOp> mOp;
+
+ // Initialized on RemoteWorkerService::Thread, read on a worker thread.
+ SafeRefPtr<InternalRequest> mInternalRequest;
+
+ RefPtr<FetchEventPreloadResponseAvailablePromise::Private>
+ mPreloadResponseAvailablePromise;
+ RefPtr<FetchEventPreloadResponseEndPromise::Private>
+ mPreloadResponseEndPromise;
+
+ Maybe<ServiceWorkerOpResult> mCachedOpResult;
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_fetcheventopproxychild_h__
diff --git a/dom/serviceworkers/FetchEventOpProxyParent.cpp b/dom/serviceworkers/FetchEventOpProxyParent.cpp
new file mode 100644
index 0000000000..8146131739
--- /dev/null
+++ b/dom/serviceworkers/FetchEventOpProxyParent.cpp
@@ -0,0 +1,227 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "FetchEventOpProxyParent.h"
+
+#include <utility>
+
+#include "mozilla/dom/FetchTypes.h"
+#include "mozilla/dom/ServiceWorkerOpArgs.h"
+#include "nsCOMPtr.h"
+#include "nsIInputStream.h"
+
+#include "mozilla/Assertions.h"
+#include "mozilla/DebugOnly.h"
+#include "mozilla/Result.h"
+#include "mozilla/ResultExtensions.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/Unused.h"
+#include "mozilla/dom/InternalResponse.h"
+#include "mozilla/dom/PRemoteWorkerParent.h"
+#include "mozilla/dom/PRemoteWorkerControllerParent.h"
+#include "mozilla/dom/FetchEventOpParent.h"
+#include "mozilla/ipc/BackgroundParent.h"
+#include "mozilla/ipc/IPCStreamUtils.h"
+#include "mozilla/RemoteLazyInputStreamStorage.h"
+
+namespace mozilla {
+
+using namespace ipc;
+
+namespace dom {
+
+namespace {
+
+nsresult MaybeDeserializeAndWrapForMainThread(
+ const Maybe<ChildToParentStream>& aSource, int64_t aBodyStreamSize,
+ Maybe<ParentToParentStream>& aSink, PBackgroundParent* aManager) {
+ if (aSource.isNothing()) {
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIInputStream> deserialized =
+ DeserializeIPCStream(aSource->stream());
+
+ aSink = Some(ParentToParentStream());
+ auto& uuid = aSink->uuid();
+
+ MOZ_TRY(nsID::GenerateUUIDInPlace(uuid));
+
+ auto storageOrErr = RemoteLazyInputStreamStorage::Get();
+
+ if (NS_WARN_IF(storageOrErr.isErr())) {
+ return storageOrErr.unwrapErr();
+ }
+
+ auto storage = storageOrErr.unwrap();
+ storage->AddStream(deserialized, uuid);
+ return NS_OK;
+}
+
+ParentToParentInternalResponse ToParentToParent(
+ const ChildToParentInternalResponse& aResponse,
+ NotNull<PBackgroundParent*> aBackgroundParent) {
+ ParentToParentInternalResponse parentToParentResponse(
+ aResponse.metadata(), Nothing(), aResponse.bodySize(), Nothing());
+
+ MOZ_ALWAYS_SUCCEEDS(MaybeDeserializeAndWrapForMainThread(
+ aResponse.body(), aResponse.bodySize(), parentToParentResponse.body(),
+ aBackgroundParent));
+ MOZ_ALWAYS_SUCCEEDS(MaybeDeserializeAndWrapForMainThread(
+ aResponse.alternativeBody(), InternalResponse::UNKNOWN_BODY_SIZE,
+ parentToParentResponse.alternativeBody(), aBackgroundParent));
+
+ return parentToParentResponse;
+}
+
+ParentToParentSynthesizeResponseArgs ToParentToParent(
+ const ChildToParentSynthesizeResponseArgs& aArgs,
+ NotNull<PBackgroundParent*> aBackgroundParent) {
+ return ParentToParentSynthesizeResponseArgs(
+ ToParentToParent(aArgs.internalResponse(), aBackgroundParent),
+ aArgs.closure(), aArgs.timeStamps());
+}
+
+ParentToParentFetchEventRespondWithResult ToParentToParent(
+ const ChildToParentFetchEventRespondWithResult& aResult,
+ NotNull<PBackgroundParent*> aBackgroundParent) {
+ switch (aResult.type()) {
+ case ChildToParentFetchEventRespondWithResult::
+ TChildToParentSynthesizeResponseArgs:
+ return ToParentToParent(aResult.get_ChildToParentSynthesizeResponseArgs(),
+ aBackgroundParent);
+
+ case ChildToParentFetchEventRespondWithResult::TResetInterceptionArgs:
+ return aResult.get_ResetInterceptionArgs();
+
+ case ChildToParentFetchEventRespondWithResult::TCancelInterceptionArgs:
+ return aResult.get_CancelInterceptionArgs();
+
+ default:
+ MOZ_CRASH("Invalid ParentToParentFetchEventRespondWithResult");
+ }
+}
+
+} // anonymous namespace
+
+/* static */ void FetchEventOpProxyParent::Create(
+ PRemoteWorkerParent* aManager,
+ RefPtr<ServiceWorkerFetchEventOpPromise::Private>&& aPromise,
+ const ParentToParentServiceWorkerFetchEventOpArgs& aArgs,
+ RefPtr<FetchEventOpParent> aReal, nsCOMPtr<nsIInputStream> aBodyStream) {
+ AssertIsInMainProcess();
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(aManager);
+ MOZ_ASSERT(aReal);
+
+ ParentToChildServiceWorkerFetchEventOpArgs copyArgs(aArgs.common(), Nothing(),
+ Nothing());
+ if (aArgs.preloadResponse().isSome()) {
+ // Convert the preload response to ParentToChildInternalResponse.
+ copyArgs.preloadResponse() = Some(ToParentToChild(
+ aArgs.preloadResponse().ref(), WrapNotNull(aManager->Manager())));
+ }
+
+ if (aArgs.preloadResponseEndArgs().isSome()) {
+ copyArgs.preloadResponseEndArgs() = aArgs.preloadResponseEndArgs();
+ }
+
+ FetchEventOpProxyParent* actor =
+ new FetchEventOpProxyParent(std::move(aReal), std::move(aPromise));
+
+ // As long as the fetch event was pending, the FetchEventOpParent was
+ // responsible for keeping the preload response, if it already arrived. Once
+ // the fetch event starts it gives up the preload response (if any) and we
+ // need to add it to the arguments. Note that we have to make sure that the
+ // arguments don't contain the preload response already, otherwise we'll end
+ // up overwriting it with a Nothing.
+ Maybe<ParentToParentInternalResponse> preloadResponse;
+ Maybe<ResponseEndArgs> preloadResponseEndArgs;
+ Tie(preloadResponse, preloadResponseEndArgs) =
+ actor->mReal->OnStart(WrapNotNull(actor));
+ if (copyArgs.preloadResponse().isNothing() && preloadResponse.isSome()) {
+ copyArgs.preloadResponse() = Some(ToParentToChild(
+ preloadResponse.ref(), WrapNotNull(aManager->Manager())));
+ }
+ if (copyArgs.preloadResponseEndArgs().isNothing() &&
+ preloadResponseEndArgs.isSome()) {
+ copyArgs.preloadResponseEndArgs() = preloadResponseEndArgs;
+ }
+
+ IPCInternalRequest& copyRequest = copyArgs.common().internalRequest();
+
+ if (aBodyStream) {
+ copyRequest.body() = Some(ParentToChildStream());
+
+ RefPtr<RemoteLazyInputStream> stream =
+ RemoteLazyInputStream::WrapStream(aBodyStream);
+ MOZ_DIAGNOSTIC_ASSERT(stream);
+
+ copyRequest.body().ref().get_ParentToChildStream().stream() = stream;
+ }
+
+ Unused << aManager->SendPFetchEventOpProxyConstructor(actor, copyArgs);
+}
+
+FetchEventOpProxyParent::~FetchEventOpProxyParent() {
+ AssertIsOnBackgroundThread();
+}
+
+FetchEventOpProxyParent::FetchEventOpProxyParent(
+ RefPtr<FetchEventOpParent>&& aReal,
+ RefPtr<ServiceWorkerFetchEventOpPromise::Private>&& aPromise)
+ : mReal(std::move(aReal)), mLifetimePromise(std::move(aPromise)) {}
+
+mozilla::ipc::IPCResult FetchEventOpProxyParent::RecvAsyncLog(
+ const nsCString& aScriptSpec, const uint32_t& aLineNumber,
+ const uint32_t& aColumnNumber, const nsCString& aMessageName,
+ nsTArray<nsString>&& aParams) {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(mReal);
+
+ Unused << mReal->SendAsyncLog(aScriptSpec, aLineNumber, aColumnNumber,
+ aMessageName, aParams);
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult FetchEventOpProxyParent::RecvRespondWith(
+ const ChildToParentFetchEventRespondWithResult& aResult) {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(mReal);
+
+ auto manager = WrapNotNull(mReal->Manager());
+ auto backgroundParent = WrapNotNull(manager->Manager());
+ Unused << mReal->SendRespondWith(ToParentToParent(aResult, backgroundParent));
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult FetchEventOpProxyParent::Recv__delete__(
+ const ServiceWorkerFetchEventOpResult& aResult) {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(mLifetimePromise);
+ MOZ_ASSERT(mReal);
+ mReal->OnFinish();
+ if (mLifetimePromise) {
+ mLifetimePromise->Resolve(aResult, __func__);
+ mLifetimePromise = nullptr;
+ mReal = nullptr;
+ }
+
+ return IPC_OK();
+}
+
+void FetchEventOpProxyParent::ActorDestroy(ActorDestroyReason) {
+ AssertIsOnBackgroundThread();
+ if (mLifetimePromise) {
+ mLifetimePromise->Reject(NS_ERROR_DOM_ABORT_ERR, __func__);
+ mLifetimePromise = nullptr;
+ mReal = nullptr;
+ }
+}
+
+} // namespace dom
+} // namespace mozilla
diff --git a/dom/serviceworkers/FetchEventOpProxyParent.h b/dom/serviceworkers/FetchEventOpProxyParent.h
new file mode 100644
index 0000000000..e657ebd21a
--- /dev/null
+++ b/dom/serviceworkers/FetchEventOpProxyParent.h
@@ -0,0 +1,68 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_fetcheventopproxyparent_h__
+#define mozilla_dom_fetcheventopproxyparent_h__
+
+#include "mozilla/RefPtr.h"
+#include "mozilla/dom/PFetchEventOpProxyParent.h"
+#include "mozilla/dom/ServiceWorkerOpPromise.h"
+
+namespace mozilla::dom {
+
+class FetchEventOpParent;
+class PRemoteWorkerParent;
+class ParentToParentServiceWorkerFetchEventOpArgs;
+
+/**
+ * FetchEventOpProxyParent owns a FetchEventOpParent in order to propagate
+ * the respondWith() value by directly calling SendRespondWith on the
+ * FetchEventOpParent, but the call to Send__delete__ is handled via MozPromise.
+ * This is done because this actor may only be created after its managing
+ * PRemoteWorker is created, which is asynchronous and may fail. We take on
+ * responsibility for the promise once we are created, but we may not be created
+ * if the RemoteWorker is never successfully launched.
+ */
+class FetchEventOpProxyParent final : public PFetchEventOpProxyParent {
+ friend class PFetchEventOpProxyParent;
+
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(FetchEventOpProxyParent, override);
+
+ static void Create(
+ PRemoteWorkerParent* aManager,
+ RefPtr<ServiceWorkerFetchEventOpPromise::Private>&& aPromise,
+ const ParentToParentServiceWorkerFetchEventOpArgs& aArgs,
+ RefPtr<FetchEventOpParent> aReal, nsCOMPtr<nsIInputStream> aBodyStream);
+
+ private:
+ FetchEventOpProxyParent(
+ RefPtr<FetchEventOpParent>&& aReal,
+ RefPtr<ServiceWorkerFetchEventOpPromise::Private>&& aPromise);
+
+ ~FetchEventOpProxyParent();
+
+ mozilla::ipc::IPCResult RecvAsyncLog(const nsCString& aScriptSpec,
+ const uint32_t& aLineNumber,
+ const uint32_t& aColumnNumber,
+ const nsCString& aMessageName,
+ nsTArray<nsString>&& aParams);
+
+ mozilla::ipc::IPCResult RecvRespondWith(
+ const ChildToParentFetchEventRespondWithResult& aResult);
+
+ mozilla::ipc::IPCResult Recv__delete__(
+ const ServiceWorkerFetchEventOpResult& aResult);
+
+ void ActorDestroy(ActorDestroyReason) override;
+
+ RefPtr<FetchEventOpParent> mReal;
+ RefPtr<ServiceWorkerFetchEventOpPromise::Private> mLifetimePromise;
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_fetcheventopproxyparent_h__
diff --git a/dom/serviceworkers/IPCNavigationPreloadState.ipdlh b/dom/serviceworkers/IPCNavigationPreloadState.ipdlh
new file mode 100644
index 0000000000..2ee4f02789
--- /dev/null
+++ b/dom/serviceworkers/IPCNavigationPreloadState.ipdlh
@@ -0,0 +1,16 @@
+/* -*- Mode: C++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 8 -*- */
+/* vim: set sw=4 ts=8 et tw=80 ft=cpp : */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+namespace mozilla {
+namespace dom {
+
+struct IPCNavigationPreloadState {
+ bool enabled;
+ nsCString headerValue;
+};
+
+} // namespace dom
+} // namespace mozilla
diff --git a/dom/serviceworkers/IPCServiceWorkerDescriptor.ipdlh b/dom/serviceworkers/IPCServiceWorkerDescriptor.ipdlh
new file mode 100644
index 0000000000..d02b6fc0f6
--- /dev/null
+++ b/dom/serviceworkers/IPCServiceWorkerDescriptor.ipdlh
@@ -0,0 +1,30 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+include PBackgroundSharedTypes;
+
+include "mozilla/dom/ServiceWorkerIPCUtils.h";
+
+using ServiceWorkerState from "mozilla/dom/ServiceWorkerBinding.h";
+
+namespace mozilla {
+namespace dom {
+
+// IPC type with enough information to create a ServiceWorker DOM object
+// in a child process. Note that the state may be slightly out-of-sync
+// with the parent and should be updated dynamically if necessary.
+[Comparable] struct IPCServiceWorkerDescriptor
+{
+ uint64_t id;
+ uint64_t registrationId;
+ uint64_t registrationVersion;
+ PrincipalInfo principalInfo;
+ nsCString scope;
+ nsCString scriptURL;
+ ServiceWorkerState state;
+ bool handlesFetch;
+};
+
+} // namespace dom
+} // namespace mozilla
diff --git a/dom/serviceworkers/IPCServiceWorkerRegistrationDescriptor.ipdlh b/dom/serviceworkers/IPCServiceWorkerRegistrationDescriptor.ipdlh
new file mode 100644
index 0000000000..db84ae33b1
--- /dev/null
+++ b/dom/serviceworkers/IPCServiceWorkerRegistrationDescriptor.ipdlh
@@ -0,0 +1,58 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+include PBackgroundSharedTypes;
+include IPCServiceWorkerDescriptor;
+
+include "ipc/ErrorIPCUtils.h";
+include "mozilla/dom/ServiceWorkerIPCUtils.h";
+
+using ServiceWorkerUpdateViaCache from "mozilla/dom/ServiceWorkerRegistrationBinding.h";
+using mozilla::CopyableErrorResult from "mozilla/ErrorResult.h";
+
+namespace mozilla {
+namespace dom {
+
+// IPC type with enough information to create a ServiceWorker DOM object
+// in a child process. Note that the state may be slightly out-of-sync
+// with the parent and should be updated dynamically if necessary.
+[Comparable] struct IPCServiceWorkerRegistrationDescriptor
+{
+ uint64_t id;
+ uint64_t version;
+
+ // These values should match the principal and scope in each
+ // associated worker. It may be possible to optimize in the future,
+ // but for now we duplicate the information here to ensure correctness.
+ // Its possible we may need to reference a registration before the
+ // worker is installed yet, etc.
+ PrincipalInfo principalInfo;
+ nsCString scope;
+
+ ServiceWorkerUpdateViaCache updateViaCache;
+
+ IPCServiceWorkerDescriptor? installing;
+ IPCServiceWorkerDescriptor? waiting;
+ IPCServiceWorkerDescriptor? active;
+};
+
+union IPCServiceWorkerRegistrationDescriptorOrCopyableErrorResult
+{
+ IPCServiceWorkerRegistrationDescriptor;
+ CopyableErrorResult;
+};
+
+struct IPCServiceWorkerRegistrationDescriptorList
+{
+ IPCServiceWorkerRegistrationDescriptor[] values;
+};
+
+union IPCServiceWorkerRegistrationDescriptorListOrCopyableErrorResult
+{
+ IPCServiceWorkerRegistrationDescriptorList;
+ CopyableErrorResult;
+};
+
+} // namespace dom
+} // namespace mozilla
diff --git a/dom/serviceworkers/NavigationPreloadManager.cpp b/dom/serviceworkers/NavigationPreloadManager.cpp
new file mode 100644
index 0000000000..93c17353ab
--- /dev/null
+++ b/dom/serviceworkers/NavigationPreloadManager.cpp
@@ -0,0 +1,139 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "NavigationPreloadManager.h"
+#include "ServiceWorkerUtils.h"
+#include "nsNetUtil.h"
+#include "mozilla/StaticPrefs_dom.h"
+#include "mozilla/dom/NavigationPreloadManagerBinding.h"
+#include "mozilla/dom/Promise.h"
+#include "mozilla/dom/ServiceWorker.h"
+#include "mozilla/ipc/MessageChannel.h"
+
+namespace mozilla::dom {
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(NavigationPreloadManager)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(NavigationPreloadManager)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(NavigationPreloadManager)
+ NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(NavigationPreloadManager,
+ mServiceWorkerRegistration)
+
+/* static */
+bool NavigationPreloadManager::IsValidHeader(const nsACString& aHeader) {
+ return NS_IsReasonableHTTPHeaderValue(aHeader);
+}
+
+bool NavigationPreloadManager::IsEnabled(JSContext* aCx, JSObject* aGlobal) {
+ return StaticPrefs::dom_serviceWorkers_navigationPreload_enabled() &&
+ ServiceWorkerVisible(aCx, aGlobal);
+}
+
+NavigationPreloadManager::NavigationPreloadManager(
+ RefPtr<ServiceWorkerRegistration>& aServiceWorkerRegistration)
+ : mServiceWorkerRegistration(aServiceWorkerRegistration) {}
+
+JSObject* NavigationPreloadManager::WrapObject(
+ JSContext* aCx, JS::Handle<JSObject*> aGivenProto) {
+ return NavigationPreloadManager_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+already_AddRefed<Promise> NavigationPreloadManager::SetEnabled(
+ bool aEnabled, ErrorResult& aError) {
+ RefPtr<Promise> promise = Promise::Create(GetParentObject(), aError);
+
+ if (NS_WARN_IF(aError.Failed())) {
+ return nullptr;
+ }
+
+ if (!mServiceWorkerRegistration) {
+ promise->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return promise.forget();
+ }
+
+ mServiceWorkerRegistration->SetNavigationPreloadEnabled(
+ aEnabled,
+ [promise](bool aSuccess) {
+ if (aSuccess) {
+ promise->MaybeResolveWithUndefined();
+ return;
+ }
+ promise->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR);
+ },
+ [promise](ErrorResult&& aRv) { promise->MaybeReject(std::move(aRv)); });
+
+ return promise.forget();
+}
+
+already_AddRefed<Promise> NavigationPreloadManager::Enable(
+ ErrorResult& aError) {
+ return SetEnabled(true, aError);
+}
+
+already_AddRefed<Promise> NavigationPreloadManager::Disable(
+ ErrorResult& aError) {
+ return SetEnabled(false, aError);
+}
+
+already_AddRefed<Promise> NavigationPreloadManager::SetHeaderValue(
+ const nsACString& aHeader, ErrorResult& aError) {
+ RefPtr<Promise> promise = Promise::Create(GetParentObject(), aError);
+
+ if (NS_WARN_IF(aError.Failed())) {
+ return nullptr;
+ }
+
+ if (!IsValidHeader(aHeader)) {
+ promise->MaybeRejectWithTypeError<MSG_INVALID_HEADER_VALUE>(aHeader);
+ return promise.forget();
+ }
+
+ if (!mServiceWorkerRegistration) {
+ promise->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return promise.forget();
+ }
+
+ mServiceWorkerRegistration->SetNavigationPreloadHeader(
+ nsAutoCString(aHeader),
+ [promise](bool aSuccess) {
+ if (aSuccess) {
+ promise->MaybeResolveWithUndefined();
+ return;
+ }
+ promise->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR);
+ },
+ [promise](ErrorResult&& aRv) { promise->MaybeReject(std::move(aRv)); });
+
+ return promise.forget();
+}
+
+already_AddRefed<Promise> NavigationPreloadManager::GetState(
+ ErrorResult& aError) {
+ RefPtr<Promise> promise = Promise::Create(GetParentObject(), aError);
+
+ if (NS_WARN_IF(aError.Failed())) {
+ return nullptr;
+ }
+
+ if (!mServiceWorkerRegistration) {
+ promise->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return promise.forget();
+ }
+
+ mServiceWorkerRegistration->GetNavigationPreloadState(
+ [promise](NavigationPreloadState&& aState) {
+ promise->MaybeResolve(std::move(aState));
+ },
+ [promise](ErrorResult&& aRv) { promise->MaybeReject(std::move(aRv)); });
+
+ return promise.forget();
+}
+
+} // namespace mozilla::dom
diff --git a/dom/serviceworkers/NavigationPreloadManager.h b/dom/serviceworkers/NavigationPreloadManager.h
new file mode 100644
index 0000000000..7b4d0ac50b
--- /dev/null
+++ b/dom/serviceworkers/NavigationPreloadManager.h
@@ -0,0 +1,65 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_NavigationPreloadManager_h
+#define mozilla_dom_NavigationPreloadManager_h
+
+#include "nsCOMPtr.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsISupports.h"
+#include "nsWrapperCache.h"
+#include "mozilla/dom/ServiceWorkerRegistration.h"
+#include "mozilla/RefPtr.h"
+
+class nsIGlobalObject;
+
+namespace mozilla::dom {
+
+class Promise;
+
+class NavigationPreloadManager final : public nsISupports,
+ public nsWrapperCache {
+ public:
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(NavigationPreloadManager)
+
+ static bool IsValidHeader(const nsACString& aHeader);
+
+ static bool IsEnabled(JSContext* aCx, JSObject* aGlobal);
+
+ explicit NavigationPreloadManager(
+ RefPtr<ServiceWorkerRegistration>& aServiceWorkerRegistration);
+
+ // Webidl binding
+ nsIGlobalObject* GetParentObject() const {
+ return mServiceWorkerRegistration->GetParentObject();
+ }
+
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ // WebIdl implementation
+ already_AddRefed<Promise> Enable(ErrorResult& aError);
+
+ already_AddRefed<Promise> Disable(ErrorResult& aError);
+
+ already_AddRefed<Promise> SetHeaderValue(const nsACString& aHeader,
+ ErrorResult& aError);
+
+ already_AddRefed<Promise> GetState(ErrorResult& aError);
+
+ private:
+ ~NavigationPreloadManager() = default;
+
+ // General method for Enable()/Disable()
+ already_AddRefed<Promise> SetEnabled(bool aEnabled, ErrorResult& aError);
+
+ RefPtr<ServiceWorkerRegistration> mServiceWorkerRegistration;
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_NavigationPreloadManager_h
diff --git a/dom/serviceworkers/PFetchEventOp.ipdl b/dom/serviceworkers/PFetchEventOp.ipdl
new file mode 100644
index 0000000000..b78bf3e2af
--- /dev/null
+++ b/dom/serviceworkers/PFetchEventOp.ipdl
@@ -0,0 +1,33 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+include protocol PRemoteWorkerController;
+
+include ServiceWorkerOpArgs;
+include FetchTypes;
+
+namespace mozilla {
+namespace dom {
+
+[ManualDealloc]
+protocol PFetchEventOp {
+ manager PRemoteWorkerController;
+
+ parent:
+ async PreloadResponse(ParentToParentInternalResponse aResponse);
+
+ async PreloadResponseEnd(ResponseEndArgs aArgs);
+
+ child:
+ async AsyncLog(nsCString aScriptSpec, uint32_t aLineNumber,
+ uint32_t aColumnNumber, nsCString aMessageName,
+ nsString[] aParams);
+
+ async RespondWith(ParentToParentFetchEventRespondWithResult aResult);
+
+ async __delete__(ServiceWorkerFetchEventOpResult aResult);
+};
+
+} // namespace dom
+} // namespace mozilla
diff --git a/dom/serviceworkers/PFetchEventOpProxy.ipdl b/dom/serviceworkers/PFetchEventOpProxy.ipdl
new file mode 100644
index 0000000000..3f3ddbb44a
--- /dev/null
+++ b/dom/serviceworkers/PFetchEventOpProxy.ipdl
@@ -0,0 +1,32 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+include protocol PRemoteWorker;
+
+include ServiceWorkerOpArgs;
+include FetchTypes;
+
+namespace mozilla {
+namespace dom {
+
+protocol PFetchEventOpProxy {
+ manager PRemoteWorker;
+
+ parent:
+ async AsyncLog(nsCString aScriptSpec, uint32_t aLineNumber,
+ uint32_t aColumnNumber, nsCString aMessageName,
+ nsString[] aParams);
+
+ async RespondWith(ChildToParentFetchEventRespondWithResult aResult);
+
+ async __delete__(ServiceWorkerFetchEventOpResult aResult);
+
+ child:
+ async PreloadResponse(ParentToChildInternalResponse aResponse);
+
+ async PreloadResponseEnd(ResponseEndArgs aArgs);
+};
+
+} // namespace dom
+} // namespace mozilla
diff --git a/dom/serviceworkers/PServiceWorker.ipdl b/dom/serviceworkers/PServiceWorker.ipdl
new file mode 100644
index 0000000000..1f8c54481d
--- /dev/null
+++ b/dom/serviceworkers/PServiceWorker.ipdl
@@ -0,0 +1,28 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+include protocol PBackground;
+
+include ClientIPCTypes;
+include DOMTypes;
+
+namespace mozilla {
+namespace dom {
+
+[ChildImpl=virtual, ParentImpl=virtual]
+protocol PServiceWorker
+{
+ manager PBackground;
+
+parent:
+ async Teardown();
+
+ async PostMessage(ClonedOrErrorMessageData aClonedData, ClientInfoAndState aSource);
+
+child:
+ async __delete__();
+};
+
+} // namespace dom
+} // namespace mozilla
diff --git a/dom/serviceworkers/PServiceWorkerContainer.ipdl b/dom/serviceworkers/PServiceWorkerContainer.ipdl
new file mode 100644
index 0000000000..b24b494631
--- /dev/null
+++ b/dom/serviceworkers/PServiceWorkerContainer.ipdl
@@ -0,0 +1,41 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+include protocol PBackground;
+
+include ClientIPCTypes;
+include IPCServiceWorkerRegistrationDescriptor;
+
+include "mozilla/dom/ServiceWorkerIPCUtils.h";
+
+namespace mozilla {
+namespace dom {
+
+[ChildImpl=virtual, ParentImpl=virtual]
+protocol PServiceWorkerContainer
+{
+ manager PBackground;
+
+parent:
+ async Teardown();
+
+ async Register(IPCClientInfo aClientInfo, nsCString aScopeURL, nsCString aScriptURL,
+ ServiceWorkerUpdateViaCache aUpdateViaCache)
+ returns (IPCServiceWorkerRegistrationDescriptorOrCopyableErrorResult aResult);
+
+ async GetRegistration(IPCClientInfo aClientInfo, nsCString aURL)
+ returns (IPCServiceWorkerRegistrationDescriptorOrCopyableErrorResult aResult);
+
+ async GetRegistrations(IPCClientInfo aClientInfo)
+ returns (IPCServiceWorkerRegistrationDescriptorListOrCopyableErrorResult aResult);
+
+ async GetReady(IPCClientInfo aClientInfo)
+ returns (IPCServiceWorkerRegistrationDescriptorOrCopyableErrorResult aResult);
+
+child:
+ async __delete__();
+};
+
+} // namespace dom
+} // namespace mozilla
diff --git a/dom/serviceworkers/PServiceWorkerManager.ipdl b/dom/serviceworkers/PServiceWorkerManager.ipdl
new file mode 100644
index 0000000000..a41e64e348
--- /dev/null
+++ b/dom/serviceworkers/PServiceWorkerManager.ipdl
@@ -0,0 +1,31 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+include protocol PBackground;
+
+include PBackgroundSharedTypes;
+include ServiceWorkerRegistrarTypes;
+
+using mozilla::OriginAttributes from "mozilla/ipc/BackgroundUtils.h";
+
+namespace mozilla {
+namespace dom {
+
+[ManualDealloc]
+protocol PServiceWorkerManager
+{
+ manager PBackground;
+
+parent:
+ async Register(ServiceWorkerRegistrationData data);
+
+ async Unregister(PrincipalInfo principalInfo, nsString scope);
+
+ async PropagateUnregister(PrincipalInfo principalInfo, nsString scope);
+
+ async __delete__();
+};
+
+} // namespace dom
+} // namespace mozilla
diff --git a/dom/serviceworkers/PServiceWorkerRegistration.ipdl b/dom/serviceworkers/PServiceWorkerRegistration.ipdl
new file mode 100644
index 0000000000..45efc2f52d
--- /dev/null
+++ b/dom/serviceworkers/PServiceWorkerRegistration.ipdl
@@ -0,0 +1,40 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+include protocol PBackground;
+
+include IPCNavigationPreloadState;
+include IPCServiceWorkerRegistrationDescriptor;
+
+include "ipc/ErrorIPCUtils.h";
+
+namespace mozilla {
+namespace dom {
+
+[ChildImpl=virtual, ParentImpl=virtual]
+protocol PServiceWorkerRegistration
+{
+ manager PBackground;
+
+parent:
+ async Teardown();
+
+ async Unregister() returns (bool aSuccess, CopyableErrorResult aRv);
+ async Update(nsCString aNewestWorkerScriptUrl) returns (
+ IPCServiceWorkerRegistrationDescriptorOrCopyableErrorResult aResult);
+
+ // For NavigationPreload interface
+ async SetNavigationPreloadEnabled(bool aEnabled) returns (bool aSuccess);
+ async SetNavigationPreloadHeader(nsCString aHeader) returns (bool aSuccess);
+ async GetNavigationPreloadState() returns (IPCNavigationPreloadState? aState);
+
+child:
+ async __delete__();
+
+ async UpdateState(IPCServiceWorkerRegistrationDescriptor aDescriptor);
+ async FireUpdateFound();
+};
+
+} // namespace dom
+} // namespace mozilla
diff --git a/dom/serviceworkers/ServiceWorker.cpp b/dom/serviceworkers/ServiceWorker.cpp
new file mode 100644
index 0000000000..74aded5250
--- /dev/null
+++ b/dom/serviceworkers/ServiceWorker.cpp
@@ -0,0 +1,379 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "ServiceWorker.h"
+
+#include "mozilla/dom/Document.h"
+#include "nsGlobalWindowInner.h"
+#include "nsPIDOMWindow.h"
+#include "ServiceWorkerChild.h"
+#include "ServiceWorkerCloneData.h"
+#include "ServiceWorkerManager.h"
+#include "ServiceWorkerPrivate.h"
+#include "ServiceWorkerRegistration.h"
+#include "ServiceWorkerUtils.h"
+
+#include "mozilla/dom/ClientIPCTypes.h"
+#include "mozilla/dom/ClientState.h"
+#include "mozilla/dom/MessagePortBinding.h"
+#include "mozilla/dom/Navigator.h"
+#include "mozilla/dom/Promise.h"
+#include "mozilla/dom/ServiceWorkerGlobalScopeBinding.h"
+#include "mozilla/dom/WorkerPrivate.h"
+#include "mozilla/ipc/BackgroundChild.h"
+#include "mozilla/ipc/PBackgroundChild.h"
+#include "mozilla/BasePrincipal.h"
+#include "mozilla/StaticPrefs_dom.h"
+#include "mozilla/StorageAccess.h"
+
+#ifdef XP_WIN
+# undef PostMessage
+#endif
+
+using mozilla::ipc::BackgroundChild;
+using mozilla::ipc::PBackgroundChild;
+
+namespace mozilla::dom {
+
+static bool IsServiceWorkersTestingEnabledInWindow(JSObject* const aGlobal) {
+ if (const nsCOMPtr<nsPIDOMWindowInner> innerWindow =
+ Navigator::GetWindowFromGlobal(aGlobal)) {
+ if (auto* bc = innerWindow->GetBrowsingContext()) {
+ return bc->Top()->ServiceWorkersTestingEnabled();
+ }
+ }
+ return false;
+}
+
+static bool IsInPrivateBrowsing(JSContext* const aCx) {
+ if (const nsCOMPtr<nsIGlobalObject> global = xpc::CurrentNativeGlobal(aCx)) {
+ if (const nsCOMPtr<nsIPrincipal> principal = global->PrincipalOrNull()) {
+ return principal->GetPrivateBrowsingId() > 0;
+ }
+ }
+ return false;
+}
+
+bool ServiceWorkersEnabled(JSContext* aCx, JSObject* aGlobal) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (!StaticPrefs::dom_serviceWorkers_enabled()) {
+ return false;
+ }
+
+ // xpc::CurrentNativeGlobal below requires rooting
+ JS::Rooted<JSObject*> global(aCx, aGlobal);
+
+ if (IsInPrivateBrowsing(aCx)) {
+ return false;
+ }
+
+ // Allow a webextension principal to register a service worker script with
+ // a moz-extension url only if 'extensions.service_worker_register.allowed'
+ // is true.
+ if (!StaticPrefs::extensions_serviceWorkerRegister_allowed()) {
+ nsIPrincipal* principal = nsContentUtils::SubjectPrincipal(aCx);
+ if (principal && BasePrincipal::Cast(principal)->AddonPolicy()) {
+ return false;
+ }
+ }
+
+ if (IsSecureContextOrObjectIsFromSecureContext(aCx, global)) {
+ return true;
+ }
+
+ return StaticPrefs::dom_serviceWorkers_testing_enabled() ||
+ IsServiceWorkersTestingEnabledInWindow(global);
+}
+
+bool ServiceWorkerVisible(JSContext* aCx, JSObject* aGlobal) {
+ if (NS_IsMainThread()) {
+ // We want to expose ServiceWorker interface only when
+ // navigator.serviceWorker is available. Currently it may not be available
+ // with some reasons:
+ // 1. navigator.serviceWorker is not supported in workers. (bug 1131324)
+ return ServiceWorkersEnabled(aCx, aGlobal);
+ }
+
+ // We are already in ServiceWorker and interfaces need to be exposed for e.g.
+ // globalThis.registration.serviceWorker. Note that navigator.serviceWorker
+ // is still not supported. (bug 1131324)
+ return IS_INSTANCE_OF(ServiceWorkerGlobalScope, aGlobal);
+}
+
+// static
+already_AddRefed<ServiceWorker> ServiceWorker::Create(
+ nsIGlobalObject* aOwner, const ServiceWorkerDescriptor& aDescriptor) {
+ RefPtr<ServiceWorker> ref = new ServiceWorker(aOwner, aDescriptor);
+ return ref.forget();
+}
+
+ServiceWorker::ServiceWorker(nsIGlobalObject* aGlobal,
+ const ServiceWorkerDescriptor& aDescriptor)
+ : DOMEventTargetHelper(aGlobal),
+ mDescriptor(aDescriptor),
+ mShutdown(false),
+ mLastNotifiedState(ServiceWorkerState::Installing) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_DIAGNOSTIC_ASSERT(aGlobal);
+
+ PBackgroundChild* parentActor =
+ BackgroundChild::GetOrCreateForCurrentThread();
+ if (NS_WARN_IF(!parentActor)) {
+ Shutdown();
+ return;
+ }
+
+ RefPtr<ServiceWorkerChild> actor = ServiceWorkerChild::Create();
+ if (NS_WARN_IF(!actor)) {
+ Shutdown();
+ return;
+ }
+
+ PServiceWorkerChild* sentActor =
+ parentActor->SendPServiceWorkerConstructor(actor, aDescriptor.ToIPC());
+ if (NS_WARN_IF(!sentActor)) {
+ Shutdown();
+ return;
+ }
+ MOZ_DIAGNOSTIC_ASSERT(sentActor == actor);
+
+ mActor = std::move(actor);
+ mActor->SetOwner(this);
+
+ KeepAliveIfHasListenersFor(nsGkAtoms::onstatechange);
+
+ // The error event handler is required by the spec currently, but is not used
+ // anywhere. Don't keep the object alive in that case.
+
+ // Attempt to get an existing binding object for the registration
+ // associated with this ServiceWorker.
+ RefPtr<ServiceWorkerRegistration> reg =
+ aGlobal->GetServiceWorkerRegistration(ServiceWorkerRegistrationDescriptor(
+ mDescriptor.RegistrationId(), mDescriptor.RegistrationVersion(),
+ mDescriptor.PrincipalInfo(), mDescriptor.Scope(),
+ ServiceWorkerUpdateViaCache::Imports));
+
+ if (reg) {
+ MaybeAttachToRegistration(reg);
+ // Following codes are commented since GetRegistration has no
+ // implementation. If we can not get an existing binding object, probably
+ // need to create one to associate to it.
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1769652
+ /*
+ } else {
+
+ RefPtr<ServiceWorker> self = this;
+ GetRegistration(
+ [self = std::move(self)](
+ const ServiceWorkerRegistrationDescriptor& aDescriptor) {
+ nsIGlobalObject* global = self->GetParentObject();
+ NS_ENSURE_TRUE_VOID(global);
+ RefPtr<ServiceWorkerRegistration> reg =
+ global->GetOrCreateServiceWorkerRegistration(aDescriptor);
+ self->MaybeAttachToRegistration(reg);
+ },
+ [](ErrorResult&& aRv) {
+ // do nothing
+ aRv.SuppressException();
+ });
+ */
+ }
+}
+
+ServiceWorker::~ServiceWorker() {
+ MOZ_ASSERT(NS_IsMainThread());
+ Shutdown();
+}
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(ServiceWorker, DOMEventTargetHelper,
+ mRegistration);
+
+NS_IMPL_ADDREF_INHERITED(ServiceWorker, DOMEventTargetHelper)
+NS_IMPL_RELEASE_INHERITED(ServiceWorker, DOMEventTargetHelper)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ServiceWorker)
+ NS_INTERFACE_MAP_ENTRY(ServiceWorker)
+NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper)
+
+JSObject* ServiceWorker::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ return ServiceWorker_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+ServiceWorkerState ServiceWorker::State() const { return mDescriptor.State(); }
+
+void ServiceWorker::SetState(ServiceWorkerState aState) {
+ NS_ENSURE_TRUE_VOID(aState >= mDescriptor.State());
+ mDescriptor.SetState(aState);
+}
+
+void ServiceWorker::MaybeDispatchStateChangeEvent() {
+ if (mDescriptor.State() <= mLastNotifiedState || !GetParentObject()) {
+ return;
+ }
+ mLastNotifiedState = mDescriptor.State();
+
+ DOMEventTargetHelper::DispatchTrustedEvent(u"statechange"_ns);
+
+ // Once we have transitioned to the redundant state then no
+ // more statechange events will occur. We can allow the DOM
+ // object to GC if script is not holding it alive.
+ if (mLastNotifiedState == ServiceWorkerState::Redundant) {
+ IgnoreKeepAliveIfHasListenersFor(nsGkAtoms::onstatechange);
+ }
+}
+
+void ServiceWorker::GetScriptURL(nsString& aURL) const {
+ CopyUTF8toUTF16(mDescriptor.ScriptURL(), aURL);
+}
+
+void ServiceWorker::PostMessage(JSContext* aCx, JS::Handle<JS::Value> aMessage,
+ const Sequence<JSObject*>& aTransferable,
+ ErrorResult& aRv) {
+ // Step 6.1 of
+ // https://w3c.github.io/ServiceWorker/#service-worker-postmessage-options
+ // invokes
+ // https://w3c.github.io/ServiceWorker/#run-service-worker
+ // which returns failure in step 3 if the ServiceWorker state is redundant.
+ // This will result in the "in parallel" step 6.1 of postMessage itself early
+ // returning without starting the ServiceWorker and without throwing an error.
+ if (State() == ServiceWorkerState::Redundant) {
+ return;
+ }
+
+ nsPIDOMWindowInner* window = GetOwner();
+ if (NS_WARN_IF(!window || !window->GetExtantDoc())) {
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return;
+ }
+
+ auto storageAllowed = StorageAllowedForWindow(window);
+ if (storageAllowed != StorageAccess::eAllow &&
+ (!StaticPrefs::privacy_partition_serviceWorkers() ||
+ !StoragePartitioningEnabled(
+ storageAllowed, window->GetExtantDoc()->CookieJarSettings()))) {
+ ServiceWorkerManager::LocalizeAndReportToAllClients(
+ mDescriptor.Scope(), "ServiceWorkerPostMessageStorageError",
+ nsTArray<nsString>{NS_ConvertUTF8toUTF16(mDescriptor.Scope())});
+ aRv.Throw(NS_ERROR_DOM_SECURITY_ERR);
+ return;
+ }
+
+ Maybe<ClientInfo> clientInfo = window->GetClientInfo();
+ Maybe<ClientState> clientState = window->GetClientState();
+ if (NS_WARN_IF(clientInfo.isNothing() || clientState.isNothing())) {
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return;
+ }
+
+ JS::Rooted<JS::Value> transferable(aCx, JS::UndefinedValue());
+ aRv = nsContentUtils::CreateJSValueFromSequenceOfObject(aCx, aTransferable,
+ &transferable);
+ if (aRv.Failed()) {
+ return;
+ }
+
+ // Window-to-SW messages do not allow memory sharing since they are not in the
+ // same agent cluster group, but we do not want to throw an error during the
+ // serialization. Because of this, ServiceWorkerCloneData will propagate an
+ // error message data if the SameProcess serialization is required. So that
+ // the receiver (service worker) knows that it needs to throw while
+ // deserialization and sharing memory objects are not propagated to the other
+ // process.
+ JS::CloneDataPolicy clonePolicy;
+ if (nsGlobalWindowInner::Cast(window)->IsSharedMemoryAllowed()) {
+ clonePolicy.allowSharedMemoryObjects();
+ }
+
+ RefPtr<ServiceWorkerCloneData> data = new ServiceWorkerCloneData();
+ data->Write(aCx, aMessage, transferable, clonePolicy, aRv);
+ if (aRv.Failed()) {
+ return;
+ }
+
+ // The value of CloneScope() is set while StructuredCloneData::Write(). If the
+ // aValue contiains a shared memory object, then the scope will be restricted
+ // and thus return SameProcess. If not, it will return DifferentProcess.
+ //
+ // When we postMessage a shared memory object from a window to a service
+ // worker, the object must be sent from a cross-origin isolated process to
+ // another one. So, we mark mark this data as an error message data if the
+ // scope is limited to same process.
+ if (data->CloneScope() ==
+ StructuredCloneHolder::StructuredCloneScope::SameProcess) {
+ data->SetAsErrorMessageData();
+ }
+
+ if (!mActor) {
+ return;
+ }
+
+ ClonedOrErrorMessageData clonedData;
+ if (!data->BuildClonedMessageData(clonedData)) {
+ return;
+ }
+
+ mActor->SendPostMessage(
+ clonedData,
+ ClientInfoAndState(clientInfo.ref().ToIPC(), clientState.ref().ToIPC()));
+}
+
+void ServiceWorker::PostMessage(JSContext* aCx, JS::Handle<JS::Value> aMessage,
+ const StructuredSerializeOptions& aOptions,
+ ErrorResult& aRv) {
+ PostMessage(aCx, aMessage, aOptions.mTransfer, aRv);
+}
+
+const ServiceWorkerDescriptor& ServiceWorker::Descriptor() const {
+ return mDescriptor;
+}
+
+void ServiceWorker::DisconnectFromOwner() {
+ DOMEventTargetHelper::DisconnectFromOwner();
+}
+
+void ServiceWorker::RevokeActor(ServiceWorkerChild* aActor) {
+ MOZ_DIAGNOSTIC_ASSERT(mActor);
+ MOZ_DIAGNOSTIC_ASSERT(mActor == aActor);
+ mActor->RevokeOwner(this);
+ mActor = nullptr;
+
+ mShutdown = true;
+}
+
+void ServiceWorker::MaybeAttachToRegistration(
+ ServiceWorkerRegistration* aRegistration) {
+ MOZ_DIAGNOSTIC_ASSERT(aRegistration);
+ MOZ_DIAGNOSTIC_ASSERT(!mRegistration);
+
+ // If the registration no longer actually references this ServiceWorker
+ // then we must be in the redundant state.
+ if (!aRegistration->Descriptor().HasWorker(mDescriptor)) {
+ SetState(ServiceWorkerState::Redundant);
+ MaybeDispatchStateChangeEvent();
+ return;
+ }
+
+ mRegistration = aRegistration;
+}
+
+void ServiceWorker::Shutdown() {
+ if (mShutdown) {
+ return;
+ }
+ mShutdown = true;
+
+ if (mActor) {
+ mActor->RevokeOwner(this);
+ mActor->MaybeStartTeardown();
+ mActor = nullptr;
+ }
+}
+
+} // namespace mozilla::dom
diff --git a/dom/serviceworkers/ServiceWorker.h b/dom/serviceworkers/ServiceWorker.h
new file mode 100644
index 0000000000..e64a773474
--- /dev/null
+++ b/dom/serviceworkers/ServiceWorker.h
@@ -0,0 +1,98 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_serviceworker_h__
+#define mozilla_dom_serviceworker_h__
+
+#include "mozilla/DOMEventTargetHelper.h"
+#include "mozilla/dom/BindingDeclarations.h"
+#include "mozilla/dom/ServiceWorkerDescriptor.h"
+#include "mozilla/dom/ServiceWorkerUtils.h"
+
+#ifdef XP_WIN
+# undef PostMessage
+#endif
+
+class nsIGlobalObject;
+
+namespace mozilla::dom {
+
+class ServiceWorkerChild;
+class ServiceWorkerCloneData;
+struct StructuredSerializeOptions;
+
+#define NS_DOM_SERVICEWORKER_IID \
+ { \
+ 0xd42e0611, 0x3647, 0x4319, { \
+ 0xae, 0x05, 0x19, 0x89, 0x59, 0xba, 0x99, 0x5e \
+ } \
+ }
+
+bool ServiceWorkersEnabled(JSContext* aCx, JSObject* aGlobal);
+
+bool ServiceWorkerVisible(JSContext* aCx, JSObject* aGlobal);
+
+class ServiceWorker final : public DOMEventTargetHelper {
+ public:
+ NS_DECLARE_STATIC_IID_ACCESSOR(NS_DOM_SERVICEWORKER_IID)
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(ServiceWorker, DOMEventTargetHelper)
+
+ IMPL_EVENT_HANDLER(statechange)
+ IMPL_EVENT_HANDLER(error)
+
+ static already_AddRefed<ServiceWorker> Create(
+ nsIGlobalObject* aOwner, const ServiceWorkerDescriptor& aDescriptor);
+
+ virtual JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ ServiceWorkerState State() const;
+
+ void SetState(ServiceWorkerState aState);
+
+ void MaybeDispatchStateChangeEvent();
+
+ void GetScriptURL(nsString& aURL) const;
+
+ void PostMessage(JSContext* aCx, JS::Handle<JS::Value> aMessage,
+ const Sequence<JSObject*>& aTransferable, ErrorResult& aRv);
+
+ void PostMessage(JSContext* aCx, JS::Handle<JS::Value> aMessage,
+ const StructuredSerializeOptions& aOptions,
+ ErrorResult& aRv);
+
+ const ServiceWorkerDescriptor& Descriptor() const;
+
+ void DisconnectFromOwner() override;
+
+ void RevokeActor(ServiceWorkerChild* aActor);
+
+ private:
+ ServiceWorker(nsIGlobalObject* aWindow,
+ const ServiceWorkerDescriptor& aDescriptor);
+
+ // This class is reference-counted and will be destroyed from Release().
+ ~ServiceWorker();
+
+ void MaybeAttachToRegistration(ServiceWorkerRegistration* aRegistration);
+
+ void Shutdown();
+
+ ServiceWorkerDescriptor mDescriptor;
+
+ RefPtr<ServiceWorkerChild> mActor;
+ bool mShutdown;
+
+ RefPtr<ServiceWorkerRegistration> mRegistration;
+ ServiceWorkerState mLastNotifiedState;
+};
+
+NS_DEFINE_STATIC_IID_ACCESSOR(ServiceWorker, NS_DOM_SERVICEWORKER_IID)
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_serviceworker_h__
diff --git a/dom/serviceworkers/ServiceWorkerActors.cpp b/dom/serviceworkers/ServiceWorkerActors.cpp
new file mode 100644
index 0000000000..145698f3ad
--- /dev/null
+++ b/dom/serviceworkers/ServiceWorkerActors.cpp
@@ -0,0 +1,37 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "ServiceWorkerActors.h"
+
+#include "ServiceWorkerChild.h"
+#include "ServiceWorkerContainerChild.h"
+#include "ServiceWorkerContainerParent.h"
+#include "ServiceWorkerParent.h"
+#include "ServiceWorkerRegistrationChild.h"
+#include "ServiceWorkerRegistrationParent.h"
+#include "mozilla/dom/WorkerRef.h"
+
+namespace mozilla::dom {
+
+void InitServiceWorkerParent(PServiceWorkerParent* aActor,
+ const IPCServiceWorkerDescriptor& aDescriptor) {
+ auto actor = static_cast<ServiceWorkerParent*>(aActor);
+ actor->Init(aDescriptor);
+}
+
+void InitServiceWorkerContainerParent(PServiceWorkerContainerParent* aActor) {
+ auto actor = static_cast<ServiceWorkerContainerParent*>(aActor);
+ actor->Init();
+}
+
+void InitServiceWorkerRegistrationParent(
+ PServiceWorkerRegistrationParent* aActor,
+ const IPCServiceWorkerRegistrationDescriptor& aDescriptor) {
+ auto actor = static_cast<ServiceWorkerRegistrationParent*>(aActor);
+ actor->Init(aDescriptor);
+}
+
+} // namespace mozilla::dom
diff --git a/dom/serviceworkers/ServiceWorkerActors.h b/dom/serviceworkers/ServiceWorkerActors.h
new file mode 100644
index 0000000000..cad7cf38e7
--- /dev/null
+++ b/dom/serviceworkers/ServiceWorkerActors.h
@@ -0,0 +1,37 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_serviceworkeractors_h__
+#define mozilla_dom_serviceworkeractors_h__
+
+namespace mozilla::dom {
+
+// PServiceWorker
+
+class IPCServiceWorkerDescriptor;
+class PServiceWorkerParent;
+
+void InitServiceWorkerParent(PServiceWorkerParent* aActor,
+ const IPCServiceWorkerDescriptor& aDescriptor);
+
+// PServiceWorkerContainer
+
+class PServiceWorkerContainerParent;
+
+void InitServiceWorkerContainerParent(PServiceWorkerContainerParent* aActor);
+
+// PServiceWorkerRegistration
+
+class IPCServiceWorkerRegistrationDescriptor;
+class PServiceWorkerRegistrationParent;
+
+void InitServiceWorkerRegistrationParent(
+ PServiceWorkerRegistrationParent* aActor,
+ const IPCServiceWorkerRegistrationDescriptor& aDescriptor);
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_serviceworkeractors_h__
diff --git a/dom/serviceworkers/ServiceWorkerChild.cpp b/dom/serviceworkers/ServiceWorkerChild.cpp
new file mode 100644
index 0000000000..9c1b045e9f
--- /dev/null
+++ b/dom/serviceworkers/ServiceWorkerChild.cpp
@@ -0,0 +1,69 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "ServiceWorkerChild.h"
+#include "ServiceWorker.h"
+#include "mozilla/dom/WorkerCommon.h"
+#include "mozilla/dom/WorkerRef.h"
+
+namespace mozilla::dom {
+
+void ServiceWorkerChild::ActorDestroy(ActorDestroyReason aReason) {
+ mIPCWorkerRef = nullptr;
+
+ if (mOwner) {
+ mOwner->RevokeActor(this);
+ MOZ_DIAGNOSTIC_ASSERT(!mOwner);
+ }
+}
+
+// static
+RefPtr<ServiceWorkerChild> ServiceWorkerChild::Create() {
+ RefPtr<ServiceWorkerChild> actor = new ServiceWorkerChild();
+
+ if (!NS_IsMainThread()) {
+ WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate();
+ MOZ_DIAGNOSTIC_ASSERT(workerPrivate);
+
+ RefPtr<IPCWorkerRefHelper<ServiceWorkerChild>> helper =
+ new IPCWorkerRefHelper<ServiceWorkerChild>(actor);
+
+ actor->mIPCWorkerRef = IPCWorkerRef::Create(
+ workerPrivate, "ServiceWorkerChild",
+ [helper] { helper->Actor()->MaybeStartTeardown(); });
+
+ if (NS_WARN_IF(!actor->mIPCWorkerRef)) {
+ return nullptr;
+ }
+ }
+
+ return actor;
+}
+
+ServiceWorkerChild::ServiceWorkerChild()
+ : mOwner(nullptr), mTeardownStarted(false) {}
+
+void ServiceWorkerChild::SetOwner(ServiceWorker* aOwner) {
+ MOZ_DIAGNOSTIC_ASSERT(!mOwner);
+ MOZ_DIAGNOSTIC_ASSERT(aOwner);
+ mOwner = aOwner;
+}
+
+void ServiceWorkerChild::RevokeOwner(ServiceWorker* aOwner) {
+ MOZ_DIAGNOSTIC_ASSERT(mOwner);
+ MOZ_DIAGNOSTIC_ASSERT(aOwner == mOwner);
+ mOwner = nullptr;
+}
+
+void ServiceWorkerChild::MaybeStartTeardown() {
+ if (mTeardownStarted) {
+ return;
+ }
+ mTeardownStarted = true;
+ Unused << SendTeardown();
+}
+
+} // namespace mozilla::dom
diff --git a/dom/serviceworkers/ServiceWorkerChild.h b/dom/serviceworkers/ServiceWorkerChild.h
new file mode 100644
index 0000000000..2352c1d0a5
--- /dev/null
+++ b/dom/serviceworkers/ServiceWorkerChild.h
@@ -0,0 +1,44 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_serviceworkerchild_h__
+#define mozilla_dom_serviceworkerchild_h__
+
+#include "mozilla/dom/PServiceWorkerChild.h"
+#include "mozilla/dom/WorkerRef.h"
+
+namespace mozilla::dom {
+
+class IPCWorkerRef;
+class ServiceWorker;
+
+class ServiceWorkerChild final : public PServiceWorkerChild {
+ RefPtr<IPCWorkerRef> mIPCWorkerRef;
+ ServiceWorker* mOwner;
+ bool mTeardownStarted;
+
+ ServiceWorkerChild();
+
+ ~ServiceWorkerChild() = default;
+
+ // PServiceWorkerChild
+ void ActorDestroy(ActorDestroyReason aReason) override;
+
+ public:
+ NS_INLINE_DECL_REFCOUNTING(ServiceWorkerChild, override);
+
+ static RefPtr<ServiceWorkerChild> Create();
+
+ void SetOwner(ServiceWorker* aOwner);
+
+ void RevokeOwner(ServiceWorker* aOwner);
+
+ void MaybeStartTeardown();
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_serviceworkerchild_h__
diff --git a/dom/serviceworkers/ServiceWorkerCloneData.cpp b/dom/serviceworkers/ServiceWorkerCloneData.cpp
new file mode 100644
index 0000000000..b74d45386a
--- /dev/null
+++ b/dom/serviceworkers/ServiceWorkerCloneData.cpp
@@ -0,0 +1,80 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "ServiceWorkerCloneData.h"
+
+#include <utility>
+#include "mozilla/RefPtr.h"
+#include "mozilla/dom/DOMTypes.h"
+#include "mozilla/dom/StructuredCloneHolder.h"
+#include "nsISerialEventTarget.h"
+#include "nsProxyRelease.h"
+#include "nsThreadUtils.h"
+
+namespace mozilla::dom {
+
+ServiceWorkerCloneData::~ServiceWorkerCloneData() {
+ RefPtr<ipc::SharedJSAllocatedData> sharedData = TakeSharedData();
+ if (sharedData) {
+ NS_ProxyRelease(__func__, mEventTarget, sharedData.forget());
+ }
+}
+
+ServiceWorkerCloneData::ServiceWorkerCloneData()
+ : ipc::StructuredCloneData(
+ StructuredCloneHolder::StructuredCloneScope::UnknownDestination,
+ StructuredCloneHolder::TransferringSupported),
+ mEventTarget(GetCurrentSerialEventTarget()),
+ mIsErrorMessageData(false) {
+ MOZ_DIAGNOSTIC_ASSERT(mEventTarget);
+}
+
+bool ServiceWorkerCloneData::BuildClonedMessageData(
+ ClonedOrErrorMessageData& aClonedData) {
+ if (IsErrorMessageData()) {
+ aClonedData = ErrorMessageData();
+ return true;
+ }
+
+ MOZ_DIAGNOSTIC_ASSERT(
+ CloneScope() ==
+ StructuredCloneHolder::StructuredCloneScope::DifferentProcess);
+
+ ClonedMessageData messageData;
+ if (!StructuredCloneData::BuildClonedMessageData(messageData)) {
+ return false;
+ }
+
+ aClonedData = std::move(messageData);
+
+ return true;
+}
+
+void ServiceWorkerCloneData::CopyFromClonedMessageData(
+ const ClonedOrErrorMessageData& aClonedData) {
+ if (aClonedData.type() == ClonedOrErrorMessageData::TErrorMessageData) {
+ mIsErrorMessageData = true;
+ return;
+ }
+
+ MOZ_DIAGNOSTIC_ASSERT(aClonedData.type() ==
+ ClonedOrErrorMessageData::TClonedMessageData);
+
+ StructuredCloneData::CopyFromClonedMessageData(aClonedData);
+}
+
+void ServiceWorkerCloneData::SetAsErrorMessageData() {
+ MOZ_ASSERT(CloneScope() ==
+ StructuredCloneHolder::StructuredCloneScope::SameProcess);
+
+ mIsErrorMessageData = true;
+}
+
+bool ServiceWorkerCloneData::IsErrorMessageData() const {
+ return mIsErrorMessageData;
+}
+
+} // namespace mozilla::dom
diff --git a/dom/serviceworkers/ServiceWorkerCloneData.h b/dom/serviceworkers/ServiceWorkerCloneData.h
new file mode 100644
index 0000000000..b29b43414b
--- /dev/null
+++ b/dom/serviceworkers/ServiceWorkerCloneData.h
@@ -0,0 +1,71 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_ServiceWorkerCloneData_h__
+#define mozilla_dom_ServiceWorkerCloneData_h__
+
+#include "mozilla/Assertions.h"
+#include "mozilla/dom/DOMTypes.h"
+#include "mozilla/dom/ipc/StructuredCloneData.h"
+#include "nsCOMPtr.h"
+#include "nsISupports.h"
+
+class nsISerialEventTarget;
+
+namespace mozilla {
+namespace ipc {
+class PBackgroundChild;
+class PBackgroundParent;
+} // namespace ipc
+
+namespace dom {
+
+class ClonedOrErrorMessageData;
+
+// Helper class used to pack structured clone data so that it can be
+// passed across thread and process boundaries. Currently the raw
+// StructuredCloneData and StructureCloneHolder APIs both make it
+// difficult to meet this needs directly. This helper class improves
+// the situation by:
+//
+// 1. Provides a ref-counted version of StructuredCloneData. We need
+// StructuredCloneData so we can serialize/deserialize across IPC.
+// The move constructor problems in StructuredCloneData (bug 1462676),
+// though, makes it hard to pass it around. Passing a ref-counted
+// pointer addresses this problem.
+// 2. Normally StructuredCloneData runs into problems if you try to move
+// it across thread boundaries because it releases its SharedJSAllocatedData
+// on the wrong thread. This helper will correctly proxy release the
+// shared data on the correct thread.
+//
+// This helper class should really just be used to serialize on one thread
+// and then move the reference across thread/process boundries to the
+// target worker thread. This class is not intended to support simultaneous
+// read/write operations from different threads at the same time.
+class ServiceWorkerCloneData final : public ipc::StructuredCloneData {
+ nsCOMPtr<nsISerialEventTarget> mEventTarget;
+ bool mIsErrorMessageData;
+
+ ~ServiceWorkerCloneData();
+
+ public:
+ ServiceWorkerCloneData();
+
+ bool BuildClonedMessageData(ClonedOrErrorMessageData& aClonedData);
+
+ void CopyFromClonedMessageData(const ClonedOrErrorMessageData& aClonedData);
+
+ void SetAsErrorMessageData();
+
+ bool IsErrorMessageData() const;
+
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(ServiceWorkerCloneData)
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_ServiceWorkerCloneData_h__
diff --git a/dom/serviceworkers/ServiceWorkerContainer.cpp b/dom/serviceworkers/ServiceWorkerContainer.cpp
new file mode 100644
index 0000000000..4e33b9fc75
--- /dev/null
+++ b/dom/serviceworkers/ServiceWorkerContainer.cpp
@@ -0,0 +1,893 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "ServiceWorkerContainer.h"
+
+#include "nsContentPolicyUtils.h"
+#include "nsContentSecurityManager.h"
+#include "nsContentUtils.h"
+#include "mozilla/dom/Document.h"
+#include "nsIServiceWorkerManager.h"
+#include "nsIScriptError.h"
+#include "nsThreadUtils.h"
+#include "nsNetUtil.h"
+#include "nsPIDOMWindow.h"
+#include "mozilla/Components.h"
+#include "mozilla/StaticPrefs_dom.h"
+
+#include "nsCycleCollectionParticipant.h"
+#include "nsServiceManagerUtils.h"
+#include "mozilla/LoadInfo.h"
+#include "mozilla/SchedulerGroup.h"
+#include "mozilla/StaticPrefs_extensions.h"
+#include "mozilla/StaticPrefs_privacy.h"
+#include "mozilla/StorageAccess.h"
+#include "mozilla/StoragePrincipalHelper.h"
+#include "mozilla/dom/ClientIPCTypes.h"
+#include "mozilla/dom/DOMMozPromiseRequestHolder.h"
+#include "mozilla/dom/MessageEvent.h"
+#include "mozilla/dom/MessageEventBinding.h"
+#include "mozilla/dom/Navigator.h"
+#include "mozilla/dom/Promise.h"
+#include "mozilla/dom/RootedDictionary.h"
+#include "mozilla/dom/ServiceWorker.h"
+#include "mozilla/dom/ServiceWorkerContainerBinding.h"
+#include "mozilla/dom/ServiceWorkerContainerChild.h"
+#include "mozilla/dom/ServiceWorkerManager.h"
+#include "mozilla/dom/ipc/StructuredCloneData.h"
+#include "mozilla/ipc/BackgroundChild.h"
+#include "mozilla/ipc/PBackgroundChild.h"
+
+#include "ServiceWorker.h"
+#include "ServiceWorkerRegistration.h"
+#include "ServiceWorkerUtils.h"
+
+// This is defined to something else on Windows
+#ifdef DispatchMessage
+# undef DispatchMessage
+#endif
+
+namespace mozilla::dom {
+
+using mozilla::ipc::BackgroundChild;
+using mozilla::ipc::PBackgroundChild;
+using mozilla::ipc::ResponseRejectReason;
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ServiceWorkerContainer)
+NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper)
+
+NS_IMPL_ADDREF_INHERITED(ServiceWorkerContainer, DOMEventTargetHelper)
+NS_IMPL_RELEASE_INHERITED(ServiceWorkerContainer, DOMEventTargetHelper)
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(ServiceWorkerContainer, DOMEventTargetHelper,
+ mControllerWorker, mReadyPromise)
+
+// static
+already_AddRefed<ServiceWorkerContainer> ServiceWorkerContainer::Create(
+ nsIGlobalObject* aGlobal) {
+ RefPtr<ServiceWorkerContainer> ref = new ServiceWorkerContainer(aGlobal);
+ return ref.forget();
+}
+
+ServiceWorkerContainer::ServiceWorkerContainer(nsIGlobalObject* aGlobal)
+ : DOMEventTargetHelper(aGlobal), mShutdown(false) {
+ PBackgroundChild* parentActor =
+ BackgroundChild::GetOrCreateForCurrentThread();
+ if (NS_WARN_IF(!parentActor)) {
+ Shutdown();
+ return;
+ }
+
+ RefPtr<ServiceWorkerContainerChild> actor =
+ ServiceWorkerContainerChild::Create();
+ if (NS_WARN_IF(!actor)) {
+ Shutdown();
+ return;
+ }
+
+ PServiceWorkerContainerChild* sentActor =
+ parentActor->SendPServiceWorkerContainerConstructor(actor);
+ if (NS_WARN_IF(!sentActor)) {
+ Shutdown();
+ return;
+ }
+ MOZ_DIAGNOSTIC_ASSERT(sentActor == actor);
+
+ mActor = std::move(actor);
+ mActor->SetOwner(this);
+
+ Maybe<ServiceWorkerDescriptor> controller = aGlobal->GetController();
+ if (controller.isSome()) {
+ mControllerWorker = aGlobal->GetOrCreateServiceWorker(controller.ref());
+ }
+}
+
+ServiceWorkerContainer::~ServiceWorkerContainer() { Shutdown(); }
+
+void ServiceWorkerContainer::DisconnectFromOwner() {
+ mControllerWorker = nullptr;
+ mReadyPromise = nullptr;
+ DOMEventTargetHelper::DisconnectFromOwner();
+}
+
+void ServiceWorkerContainer::ControllerChanged(ErrorResult& aRv) {
+ nsCOMPtr<nsIGlobalObject> go = GetParentObject();
+ if (!go) {
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return;
+ }
+ mControllerWorker = go->GetOrCreateServiceWorker(go->GetController().ref());
+ aRv = DispatchTrustedEvent(u"controllerchange"_ns);
+}
+
+using mozilla::dom::ipc::StructuredCloneData;
+
+// A ReceivedMessage represents a message sent via
+// Client.postMessage(). It is used as used both for queuing of
+// incoming messages and as an interface to DispatchMessage().
+struct MOZ_HEAP_CLASS ServiceWorkerContainer::ReceivedMessage {
+ explicit ReceivedMessage(const ClientPostMessageArgs& aArgs)
+ : mServiceWorker(aArgs.serviceWorker()) {
+ mClonedData.CopyFromClonedMessageData(aArgs.clonedData());
+ }
+
+ ServiceWorkerDescriptor mServiceWorker;
+ StructuredCloneData mClonedData;
+
+ NS_INLINE_DECL_REFCOUNTING(ReceivedMessage)
+
+ private:
+ ~ReceivedMessage() = default;
+};
+
+void ServiceWorkerContainer::ReceiveMessage(
+ const ClientPostMessageArgs& aArgs) {
+ RefPtr<ReceivedMessage> message = new ReceivedMessage(aArgs);
+ if (mMessagesStarted) {
+ EnqueueReceivedMessageDispatch(std::move(message));
+ } else {
+ mPendingMessages.AppendElement(message.forget());
+ }
+}
+
+void ServiceWorkerContainer::RevokeActor(ServiceWorkerContainerChild* aActor) {
+ MOZ_DIAGNOSTIC_ASSERT(mActor);
+ MOZ_DIAGNOSTIC_ASSERT(mActor == aActor);
+ mActor->RevokeOwner(this);
+ mActor = nullptr;
+
+ mShutdown = true;
+}
+
+JSObject* ServiceWorkerContainer::WrapObject(
+ JSContext* aCx, JS::Handle<JSObject*> aGivenProto) {
+ return ServiceWorkerContainer_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+namespace {
+
+already_AddRefed<nsIURI> GetBaseURIFromGlobal(nsIGlobalObject* aGlobal,
+ ErrorResult& aRv) {
+ // It would be nice not to require a window here, but right
+ // now we don't have a great way to get the base URL just
+ // from the nsIGlobalObject.
+ nsCOMPtr<nsPIDOMWindowInner> window = do_QueryInterface(aGlobal);
+ if (!window) {
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return nullptr;
+ }
+
+ Document* doc = window->GetExtantDoc();
+ if (!doc) {
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return nullptr;
+ }
+
+ nsCOMPtr<nsIURI> baseURI = doc->GetDocBaseURI();
+ if (!baseURI) {
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return nullptr;
+ }
+
+ return baseURI.forget();
+}
+
+} // anonymous namespace
+
+already_AddRefed<Promise> ServiceWorkerContainer::Register(
+ const nsAString& aScriptURL, const RegistrationOptions& aOptions,
+ const CallerType aCallerType, ErrorResult& aRv) {
+ // Note, we can't use GetGlobalIfValid() from the start here. If we
+ // hit a storage failure we want to log a message with the final
+ // scope string we put together below.
+ nsIGlobalObject* global = GetParentObject();
+ if (!global) {
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return nullptr;
+ }
+
+ Maybe<ClientInfo> clientInfo = global->GetClientInfo();
+ if (clientInfo.isNothing()) {
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return nullptr;
+ }
+
+ nsCOMPtr<nsIURI> baseURI = GetBaseURIFromGlobal(global, aRv);
+ if (aRv.Failed()) {
+ return nullptr;
+ }
+
+ // Don't use NS_ConvertUTF16toUTF8 because that doesn't let us handle OOM.
+ nsAutoCString scriptURL;
+ if (!AppendUTF16toUTF8(aScriptURL, scriptURL, fallible)) {
+ aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
+ return nullptr;
+ }
+
+ nsCOMPtr<nsIURI> scriptURI;
+ nsresult rv =
+ NS_NewURI(getter_AddRefs(scriptURI), scriptURL, nullptr, baseURI);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ aRv.ThrowTypeError<MSG_INVALID_URL>(scriptURL);
+ return nullptr;
+ }
+
+ // Never allow script URL with moz-extension scheme if support is fully
+ // disabled by the 'extensions.background_service_worker.enabled' pref.
+ if (scriptURI->SchemeIs("moz-extension") &&
+ !StaticPrefs::extensions_backgroundServiceWorker_enabled_AtStartup()) {
+ aRv.Throw(NS_ERROR_DOM_SECURITY_ERR);
+ return nullptr;
+ }
+
+ // In ServiceWorkerContainer.register() the scope argument is parsed against
+ // different base URLs depending on whether it was passed or not.
+ nsCOMPtr<nsIURI> scopeURI;
+
+ // Step 4. If none passed, parse against script's URL
+ if (!aOptions.mScope.WasPassed()) {
+ constexpr auto defaultScope = "./"_ns;
+ rv = NS_NewURI(getter_AddRefs(scopeURI), defaultScope, nullptr, scriptURI);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ nsAutoCString spec;
+ scriptURI->GetSpec(spec);
+ aRv.ThrowTypeError<MSG_INVALID_SCOPE>(defaultScope, spec);
+ return nullptr;
+ }
+ } else {
+ // Step 5. Parse against entry settings object's base URL.
+ rv = NS_NewURI(getter_AddRefs(scopeURI), aOptions.mScope.Value(), nullptr,
+ baseURI);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ nsIURI* uri = baseURI ? baseURI : scriptURI;
+ nsAutoCString spec;
+ uri->GetSpec(spec);
+ aRv.ThrowTypeError<MSG_INVALID_SCOPE>(
+ NS_ConvertUTF16toUTF8(aOptions.mScope.Value()), spec);
+ return nullptr;
+ }
+ }
+
+ // Strip the any ref from both the script and scope URLs.
+ nsCOMPtr<nsIURI> cloneWithoutRef;
+ aRv = NS_GetURIWithoutRef(scriptURI, getter_AddRefs(cloneWithoutRef));
+ if (aRv.Failed()) {
+ return nullptr;
+ }
+ scriptURI = std::move(cloneWithoutRef);
+
+ aRv = NS_GetURIWithoutRef(scopeURI, getter_AddRefs(cloneWithoutRef));
+ if (aRv.Failed()) {
+ return nullptr;
+ }
+ scopeURI = std::move(cloneWithoutRef);
+
+ ServiceWorkerScopeAndScriptAreValid(clientInfo.ref(), scopeURI, scriptURI,
+ aRv);
+ if (aRv.Failed()) {
+ return nullptr;
+ }
+
+ nsCOMPtr<nsPIDOMWindowInner> window = do_QueryInterface(global);
+ if (!window) {
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return nullptr;
+ }
+
+ Document* doc = window->GetExtantDoc();
+ if (!doc) {
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return nullptr;
+ }
+
+ // The next section of code executes an NS_CheckContentLoadPolicy()
+ // check. This is necessary to enforce the CSP of the calling client.
+ // Currently this requires an Document. Once bug 965637 lands we
+ // should try to move this into ServiceWorkerScopeAndScriptAreValid()
+ // using the ClientInfo instead of doing a window-specific check here.
+ // See bug 1455077 for further investigation.
+ nsCOMPtr<nsILoadInfo> secCheckLoadInfo = new mozilla::net::LoadInfo(
+ doc->NodePrincipal(), // loading principal
+ doc->NodePrincipal(), // triggering principal
+ doc, // loading node
+ nsILoadInfo::SEC_ONLY_FOR_EXPLICIT_CONTENTSEC_CHECK,
+ nsIContentPolicy::TYPE_INTERNAL_SERVICE_WORKER);
+
+ // Check content policy.
+ int16_t decision = nsIContentPolicy::ACCEPT;
+ rv = NS_CheckContentLoadPolicy(scriptURI, secCheckLoadInfo,
+ "application/javascript"_ns, &decision);
+ if (NS_FAILED(rv)) {
+ aRv.Throw(NS_ERROR_DOM_SECURITY_ERR);
+ return nullptr;
+ }
+ if (NS_WARN_IF(decision != nsIContentPolicy::ACCEPT)) {
+ aRv.Throw(NS_ERROR_CONTENT_BLOCKED);
+ return nullptr;
+ }
+
+ // Get the string representation for both the script and scope since
+ // we sanitized them above.
+ nsCString cleanedScopeURL;
+ aRv = scopeURI->GetSpec(cleanedScopeURL);
+ if (aRv.Failed()) {
+ return nullptr;
+ }
+
+ nsCString cleanedScriptURL;
+ aRv = scriptURI->GetSpec(cleanedScriptURL);
+ if (aRv.Failed()) {
+ return nullptr;
+ }
+
+ // Verify that the global is valid and has permission to store
+ // data. We perform this late so that we can report the final
+ // scope URL in any error message.
+ Unused << GetGlobalIfValid(aRv, [&](Document* aDoc) {
+ AutoTArray<nsString, 1> param;
+ CopyUTF8toUTF16(cleanedScopeURL, *param.AppendElement());
+ nsContentUtils::ReportToConsole(nsIScriptError::errorFlag,
+ "Service Workers"_ns, aDoc,
+ nsContentUtils::eDOM_PROPERTIES,
+ "ServiceWorkerRegisterStorageError", param);
+ });
+
+ window->NoteCalledRegisterForServiceWorkerScope(cleanedScopeURL);
+
+ RefPtr<Promise> outer =
+ Promise::Create(global, aRv, Promise::ePropagateUserInteraction);
+ if (aRv.Failed()) {
+ return nullptr;
+ }
+
+ RefPtr<ServiceWorkerContainer> self = this;
+
+ if (!mActor) {
+ aRv.ThrowInvalidStateError("Can't register service worker");
+ return nullptr;
+ }
+
+ mActor->SendRegister(
+ clientInfo.ref().ToIPC(), nsCString(cleanedScopeURL),
+ nsCString(cleanedScriptURL), aOptions.mUpdateViaCache,
+ [self,
+ outer](const IPCServiceWorkerRegistrationDescriptorOrCopyableErrorResult&
+ aResult) {
+ if (aResult.type() ==
+ IPCServiceWorkerRegistrationDescriptorOrCopyableErrorResult::
+ TCopyableErrorResult) {
+ // application layer error
+ CopyableErrorResult rv = aResult.get_CopyableErrorResult();
+ MOZ_DIAGNOSTIC_ASSERT(rv.Failed());
+ outer->MaybeReject(std::move(rv));
+ return;
+ }
+ // success
+ const auto& ipcDesc =
+ aResult.get_IPCServiceWorkerRegistrationDescriptor();
+ ErrorResult rv;
+ nsIGlobalObject* global = self->GetGlobalIfValid(rv);
+ if (rv.Failed()) {
+ outer->MaybeReject(std::move(rv));
+ return;
+ }
+ RefPtr<ServiceWorkerRegistration> reg =
+ global->GetOrCreateServiceWorkerRegistration(
+ ServiceWorkerRegistrationDescriptor(ipcDesc));
+ outer->MaybeResolve(reg);
+ },
+ [outer](ResponseRejectReason&& aReason) {
+ // IPC layer error
+ CopyableErrorResult rv;
+ rv.ThrowInvalidStateError("Failed to register service worker");
+ outer->MaybeReject(std::move(rv));
+ });
+
+ return outer.forget();
+}
+
+already_AddRefed<ServiceWorker> ServiceWorkerContainer::GetController() {
+ RefPtr<ServiceWorker> ref = mControllerWorker;
+ return ref.forget();
+}
+
+already_AddRefed<Promise> ServiceWorkerContainer::GetRegistrations(
+ ErrorResult& aRv) {
+ nsIGlobalObject* global = GetGlobalIfValid(aRv, [](Document* aDoc) {
+ nsContentUtils::ReportToConsole(nsIScriptError::errorFlag,
+ "Service Workers"_ns, aDoc,
+ nsContentUtils::eDOM_PROPERTIES,
+ "ServiceWorkerGetRegistrationStorageError");
+ });
+ if (aRv.Failed()) {
+ return nullptr;
+ }
+
+ Maybe<ClientInfo> clientInfo = global->GetClientInfo();
+ if (clientInfo.isNothing()) {
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return nullptr;
+ }
+
+ RefPtr<Promise> outer =
+ Promise::Create(global, aRv, Promise::ePropagateUserInteraction);
+ if (aRv.Failed()) {
+ return nullptr;
+ }
+
+ RefPtr<ServiceWorkerContainer> self = this;
+
+ if (!mActor) {
+ outer->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return outer.forget();
+ }
+
+ mActor->SendGetRegistrations(
+ clientInfo.ref().ToIPC(),
+ [self, outer](
+ const IPCServiceWorkerRegistrationDescriptorListOrCopyableErrorResult&
+ aResult) {
+ if (aResult.type() ==
+ IPCServiceWorkerRegistrationDescriptorListOrCopyableErrorResult::
+ TCopyableErrorResult) {
+ // application layer error
+ const auto& rv = aResult.get_CopyableErrorResult();
+ MOZ_DIAGNOSTIC_ASSERT(rv.Failed());
+ outer->MaybeReject(CopyableErrorResult(rv));
+ return;
+ }
+ // success
+ const auto& ipcList =
+ aResult.get_IPCServiceWorkerRegistrationDescriptorList();
+ nsTArray<ServiceWorkerRegistrationDescriptor> list(
+ ipcList.values().Length());
+ for (const auto& ipcDesc : ipcList.values()) {
+ list.AppendElement(ServiceWorkerRegistrationDescriptor(ipcDesc));
+ }
+
+ ErrorResult rv;
+ nsIGlobalObject* global = self->GetGlobalIfValid(rv);
+ if (rv.Failed()) {
+ outer->MaybeReject(std::move(rv));
+ return;
+ }
+ nsTArray<RefPtr<ServiceWorkerRegistration>> regList;
+ for (auto& desc : list) {
+ RefPtr<ServiceWorkerRegistration> reg =
+ global->GetOrCreateServiceWorkerRegistration(desc);
+ if (reg) {
+ regList.AppendElement(std::move(reg));
+ }
+ }
+ outer->MaybeResolve(regList);
+ },
+ [outer](ResponseRejectReason&& aReason) {
+ // IPC layer error
+ outer->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR);
+ });
+
+ return outer.forget();
+}
+
+void ServiceWorkerContainer::StartMessages() {
+ while (!mPendingMessages.IsEmpty()) {
+ EnqueueReceivedMessageDispatch(mPendingMessages.ElementAt(0));
+ mPendingMessages.RemoveElementAt(0);
+ }
+ mMessagesStarted = true;
+}
+
+already_AddRefed<Promise> ServiceWorkerContainer::GetRegistration(
+ const nsAString& aURL, ErrorResult& aRv) {
+ nsIGlobalObject* global = GetGlobalIfValid(aRv, [](Document* aDoc) {
+ nsContentUtils::ReportToConsole(nsIScriptError::errorFlag,
+ "Service Workers"_ns, aDoc,
+ nsContentUtils::eDOM_PROPERTIES,
+ "ServiceWorkerGetRegistrationStorageError");
+ });
+ if (aRv.Failed()) {
+ return nullptr;
+ }
+
+ Maybe<ClientInfo> clientInfo = global->GetClientInfo();
+ if (clientInfo.isNothing()) {
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return nullptr;
+ }
+
+ nsCOMPtr<nsIURI> baseURI = GetBaseURIFromGlobal(global, aRv);
+ if (aRv.Failed()) {
+ return nullptr;
+ }
+
+ nsCOMPtr<nsIURI> uri;
+ aRv = NS_NewURI(getter_AddRefs(uri), aURL, nullptr, baseURI);
+ if (aRv.Failed()) {
+ return nullptr;
+ }
+
+ nsCString spec;
+ aRv = uri->GetSpec(spec);
+ if (aRv.Failed()) {
+ return nullptr;
+ }
+
+ RefPtr<Promise> outer =
+ Promise::Create(global, aRv, Promise::ePropagateUserInteraction);
+ if (aRv.Failed()) {
+ return nullptr;
+ }
+
+ RefPtr<ServiceWorkerContainer> self = this;
+
+ if (!mActor) {
+ outer->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return outer.forget();
+ }
+
+ mActor->SendGetRegistration(
+ clientInfo.ref().ToIPC(), spec,
+ [self,
+ outer](const IPCServiceWorkerRegistrationDescriptorOrCopyableErrorResult&
+ aResult) {
+ if (aResult.type() ==
+ IPCServiceWorkerRegistrationDescriptorOrCopyableErrorResult::
+ TCopyableErrorResult) {
+ CopyableErrorResult ipcRv(aResult.get_CopyableErrorResult());
+ ErrorResult rv(std::move(ipcRv));
+ if (!rv.Failed()) {
+ // ErrorResult rv;
+ // If rv is a failure then this is an application layer error.
+ // Note, though, we also reject with NS_OK to indicate that we just
+ // didn't find a registration.
+ Unused << self->GetGlobalIfValid(rv);
+ if (!rv.Failed()) {
+ outer->MaybeResolveWithUndefined();
+ return;
+ }
+ }
+ outer->MaybeReject(std::move(rv));
+ return;
+ }
+ // success
+ const auto& ipcDesc =
+ aResult.get_IPCServiceWorkerRegistrationDescriptor();
+ ErrorResult rv;
+ nsIGlobalObject* global = self->GetGlobalIfValid(rv);
+ if (rv.Failed()) {
+ outer->MaybeReject(std::move(rv));
+ return;
+ }
+ RefPtr<ServiceWorkerRegistration> reg =
+ global->GetOrCreateServiceWorkerRegistration(
+ ServiceWorkerRegistrationDescriptor(ipcDesc));
+ outer->MaybeResolve(reg);
+ },
+ [self, outer](ResponseRejectReason&& aReason) {
+ // IPC layer error
+ outer->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR);
+ });
+ return outer.forget();
+}
+
+Promise* ServiceWorkerContainer::GetReady(ErrorResult& aRv) {
+ if (mReadyPromise) {
+ return mReadyPromise;
+ }
+
+ nsIGlobalObject* global = GetGlobalIfValid(aRv);
+ if (aRv.Failed()) {
+ return nullptr;
+ }
+ MOZ_DIAGNOSTIC_ASSERT(global);
+
+ Maybe<ClientInfo> clientInfo(global->GetClientInfo());
+ if (clientInfo.isNothing()) {
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return nullptr;
+ }
+
+ mReadyPromise =
+ Promise::Create(global, aRv, Promise::ePropagateUserInteraction);
+ if (aRv.Failed()) {
+ return nullptr;
+ }
+
+ RefPtr<ServiceWorkerContainer> self = this;
+ RefPtr<Promise> outer = mReadyPromise;
+
+ if (!mActor) {
+ mReadyPromise->MaybeReject(
+ CopyableErrorResult(NS_ERROR_DOM_INVALID_STATE_ERR));
+ return mReadyPromise;
+ }
+
+ mActor->SendGetReady(
+ clientInfo.ref().ToIPC(),
+ [self,
+ outer](const IPCServiceWorkerRegistrationDescriptorOrCopyableErrorResult&
+ aResult) {
+ if (aResult.type() ==
+ IPCServiceWorkerRegistrationDescriptorOrCopyableErrorResult::
+ TCopyableErrorResult) {
+ // application layer error
+ CopyableErrorResult rv(aResult.get_CopyableErrorResult());
+ MOZ_DIAGNOSTIC_ASSERT(rv.Failed());
+ outer->MaybeReject(std::move(rv));
+ return;
+ }
+ // success
+ const auto& ipcDesc =
+ aResult.get_IPCServiceWorkerRegistrationDescriptor();
+ ErrorResult rv;
+ nsIGlobalObject* global = self->GetGlobalIfValid(rv);
+ if (rv.Failed()) {
+ outer->MaybeReject(std::move(rv));
+ return;
+ }
+ RefPtr<ServiceWorkerRegistration> reg =
+ global->GetOrCreateServiceWorkerRegistration(
+ ServiceWorkerRegistrationDescriptor(ipcDesc));
+ NS_ENSURE_TRUE_VOID(reg);
+
+ // Don't resolve the ready promise until the registration has
+ // reached the right version. This ensures that the active
+ // worker property is set correctly on the registration.
+ reg->WhenVersionReached(ipcDesc.version(), [outer, reg](bool aResult) {
+ outer->MaybeResolve(reg);
+ });
+ },
+ [outer](ResponseRejectReason&& aReason) {
+ // IPC layer error
+ outer->MaybeReject(CopyableErrorResult(NS_ERROR_DOM_INVALID_STATE_ERR));
+ });
+
+ return mReadyPromise;
+}
+
+// Testing only.
+void ServiceWorkerContainer::GetScopeForUrl(const nsAString& aUrl,
+ nsString& aScope,
+ ErrorResult& aRv) {
+ nsCOMPtr<nsIServiceWorkerManager> swm =
+ mozilla::components::ServiceWorkerManager::Service();
+ if (!swm) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return;
+ }
+
+ nsCOMPtr<nsPIDOMWindowInner> window = GetOwner();
+ if (NS_WARN_IF(!window)) {
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return;
+ }
+
+ nsCOMPtr<nsIPrincipal> principal;
+ nsresult rv = StoragePrincipalHelper::GetPrincipal(
+ window,
+ StaticPrefs::privacy_partition_serviceWorkers()
+ ? StoragePrincipalHelper::eForeignPartitionedPrincipal
+ : StoragePrincipalHelper::eRegularPrincipal,
+ getter_AddRefs(principal));
+
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ aRv.Throw(rv);
+ return;
+ }
+
+ aRv = swm->GetScopeForUrl(principal, aUrl, aScope);
+}
+
+nsIGlobalObject* ServiceWorkerContainer::GetGlobalIfValid(
+ ErrorResult& aRv,
+ const std::function<void(Document*)>&& aStorageFailureCB) const {
+ // For now we require a window since ServiceWorkerContainer is
+ // not exposed on worker globals yet. The main thing we need
+ // to fix here to support that is the storage access check via
+ // the nsIGlobalObject.
+ nsPIDOMWindowInner* window = GetOwner();
+ if (NS_WARN_IF(!window)) {
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return nullptr;
+ }
+
+ nsCOMPtr<Document> doc = window->GetExtantDoc();
+ if (NS_WARN_IF(!doc)) {
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return nullptr;
+ }
+
+ // Don't allow a service worker to access service worker registrations
+ // from a window with storage disabled. If these windows can access
+ // the registration it increases the chance they can bypass the storage
+ // block via postMessage(), etc.
+ auto storageAllowed = StorageAllowedForWindow(window);
+ if (NS_WARN_IF(storageAllowed != StorageAccess::eAllow &&
+ (!StaticPrefs::privacy_partition_serviceWorkers() ||
+ !StoragePartitioningEnabled(storageAllowed,
+ doc->CookieJarSettings())))) {
+ if (aStorageFailureCB) {
+ aStorageFailureCB(doc);
+ }
+ aRv.Throw(NS_ERROR_DOM_SECURITY_ERR);
+ return nullptr;
+ }
+
+ // Don't allow service workers when the document is chrome.
+ if (NS_WARN_IF(doc->NodePrincipal()->IsSystemPrincipal())) {
+ aRv.Throw(NS_ERROR_DOM_SECURITY_ERR);
+ return nullptr;
+ }
+
+ return window->AsGlobal();
+}
+
+void ServiceWorkerContainer::EnqueueReceivedMessageDispatch(
+ RefPtr<ReceivedMessage> aMessage) {
+ if (nsPIDOMWindowInner* const window = GetOwner()) {
+ if (auto* const target = window->EventTargetFor(TaskCategory::Other)) {
+ target->Dispatch(NewRunnableMethod<RefPtr<ReceivedMessage>>(
+ "ServiceWorkerContainer::DispatchMessage", this,
+ &ServiceWorkerContainer::DispatchMessage, std::move(aMessage)));
+ }
+ }
+}
+
+template <typename F>
+void ServiceWorkerContainer::RunWithJSContext(F&& aCallable) {
+ nsCOMPtr<nsIGlobalObject> globalObject;
+ if (nsPIDOMWindowInner* const window = GetOwner()) {
+ globalObject = do_QueryInterface(window);
+ }
+
+ // If AutoJSAPI::Init() fails then either global is nullptr or not
+ // in a usable state.
+ AutoJSAPI jsapi;
+ if (!jsapi.Init(globalObject)) {
+ return;
+ }
+
+ aCallable(jsapi.cx(), globalObject);
+}
+
+void ServiceWorkerContainer::DispatchMessage(RefPtr<ReceivedMessage> aMessage) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ // When dispatching a message, either DOMContentLoaded has already
+ // been fired, or someone called startMessages() or set onmessage.
+ // Either way, a global object is supposed to be present. If it's
+ // not, we'd fail to initialize the JS API and exit.
+ RunWithJSContext([this, message = std::move(aMessage)](
+ JSContext* const aCx, nsIGlobalObject* const aGlobal) {
+ ErrorResult result;
+ bool deserializationFailed = false;
+ RootedDictionary<MessageEventInit> init(aCx);
+ auto res = FillInMessageEventInit(aCx, aGlobal, *message, init, result);
+ if (res.isErr()) {
+ deserializationFailed = res.unwrapErr();
+ MOZ_ASSERT_IF(deserializationFailed, init.mData.isNull());
+ MOZ_ASSERT_IF(deserializationFailed, init.mPorts.IsEmpty());
+ MOZ_ASSERT_IF(deserializationFailed, !init.mOrigin.IsEmpty());
+ MOZ_ASSERT_IF(deserializationFailed, !init.mSource.IsNull());
+ result.SuppressException();
+
+ if (!deserializationFailed && result.MaybeSetPendingException(aCx)) {
+ return;
+ }
+ }
+
+ RefPtr<MessageEvent> event = MessageEvent::Constructor(
+ this, deserializationFailed ? u"messageerror"_ns : u"message"_ns, init);
+ event->SetTrusted(true);
+
+ result = NS_OK;
+ DispatchEvent(*event, result);
+ if (result.Failed()) {
+ result.SuppressException();
+ }
+ });
+}
+
+namespace {
+
+nsresult FillInOriginNoSuffix(const ServiceWorkerDescriptor& aServiceWorker,
+ nsString& aOrigin) {
+ using mozilla::ipc::PrincipalInfoToPrincipal;
+
+ nsresult rv;
+
+ auto principalOrErr =
+ PrincipalInfoToPrincipal(aServiceWorker.PrincipalInfo());
+ if (NS_WARN_IF(principalOrErr.isErr())) {
+ return principalOrErr.unwrapErr();
+ }
+
+ nsAutoCString originUTF8;
+ rv = principalOrErr.unwrap()->GetOriginNoSuffix(originUTF8);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ CopyUTF8toUTF16(originUTF8, aOrigin);
+ return NS_OK;
+}
+
+} // namespace
+
+Result<Ok, bool> ServiceWorkerContainer::FillInMessageEventInit(
+ JSContext* const aCx, nsIGlobalObject* const aGlobal,
+ ReceivedMessage& aMessage, MessageEventInit& aInit, ErrorResult& aRv) {
+ // Determining the source and origin should preceed attempting deserialization
+ // because on a "messageerror" event (i.e. when deserialization fails), the
+ // dispatched message needs to contain such an origin and source, per spec:
+ //
+ // "If this throws an exception, catch it, fire an event named messageerror
+ // at destination, using MessageEvent, with the origin attribute initialized
+ // to origin and the source attribute initialized to source, and then abort
+ // these steps." - 6.4 of postMessage
+ // See: https://w3c.github.io/ServiceWorker/#service-worker-postmessage
+ const RefPtr<ServiceWorker> serviceWorkerInstance =
+ aGlobal->GetOrCreateServiceWorker(aMessage.mServiceWorker);
+ if (serviceWorkerInstance) {
+ aInit.mSource.SetValue().SetAsServiceWorker() = serviceWorkerInstance;
+ }
+
+ const nsresult rv =
+ FillInOriginNoSuffix(aMessage.mServiceWorker, aInit.mOrigin);
+ if (NS_FAILED(rv)) {
+ return Err(false);
+ }
+
+ JS::Rooted<JS::Value> messageData(aCx);
+ aMessage.mClonedData.Read(aCx, &messageData, aRv);
+ if (aRv.Failed()) {
+ return Err(true);
+ }
+
+ aInit.mData = messageData;
+
+ if (!aMessage.mClonedData.TakeTransferredPortsAsSequence(aInit.mPorts)) {
+ xpc::Throw(aCx, NS_ERROR_OUT_OF_MEMORY);
+ return Err(false);
+ }
+
+ return Ok();
+}
+
+void ServiceWorkerContainer::Shutdown() {
+ if (mShutdown) {
+ return;
+ }
+ mShutdown = true;
+
+ if (mActor) {
+ mActor->RevokeOwner(this);
+ mActor->MaybeStartTeardown();
+ mActor = nullptr;
+ }
+}
+
+} // namespace mozilla::dom
diff --git a/dom/serviceworkers/ServiceWorkerContainer.h b/dom/serviceworkers/ServiceWorkerContainer.h
new file mode 100644
index 0000000000..3a5dd5fa5d
--- /dev/null
+++ b/dom/serviceworkers/ServiceWorkerContainer.h
@@ -0,0 +1,143 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_serviceworkercontainer_h__
+#define mozilla_dom_serviceworkercontainer_h__
+
+#include "mozilla/DOMEventTargetHelper.h"
+#include "mozilla/ErrorResult.h"
+#include "mozilla/dom/ServiceWorkerUtils.h"
+
+class nsIGlobalWindow;
+
+namespace mozilla::dom {
+
+class ClientPostMessageArgs;
+struct MessageEventInit;
+class Promise;
+struct RegistrationOptions;
+class ServiceWorker;
+class ServiceWorkerContainerChild;
+
+// Lightweight serviceWorker APIs collection.
+class ServiceWorkerContainer final : public DOMEventTargetHelper {
+ public:
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(ServiceWorkerContainer,
+ DOMEventTargetHelper)
+
+ IMPL_EVENT_HANDLER(controllerchange)
+ IMPL_EVENT_HANDLER(messageerror)
+
+ // Almost a manual expansion of IMPL_EVENT_HANDLER(message), but
+ // with the additional StartMessages() when setting the handler, as
+ // required by the spec.
+ inline mozilla::dom::EventHandlerNonNull* GetOnmessage() {
+ return GetEventHandler(nsGkAtoms::onmessage);
+ }
+ inline void SetOnmessage(mozilla::dom::EventHandlerNonNull* aCallback) {
+ SetEventHandler(nsGkAtoms::onmessage, aCallback);
+ StartMessages();
+ }
+
+ static bool IsEnabled(JSContext* aCx, JSObject* aGlobal);
+
+ static already_AddRefed<ServiceWorkerContainer> Create(
+ nsIGlobalObject* aGlobal);
+
+ virtual JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ already_AddRefed<Promise> Register(const nsAString& aScriptURL,
+ const RegistrationOptions& aOptions,
+ const CallerType aCallerType,
+ ErrorResult& aRv);
+
+ already_AddRefed<ServiceWorker> GetController();
+
+ already_AddRefed<Promise> GetRegistration(const nsAString& aDocumentURL,
+ ErrorResult& aRv);
+
+ already_AddRefed<Promise> GetRegistrations(ErrorResult& aRv);
+
+ void StartMessages();
+
+ Promise* GetReady(ErrorResult& aRv);
+
+ // Testing only.
+ void GetScopeForUrl(const nsAString& aUrl, nsString& aScope,
+ ErrorResult& aRv);
+
+ // DOMEventTargetHelper
+ void DisconnectFromOwner() override;
+
+ // Invalidates |mControllerWorker| and dispatches a "controllerchange"
+ // event.
+ void ControllerChanged(ErrorResult& aRv);
+
+ void ReceiveMessage(const ClientPostMessageArgs& aArgs);
+
+ void RevokeActor(ServiceWorkerContainerChild* aActor);
+
+ private:
+ explicit ServiceWorkerContainer(nsIGlobalObject* aGlobal);
+
+ ~ServiceWorkerContainer();
+
+ // Utility method to get the global if its present and if certain
+ // additional validaty checks pass. One of these additional checks
+ // verifies the global can access storage. Since storage access can
+ // vary based on user settings we want to often provide some error
+ // message if the storage check fails. This method takes an optional
+ // callback that can be used to report the storage failure to the
+ // devtools console.
+ nsIGlobalObject* GetGlobalIfValid(
+ ErrorResult& aRv,
+ const std::function<void(Document*)>&& aStorageFailureCB = nullptr) const;
+
+ struct ReceivedMessage;
+
+ // Dispatch a Runnable that dispatches the given message on this
+ // object. When the owner of this object is a Window, the Runnable
+ // is dispatched on the corresponding TabGroup.
+ void EnqueueReceivedMessageDispatch(RefPtr<ReceivedMessage> aMessage);
+
+ template <typename F>
+ void RunWithJSContext(F&& aCallable);
+
+ void DispatchMessage(RefPtr<ReceivedMessage> aMessage);
+
+ // When it fails, returning boolean means whether it's because deserailization
+ // failed or not.
+ static Result<Ok, bool> FillInMessageEventInit(JSContext* aCx,
+ nsIGlobalObject* aGlobal,
+ ReceivedMessage& aMessage,
+ MessageEventInit& aInit,
+ ErrorResult& aRv);
+
+ void Shutdown();
+
+ RefPtr<ServiceWorkerContainerChild> mActor;
+ bool mShutdown;
+
+ // This only changes when a worker hijacks everything in its scope by calling
+ // claim.
+ RefPtr<ServiceWorker> mControllerWorker;
+
+ RefPtr<Promise> mReadyPromise;
+ MozPromiseRequestHolder<ServiceWorkerRegistrationPromise> mReadyPromiseHolder;
+
+ // Set after StartMessages() has been called.
+ bool mMessagesStarted = false;
+
+ // Queue holding messages posted from service worker as long as
+ // StartMessages() hasn't been called.
+ nsTArray<RefPtr<ReceivedMessage>> mPendingMessages;
+};
+
+} // namespace mozilla::dom
+
+#endif /* mozilla_dom_serviceworkercontainer_h__ */
diff --git a/dom/serviceworkers/ServiceWorkerContainerChild.cpp b/dom/serviceworkers/ServiceWorkerContainerChild.cpp
new file mode 100644
index 0000000000..1093bca91a
--- /dev/null
+++ b/dom/serviceworkers/ServiceWorkerContainerChild.cpp
@@ -0,0 +1,70 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/PServiceWorkerContainerChild.h"
+#include "mozilla/dom/WorkerCommon.h"
+#include "mozilla/dom/WorkerRef.h"
+
+#include "ServiceWorkerContainer.h"
+#include "ServiceWorkerContainerChild.h"
+
+namespace mozilla::dom {
+
+void ServiceWorkerContainerChild::ActorDestroy(ActorDestroyReason aReason) {
+ mIPCWorkerRef = nullptr;
+
+ if (mOwner) {
+ mOwner->RevokeActor(this);
+ MOZ_DIAGNOSTIC_ASSERT(!mOwner);
+ }
+}
+
+// static
+already_AddRefed<ServiceWorkerContainerChild>
+ServiceWorkerContainerChild::Create() {
+ RefPtr actor = new ServiceWorkerContainerChild;
+
+ if (!NS_IsMainThread()) {
+ WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate();
+ MOZ_DIAGNOSTIC_ASSERT(workerPrivate);
+
+ RefPtr<IPCWorkerRefHelper<ServiceWorkerContainerChild>> helper =
+ new IPCWorkerRefHelper<ServiceWorkerContainerChild>(actor);
+
+ actor->mIPCWorkerRef = IPCWorkerRef::Create(
+ workerPrivate, "ServiceWorkerContainerChild",
+ [helper] { helper->Actor()->MaybeStartTeardown(); });
+ if (NS_WARN_IF(!actor->mIPCWorkerRef)) {
+ return nullptr;
+ }
+ }
+
+ return actor.forget();
+}
+
+ServiceWorkerContainerChild::ServiceWorkerContainerChild()
+ : mOwner(nullptr), mTeardownStarted(false) {}
+
+void ServiceWorkerContainerChild::SetOwner(ServiceWorkerContainer* aOwner) {
+ MOZ_DIAGNOSTIC_ASSERT(!mOwner);
+ MOZ_DIAGNOSTIC_ASSERT(aOwner);
+ mOwner = aOwner;
+}
+
+void ServiceWorkerContainerChild::RevokeOwner(ServiceWorkerContainer* aOwner) {
+ MOZ_DIAGNOSTIC_ASSERT(mOwner);
+ MOZ_DIAGNOSTIC_ASSERT(aOwner == mOwner);
+ mOwner = nullptr;
+}
+
+void ServiceWorkerContainerChild::MaybeStartTeardown() {
+ if (mTeardownStarted) {
+ return;
+ }
+ mTeardownStarted = true;
+}
+
+} // namespace mozilla::dom
diff --git a/dom/serviceworkers/ServiceWorkerContainerChild.h b/dom/serviceworkers/ServiceWorkerContainerChild.h
new file mode 100644
index 0000000000..4d9df91f0b
--- /dev/null
+++ b/dom/serviceworkers/ServiceWorkerContainerChild.h
@@ -0,0 +1,47 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_serviceworkercontainerchild_h__
+#define mozilla_dom_serviceworkercontainerchild_h__
+
+#include "mozilla/dom/PServiceWorkerContainerChild.h"
+
+// XXX Avoid including this here by moving function bodies to the cpp file
+#include "mozilla/dom/WorkerRef.h"
+
+namespace mozilla::dom {
+
+class ServiceWorkerContainer;
+
+class IPCWorkerRef;
+
+class ServiceWorkerContainerChild final : public PServiceWorkerContainerChild {
+ RefPtr<IPCWorkerRef> mIPCWorkerRef;
+ ServiceWorkerContainer* mOwner;
+ bool mTeardownStarted;
+
+ ServiceWorkerContainerChild();
+
+ ~ServiceWorkerContainerChild() = default;
+
+ // PServiceWorkerContainerChild
+ void ActorDestroy(ActorDestroyReason aReason) override;
+
+ public:
+ NS_INLINE_DECL_REFCOUNTING(ServiceWorkerContainerChild, override);
+
+ static already_AddRefed<ServiceWorkerContainerChild> Create();
+
+ void SetOwner(ServiceWorkerContainer* aOwner);
+
+ void RevokeOwner(ServiceWorkerContainer* aOwner);
+
+ void MaybeStartTeardown();
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_serviceworkercontainerchild_h__
diff --git a/dom/serviceworkers/ServiceWorkerContainerParent.cpp b/dom/serviceworkers/ServiceWorkerContainerParent.cpp
new file mode 100644
index 0000000000..664586d5e8
--- /dev/null
+++ b/dom/serviceworkers/ServiceWorkerContainerParent.cpp
@@ -0,0 +1,129 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "ServiceWorkerContainerParent.h"
+
+#include "ServiceWorkerContainerProxy.h"
+#include "mozilla/dom/ClientInfo.h"
+
+namespace mozilla::dom {
+
+using mozilla::ipc::IPCResult;
+
+void ServiceWorkerContainerParent::ActorDestroy(ActorDestroyReason aReason) {
+ if (mProxy) {
+ mProxy->RevokeActor(this);
+ mProxy = nullptr;
+ }
+}
+
+IPCResult ServiceWorkerContainerParent::RecvTeardown() {
+ Unused << Send__delete__(this);
+ return IPC_OK();
+}
+
+IPCResult ServiceWorkerContainerParent::RecvRegister(
+ const IPCClientInfo& aClientInfo, const nsACString& aScopeURL,
+ const nsACString& aScriptURL,
+ const ServiceWorkerUpdateViaCache& aUpdateViaCache,
+ RegisterResolver&& aResolver) {
+ if (!mProxy) {
+ aResolver(CopyableErrorResult(NS_ERROR_DOM_INVALID_STATE_ERR));
+ return IPC_OK();
+ }
+
+ mProxy
+ ->Register(ClientInfo(aClientInfo), aScopeURL, aScriptURL,
+ aUpdateViaCache)
+ ->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [aResolver](const ServiceWorkerRegistrationDescriptor& aDescriptor) {
+ aResolver(aDescriptor.ToIPC());
+ },
+ [aResolver](const CopyableErrorResult& aResult) {
+ aResolver(aResult);
+ });
+
+ return IPC_OK();
+}
+
+IPCResult ServiceWorkerContainerParent::RecvGetRegistration(
+ const IPCClientInfo& aClientInfo, const nsACString& aURL,
+ GetRegistrationResolver&& aResolver) {
+ if (!mProxy) {
+ aResolver(CopyableErrorResult(NS_ERROR_DOM_INVALID_STATE_ERR));
+ return IPC_OK();
+ }
+
+ mProxy->GetRegistration(ClientInfo(aClientInfo), aURL)
+ ->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [aResolver](const ServiceWorkerRegistrationDescriptor& aDescriptor) {
+ aResolver(aDescriptor.ToIPC());
+ },
+ [aResolver](const CopyableErrorResult& aResult) {
+ aResolver(aResult);
+ });
+
+ return IPC_OK();
+}
+
+IPCResult ServiceWorkerContainerParent::RecvGetRegistrations(
+ const IPCClientInfo& aClientInfo, GetRegistrationsResolver&& aResolver) {
+ if (!mProxy) {
+ aResolver(CopyableErrorResult(NS_ERROR_DOM_INVALID_STATE_ERR));
+ return IPC_OK();
+ }
+
+ mProxy->GetRegistrations(ClientInfo(aClientInfo))
+ ->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [aResolver](
+ const nsTArray<ServiceWorkerRegistrationDescriptor>& aList) {
+ IPCServiceWorkerRegistrationDescriptorList ipcList;
+ for (auto& desc : aList) {
+ ipcList.values().AppendElement(desc.ToIPC());
+ }
+ aResolver(std::move(ipcList));
+ },
+ [aResolver](const CopyableErrorResult& aResult) {
+ aResolver(aResult);
+ });
+
+ return IPC_OK();
+}
+
+IPCResult ServiceWorkerContainerParent::RecvGetReady(
+ const IPCClientInfo& aClientInfo, GetReadyResolver&& aResolver) {
+ if (!mProxy) {
+ aResolver(CopyableErrorResult(NS_ERROR_DOM_INVALID_STATE_ERR));
+ return IPC_OK();
+ }
+
+ mProxy->GetReady(ClientInfo(aClientInfo))
+ ->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [aResolver](const ServiceWorkerRegistrationDescriptor& aDescriptor) {
+ aResolver(aDescriptor.ToIPC());
+ },
+ [aResolver](const CopyableErrorResult& aResult) {
+ aResolver(aResult);
+ });
+
+ return IPC_OK();
+}
+
+ServiceWorkerContainerParent::ServiceWorkerContainerParent() = default;
+
+ServiceWorkerContainerParent::~ServiceWorkerContainerParent() {
+ MOZ_DIAGNOSTIC_ASSERT(!mProxy);
+}
+
+void ServiceWorkerContainerParent::Init() {
+ mProxy = new ServiceWorkerContainerProxy(this);
+}
+
+} // namespace mozilla::dom
diff --git a/dom/serviceworkers/ServiceWorkerContainerParent.h b/dom/serviceworkers/ServiceWorkerContainerParent.h
new file mode 100644
index 0000000000..787a026c18
--- /dev/null
+++ b/dom/serviceworkers/ServiceWorkerContainerParent.h
@@ -0,0 +1,55 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_serviceworkercontainerparent_h__
+#define mozilla_dom_serviceworkercontainerparent_h__
+
+#include "mozilla/dom/PServiceWorkerContainerParent.h"
+
+namespace mozilla::dom {
+
+class IPCServiceWorkerDescriptor;
+class ServiceWorkerContainerProxy;
+
+class ServiceWorkerContainerParent final
+ : public PServiceWorkerContainerParent {
+ RefPtr<ServiceWorkerContainerProxy> mProxy;
+
+ ~ServiceWorkerContainerParent();
+
+ // PServiceWorkerContainerParent
+ void ActorDestroy(ActorDestroyReason aReason) override;
+
+ mozilla::ipc::IPCResult RecvTeardown() override;
+
+ mozilla::ipc::IPCResult RecvRegister(
+ const IPCClientInfo& aClientInfo, const nsACString& aScopeURL,
+ const nsACString& aScriptURL,
+ const ServiceWorkerUpdateViaCache& aUpdateViaCache,
+ RegisterResolver&& aResolver) override;
+
+ mozilla::ipc::IPCResult RecvGetRegistration(
+ const IPCClientInfo& aClientInfo, const nsACString& aURL,
+ GetRegistrationResolver&& aResolver) override;
+
+ mozilla::ipc::IPCResult RecvGetRegistrations(
+ const IPCClientInfo& aClientInfo,
+ GetRegistrationsResolver&& aResolver) override;
+
+ mozilla::ipc::IPCResult RecvGetReady(const IPCClientInfo& aClientInfo,
+ GetReadyResolver&& aResolver) override;
+
+ public:
+ NS_INLINE_DECL_REFCOUNTING(ServiceWorkerContainerParent, override);
+
+ ServiceWorkerContainerParent();
+
+ void Init();
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_serviceworkercontainerparent_h__
diff --git a/dom/serviceworkers/ServiceWorkerContainerProxy.cpp b/dom/serviceworkers/ServiceWorkerContainerProxy.cpp
new file mode 100644
index 0000000000..71a853e1ee
--- /dev/null
+++ b/dom/serviceworkers/ServiceWorkerContainerProxy.cpp
@@ -0,0 +1,153 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "ServiceWorkerContainerProxy.h"
+
+#include "mozilla/dom/ServiceWorkerContainerParent.h"
+#include "mozilla/dom/ServiceWorkerManager.h"
+#include "mozilla/ipc/BackgroundParent.h"
+#include "mozilla/SchedulerGroup.h"
+#include "mozilla/ScopeExit.h"
+
+namespace mozilla::dom {
+
+using mozilla::ipc::AssertIsOnBackgroundThread;
+
+ServiceWorkerContainerProxy::~ServiceWorkerContainerProxy() {
+ // Any thread
+ MOZ_DIAGNOSTIC_ASSERT(!mActor);
+}
+
+ServiceWorkerContainerProxy::ServiceWorkerContainerProxy(
+ ServiceWorkerContainerParent* aActor)
+ : mActor(aActor) {
+ AssertIsOnBackgroundThread();
+ MOZ_DIAGNOSTIC_ASSERT(mActor);
+
+ // The container does not directly listen for updates, so we don't need
+ // to immediately initialize. The controllerchange event comes via the
+ // ClientSource associated with the ServiceWorkerContainer's bound global.
+}
+
+void ServiceWorkerContainerProxy::RevokeActor(
+ ServiceWorkerContainerParent* aActor) {
+ AssertIsOnBackgroundThread();
+ MOZ_DIAGNOSTIC_ASSERT(mActor);
+ MOZ_DIAGNOSTIC_ASSERT(mActor == aActor);
+ mActor = nullptr;
+}
+
+RefPtr<ServiceWorkerRegistrationPromise> ServiceWorkerContainerProxy::Register(
+ const ClientInfo& aClientInfo, const nsACString& aScopeURL,
+ const nsACString& aScriptURL, ServiceWorkerUpdateViaCache aUpdateViaCache) {
+ AssertIsOnBackgroundThread();
+
+ RefPtr<ServiceWorkerRegistrationPromise::Private> promise =
+ new ServiceWorkerRegistrationPromise::Private(__func__);
+
+ nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction(
+ __func__,
+ [aClientInfo, aScopeURL = nsCString(aScopeURL),
+ aScriptURL = nsCString(aScriptURL), aUpdateViaCache, promise]() mutable {
+ auto scopeExit = MakeScopeExit(
+ [&] { promise->Reject(NS_ERROR_DOM_INVALID_STATE_ERR, __func__); });
+
+ RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance();
+ NS_ENSURE_TRUE_VOID(swm);
+
+ swm->Register(aClientInfo, aScopeURL, aScriptURL, aUpdateViaCache)
+ ->ChainTo(promise.forget(), __func__);
+
+ scopeExit.release();
+ });
+
+ MOZ_ALWAYS_SUCCEEDS(
+ SchedulerGroup::Dispatch(TaskCategory::Other, r.forget()));
+
+ return promise;
+}
+
+RefPtr<ServiceWorkerRegistrationPromise>
+ServiceWorkerContainerProxy::GetRegistration(const ClientInfo& aClientInfo,
+ const nsACString& aURL) {
+ AssertIsOnBackgroundThread();
+
+ RefPtr<ServiceWorkerRegistrationPromise::Private> promise =
+ new ServiceWorkerRegistrationPromise::Private(__func__);
+
+ nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction(
+ __func__, [aClientInfo, aURL = nsCString(aURL), promise]() mutable {
+ auto scopeExit = MakeScopeExit(
+ [&] { promise->Reject(NS_ERROR_DOM_INVALID_STATE_ERR, __func__); });
+
+ RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance();
+ NS_ENSURE_TRUE_VOID(swm);
+
+ swm->GetRegistration(aClientInfo, aURL)
+ ->ChainTo(promise.forget(), __func__);
+
+ scopeExit.release();
+ });
+
+ MOZ_ALWAYS_SUCCEEDS(
+ SchedulerGroup::Dispatch(TaskCategory::Other, r.forget()));
+
+ return promise;
+}
+
+RefPtr<ServiceWorkerRegistrationListPromise>
+ServiceWorkerContainerProxy::GetRegistrations(const ClientInfo& aClientInfo) {
+ AssertIsOnBackgroundThread();
+
+ RefPtr<ServiceWorkerRegistrationListPromise::Private> promise =
+ new ServiceWorkerRegistrationListPromise::Private(__func__);
+
+ nsCOMPtr<nsIRunnable> r =
+ NS_NewRunnableFunction(__func__, [aClientInfo, promise]() mutable {
+ auto scopeExit = MakeScopeExit(
+ [&] { promise->Reject(NS_ERROR_DOM_INVALID_STATE_ERR, __func__); });
+
+ RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance();
+ NS_ENSURE_TRUE_VOID(swm);
+
+ swm->GetRegistrations(aClientInfo)->ChainTo(promise.forget(), __func__);
+
+ scopeExit.release();
+ });
+
+ MOZ_ALWAYS_SUCCEEDS(
+ SchedulerGroup::Dispatch(TaskCategory::Other, r.forget()));
+
+ return promise;
+}
+
+RefPtr<ServiceWorkerRegistrationPromise> ServiceWorkerContainerProxy::GetReady(
+ const ClientInfo& aClientInfo) {
+ AssertIsOnBackgroundThread();
+
+ RefPtr<ServiceWorkerRegistrationPromise::Private> promise =
+ new ServiceWorkerRegistrationPromise::Private(__func__);
+
+ nsCOMPtr<nsIRunnable> r =
+ NS_NewRunnableFunction(__func__, [aClientInfo, promise]() mutable {
+ auto scopeExit = MakeScopeExit(
+ [&] { promise->Reject(NS_ERROR_DOM_INVALID_STATE_ERR, __func__); });
+
+ RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance();
+ NS_ENSURE_TRUE_VOID(swm);
+
+ swm->WhenReady(aClientInfo)->ChainTo(promise.forget(), __func__);
+
+ scopeExit.release();
+ });
+
+ MOZ_ALWAYS_SUCCEEDS(
+ SchedulerGroup::Dispatch(TaskCategory::Other, r.forget()));
+
+ return promise;
+}
+
+} // namespace mozilla::dom
diff --git a/dom/serviceworkers/ServiceWorkerContainerProxy.h b/dom/serviceworkers/ServiceWorkerContainerProxy.h
new file mode 100644
index 0000000000..b380465a3e
--- /dev/null
+++ b/dom/serviceworkers/ServiceWorkerContainerProxy.h
@@ -0,0 +1,47 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef moz_dom_ServiceWorkerContainerProxy_h
+#define moz_dom_ServiceWorkerContainerProxy_h
+
+#include "mozilla/dom/ServiceWorkerUtils.h"
+#include "mozilla/RefPtr.h"
+
+namespace mozilla::dom {
+
+class ServiceWorkerContainerParent;
+
+class ServiceWorkerContainerProxy final {
+ // Background thread only
+ RefPtr<ServiceWorkerContainerParent> mActor;
+
+ ~ServiceWorkerContainerProxy();
+
+ public:
+ explicit ServiceWorkerContainerProxy(ServiceWorkerContainerParent* aActor);
+
+ void RevokeActor(ServiceWorkerContainerParent* aActor);
+
+ RefPtr<ServiceWorkerRegistrationPromise> Register(
+ const ClientInfo& aClientInfo, const nsACString& aScopeURL,
+ const nsACString& aScriptURL,
+ ServiceWorkerUpdateViaCache aUpdateViaCache);
+
+ RefPtr<ServiceWorkerRegistrationPromise> GetRegistration(
+ const ClientInfo& aClientInfo, const nsACString& aURL);
+
+ RefPtr<ServiceWorkerRegistrationListPromise> GetRegistrations(
+ const ClientInfo& aClientInfo);
+
+ RefPtr<ServiceWorkerRegistrationPromise> GetReady(
+ const ClientInfo& aClientInfo);
+
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(ServiceWorkerContainerProxy);
+};
+
+} // namespace mozilla::dom
+
+#endif // moz_dom_ServiceWorkerContainerProxy_h
diff --git a/dom/serviceworkers/ServiceWorkerDescriptor.cpp b/dom/serviceworkers/ServiceWorkerDescriptor.cpp
new file mode 100644
index 0000000000..898f271fea
--- /dev/null
+++ b/dom/serviceworkers/ServiceWorkerDescriptor.cpp
@@ -0,0 +1,141 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "ServiceWorkerDescriptor.h"
+#include "mozilla/dom/IPCServiceWorkerDescriptor.h"
+#include "mozilla/dom/ServiceWorkerBinding.h"
+#include "mozilla/ipc/PBackgroundSharedTypes.h"
+
+namespace mozilla::dom {
+
+using mozilla::ipc::PrincipalInfo;
+using mozilla::ipc::PrincipalInfoToPrincipal;
+
+ServiceWorkerDescriptor::ServiceWorkerDescriptor(
+ uint64_t aId, uint64_t aRegistrationId, uint64_t aRegistrationVersion,
+ nsIPrincipal* aPrincipal, const nsACString& aScope,
+ const nsACString& aScriptURL, ServiceWorkerState aState)
+ : mData(MakeUnique<IPCServiceWorkerDescriptor>()) {
+ MOZ_ALWAYS_SUCCEEDS(
+ PrincipalToPrincipalInfo(aPrincipal, &mData->principalInfo()));
+
+ mData->id() = aId;
+ mData->registrationId() = aRegistrationId;
+ mData->registrationVersion() = aRegistrationVersion;
+ mData->scope() = aScope;
+ mData->scriptURL() = aScriptURL;
+ mData->state() = aState;
+ // Set HandlesFetch as true in default
+ mData->handlesFetch() = true;
+}
+
+ServiceWorkerDescriptor::ServiceWorkerDescriptor(
+ uint64_t aId, uint64_t aRegistrationId, uint64_t aRegistrationVersion,
+ const mozilla::ipc::PrincipalInfo& aPrincipalInfo, const nsACString& aScope,
+ const nsACString& aScriptURL, ServiceWorkerState aState)
+ : mData(MakeUnique<IPCServiceWorkerDescriptor>(
+ aId, aRegistrationId, aRegistrationVersion, aPrincipalInfo,
+ nsCString(aScriptURL), nsCString(aScope), aState, true)) {}
+
+ServiceWorkerDescriptor::ServiceWorkerDescriptor(
+ const IPCServiceWorkerDescriptor& aDescriptor)
+ : mData(MakeUnique<IPCServiceWorkerDescriptor>(aDescriptor)) {}
+
+ServiceWorkerDescriptor::ServiceWorkerDescriptor(
+ const ServiceWorkerDescriptor& aRight) {
+ operator=(aRight);
+}
+
+ServiceWorkerDescriptor& ServiceWorkerDescriptor::operator=(
+ const ServiceWorkerDescriptor& aRight) {
+ if (this == &aRight) {
+ return *this;
+ }
+ mData.reset();
+ mData = MakeUnique<IPCServiceWorkerDescriptor>(*aRight.mData);
+ return *this;
+}
+
+ServiceWorkerDescriptor::ServiceWorkerDescriptor(
+ ServiceWorkerDescriptor&& aRight)
+ : mData(std::move(aRight.mData)) {}
+
+ServiceWorkerDescriptor& ServiceWorkerDescriptor::operator=(
+ ServiceWorkerDescriptor&& aRight) {
+ mData.reset();
+ mData = std::move(aRight.mData);
+ return *this;
+}
+
+ServiceWorkerDescriptor::~ServiceWorkerDescriptor() = default;
+
+bool ServiceWorkerDescriptor::operator==(
+ const ServiceWorkerDescriptor& aRight) const {
+ return *mData == *aRight.mData;
+}
+
+uint64_t ServiceWorkerDescriptor::Id() const { return mData->id(); }
+
+uint64_t ServiceWorkerDescriptor::RegistrationId() const {
+ return mData->registrationId();
+}
+
+uint64_t ServiceWorkerDescriptor::RegistrationVersion() const {
+ return mData->registrationVersion();
+}
+
+const mozilla::ipc::PrincipalInfo& ServiceWorkerDescriptor::PrincipalInfo()
+ const {
+ return mData->principalInfo();
+}
+
+Result<nsCOMPtr<nsIPrincipal>, nsresult> ServiceWorkerDescriptor::GetPrincipal()
+ const {
+ AssertIsOnMainThread();
+ return PrincipalInfoToPrincipal(mData->principalInfo());
+}
+
+const nsCString& ServiceWorkerDescriptor::Scope() const {
+ return mData->scope();
+}
+
+const nsCString& ServiceWorkerDescriptor::ScriptURL() const {
+ return mData->scriptURL();
+}
+
+ServiceWorkerState ServiceWorkerDescriptor::State() const {
+ return mData->state();
+}
+
+void ServiceWorkerDescriptor::SetState(ServiceWorkerState aState) {
+ mData->state() = aState;
+}
+
+void ServiceWorkerDescriptor::SetRegistrationVersion(uint64_t aVersion) {
+ MOZ_DIAGNOSTIC_ASSERT(aVersion > mData->registrationVersion());
+ mData->registrationVersion() = aVersion;
+}
+
+bool ServiceWorkerDescriptor::HandlesFetch() const {
+ return mData->handlesFetch();
+}
+
+void ServiceWorkerDescriptor::SetHandlesFetch(bool aHandlesFetch) {
+ mData->handlesFetch() = aHandlesFetch;
+}
+
+bool ServiceWorkerDescriptor::Matches(
+ const ServiceWorkerDescriptor& aDescriptor) const {
+ return Id() == aDescriptor.Id() && Scope() == aDescriptor.Scope() &&
+ ScriptURL() == aDescriptor.ScriptURL() &&
+ PrincipalInfo() == aDescriptor.PrincipalInfo();
+}
+
+const IPCServiceWorkerDescriptor& ServiceWorkerDescriptor::ToIPC() const {
+ return *mData;
+}
+
+} // namespace mozilla::dom
diff --git a/dom/serviceworkers/ServiceWorkerDescriptor.h b/dom/serviceworkers/ServiceWorkerDescriptor.h
new file mode 100644
index 0000000000..b85890089b
--- /dev/null
+++ b/dom/serviceworkers/ServiceWorkerDescriptor.h
@@ -0,0 +1,100 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#ifndef _mozilla_dom_ServiceWorkerDescriptor_h
+#define _mozilla_dom_ServiceWorkerDescriptor_h
+
+#include "mozilla/UniquePtr.h"
+#include "nsCOMPtr.h"
+#include "nsString.h"
+
+class nsIPrincipal;
+
+namespace mozilla {
+
+namespace ipc {
+class PrincipalInfo;
+} // namespace ipc
+
+namespace dom {
+
+class IPCServiceWorkerDescriptor;
+enum class ServiceWorkerState : uint8_t;
+
+// This class represents a snapshot of a particular ServiceWorkerInfo object.
+// It is threadsafe and can be transferred across processes. This is useful
+// because most of its values are immutable and can be relied upon to be
+// accurate. Currently the only variable field is the ServiceWorkerState.
+class ServiceWorkerDescriptor final {
+ // This class is largely a wrapper around an IPDL generated struct. We
+ // need the wrapper class since IPDL generated code includes windows.h
+ // which is in turn incompatible with bindings code.
+ UniquePtr<IPCServiceWorkerDescriptor> mData;
+
+ public:
+ ServiceWorkerDescriptor(uint64_t aId, uint64_t aRegistrationId,
+ uint64_t aRegistrationVersion,
+ nsIPrincipal* aPrincipal, const nsACString& aScope,
+ const nsACString& aScriptURL,
+ ServiceWorkerState aState);
+
+ ServiceWorkerDescriptor(uint64_t aId, uint64_t aRegistrationId,
+ uint64_t aRegistrationVersion,
+ const mozilla::ipc::PrincipalInfo& aPrincipalInfo,
+ const nsACString& aScope,
+ const nsACString& aScriptURL,
+ ServiceWorkerState aState);
+
+ explicit ServiceWorkerDescriptor(
+ const IPCServiceWorkerDescriptor& aDescriptor);
+
+ ServiceWorkerDescriptor(const ServiceWorkerDescriptor& aRight);
+
+ ServiceWorkerDescriptor& operator=(const ServiceWorkerDescriptor& aRight);
+
+ ServiceWorkerDescriptor(ServiceWorkerDescriptor&& aRight);
+
+ ServiceWorkerDescriptor& operator=(ServiceWorkerDescriptor&& aRight);
+
+ ~ServiceWorkerDescriptor();
+
+ bool operator==(const ServiceWorkerDescriptor& aRight) const;
+
+ uint64_t Id() const;
+
+ uint64_t RegistrationId() const;
+
+ uint64_t RegistrationVersion() const;
+
+ const mozilla::ipc::PrincipalInfo& PrincipalInfo() const;
+
+ Result<nsCOMPtr<nsIPrincipal>, nsresult> GetPrincipal() const;
+
+ const nsCString& Scope() const;
+
+ const nsCString& ScriptURL() const;
+
+ ServiceWorkerState State() const;
+
+ void SetState(ServiceWorkerState aState);
+
+ void SetRegistrationVersion(uint64_t aVersion);
+
+ bool HandlesFetch() const;
+
+ void SetHandlesFetch(bool aHandlesFetch);
+
+ // Try to determine if two workers match each other. This is less strict
+ // than an operator==() call since it ignores mutable values like State().
+ bool Matches(const ServiceWorkerDescriptor& aDescriptor) const;
+
+ // Expose the underlying IPC type so that it can be passed via IPC.
+ const IPCServiceWorkerDescriptor& ToIPC() const;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // _mozilla_dom_ServiceWorkerDescriptor_h
diff --git a/dom/serviceworkers/ServiceWorkerEvents.cpp b/dom/serviceworkers/ServiceWorkerEvents.cpp
new file mode 100644
index 0000000000..5c4b55ebcf
--- /dev/null
+++ b/dom/serviceworkers/ServiceWorkerEvents.cpp
@@ -0,0 +1,1278 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "ServiceWorkerEvents.h"
+
+#include <utility>
+
+#include "ServiceWorker.h"
+#include "ServiceWorkerManager.h"
+#include "js/Conversions.h"
+#include "js/Exception.h" // JS::ExceptionStack, JS::StealPendingExceptionStack
+#include "js/TypeDecls.h"
+#include "mozilla/Encoding.h"
+#include "mozilla/ErrorResult.h"
+#include "mozilla/HoldDropJSObjects.h"
+#include "mozilla/LoadInfo.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/dom/BodyUtil.h"
+#include "mozilla/dom/Client.h"
+#include "mozilla/dom/EventBinding.h"
+#include "mozilla/dom/FetchEventBinding.h"
+#include "mozilla/dom/MessagePort.h"
+#include "mozilla/dom/PromiseNativeHandler.h"
+#include "mozilla/dom/PushEventBinding.h"
+#include "mozilla/dom/PushMessageDataBinding.h"
+#include "mozilla/dom/PushUtil.h"
+#include "mozilla/dom/Request.h"
+#include "mozilla/dom/Response.h"
+#include "mozilla/dom/ServiceWorkerOp.h"
+#include "mozilla/dom/TypedArray.h"
+#include "mozilla/dom/WorkerPrivate.h"
+#include "mozilla/dom/WorkerScope.h"
+#include "mozilla/net/NeckoChannelParams.h"
+#include "mozilla/Telemetry.h"
+#include "nsComponentManagerUtils.h"
+#include "nsContentPolicyUtils.h"
+#include "nsContentUtils.h"
+#include "nsIConsoleReportCollector.h"
+#include "nsINetworkInterceptController.h"
+#include "nsIScriptError.h"
+#include "nsNetCID.h"
+#include "nsNetUtil.h"
+#include "nsQueryObject.h"
+#include "nsSerializationHelper.h"
+#include "nsServiceManagerUtils.h"
+#include "nsStreamUtils.h"
+#include "xpcpublic.h"
+
+using namespace mozilla;
+using namespace mozilla::dom;
+
+namespace {
+
+void AsyncLog(nsIInterceptedChannel* aInterceptedChannel,
+ const nsACString& aRespondWithScriptSpec,
+ uint32_t aRespondWithLineNumber,
+ uint32_t aRespondWithColumnNumber, const nsACString& aMessageName,
+ const nsTArray<nsString>& aParams) {
+ MOZ_ASSERT(aInterceptedChannel);
+ nsCOMPtr<nsIConsoleReportCollector> reporter =
+ aInterceptedChannel->GetConsoleReportCollector();
+ if (reporter) {
+ reporter->AddConsoleReport(nsIScriptError::errorFlag,
+ "Service Worker Interception"_ns,
+ nsContentUtils::eDOM_PROPERTIES,
+ aRespondWithScriptSpec, aRespondWithLineNumber,
+ aRespondWithColumnNumber, aMessageName, aParams);
+ }
+}
+
+template <typename... Params>
+void AsyncLog(nsIInterceptedChannel* aInterceptedChannel,
+ const nsACString& aRespondWithScriptSpec,
+ uint32_t aRespondWithLineNumber,
+ uint32_t aRespondWithColumnNumber,
+ // We have to list one explicit string so that calls with an
+ // nsTArray of params won't end up in here.
+ const nsACString& aMessageName, const nsAString& aFirstParam,
+ Params&&... aParams) {
+ nsTArray<nsString> paramsList(sizeof...(Params) + 1);
+ StringArrayAppender::Append(paramsList, sizeof...(Params) + 1, aFirstParam,
+ std::forward<Params>(aParams)...);
+ AsyncLog(aInterceptedChannel, aRespondWithScriptSpec, aRespondWithLineNumber,
+ aRespondWithColumnNumber, aMessageName, paramsList);
+}
+
+} // anonymous namespace
+
+namespace mozilla::dom {
+
+CancelChannelRunnable::CancelChannelRunnable(
+ nsMainThreadPtrHandle<nsIInterceptedChannel>& aChannel,
+ nsMainThreadPtrHandle<ServiceWorkerRegistrationInfo>& aRegistration,
+ nsresult aStatus)
+ : Runnable("dom::CancelChannelRunnable"),
+ mChannel(aChannel),
+ mRegistration(aRegistration),
+ mStatus(aStatus) {}
+
+NS_IMETHODIMP
+CancelChannelRunnable::Run() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ mChannel->CancelInterception(mStatus);
+ mRegistration->MaybeScheduleUpdate();
+ return NS_OK;
+}
+
+FetchEvent::FetchEvent(EventTarget* aOwner)
+ : ExtendableEvent(aOwner),
+ mPreventDefaultLineNumber(0),
+ mPreventDefaultColumnNumber(0),
+ mWaitToRespond(false) {}
+
+FetchEvent::~FetchEvent() = default;
+
+void FetchEvent::PostInit(
+ nsMainThreadPtrHandle<nsIInterceptedChannel>& aChannel,
+ nsMainThreadPtrHandle<ServiceWorkerRegistrationInfo>& aRegistration,
+ const nsACString& aScriptSpec) {
+ mChannel = aChannel;
+ mRegistration = aRegistration;
+ mScriptSpec.Assign(aScriptSpec);
+}
+
+void FetchEvent::PostInit(const nsACString& aScriptSpec,
+ RefPtr<FetchEventOp> aRespondWithHandler) {
+ MOZ_ASSERT(aRespondWithHandler);
+
+ mScriptSpec.Assign(aScriptSpec);
+ mRespondWithHandler = std::move(aRespondWithHandler);
+}
+
+/*static*/
+already_AddRefed<FetchEvent> FetchEvent::Constructor(
+ const GlobalObject& aGlobal, const nsAString& aType,
+ const FetchEventInit& aOptions) {
+ RefPtr<EventTarget> owner = do_QueryObject(aGlobal.GetAsSupports());
+ MOZ_ASSERT(owner);
+ RefPtr<FetchEvent> e = new FetchEvent(owner);
+ bool trusted = e->Init(owner);
+ e->InitEvent(aType, aOptions.mBubbles, aOptions.mCancelable);
+ e->SetTrusted(trusted);
+ e->SetComposed(aOptions.mComposed);
+ e->mRequest = aOptions.mRequest;
+ e->mClientId = aOptions.mClientId;
+ e->mResultingClientId = aOptions.mResultingClientId;
+ RefPtr<nsIGlobalObject> global = do_QueryObject(aGlobal.GetAsSupports());
+ MOZ_ASSERT(global);
+ ErrorResult rv;
+ e->mHandled = Promise::Create(global, rv);
+ if (rv.Failed()) {
+ rv.SuppressException();
+ return nullptr;
+ }
+ e->mPreloadResponse = Promise::Create(global, rv);
+ if (rv.Failed()) {
+ rv.SuppressException();
+ return nullptr;
+ }
+ return e.forget();
+}
+
+namespace {
+
+struct RespondWithClosure {
+ nsMainThreadPtrHandle<nsIInterceptedChannel> mInterceptedChannel;
+ nsMainThreadPtrHandle<ServiceWorkerRegistrationInfo> mRegistration;
+ const nsString mRequestURL;
+ const nsCString mRespondWithScriptSpec;
+ const uint32_t mRespondWithLineNumber;
+ const uint32_t mRespondWithColumnNumber;
+
+ RespondWithClosure(
+ nsMainThreadPtrHandle<nsIInterceptedChannel>& aChannel,
+ nsMainThreadPtrHandle<ServiceWorkerRegistrationInfo>& aRegistration,
+ const nsAString& aRequestURL, const nsACString& aRespondWithScriptSpec,
+ uint32_t aRespondWithLineNumber, uint32_t aRespondWithColumnNumber)
+ : mInterceptedChannel(aChannel),
+ mRegistration(aRegistration),
+ mRequestURL(aRequestURL),
+ mRespondWithScriptSpec(aRespondWithScriptSpec),
+ mRespondWithLineNumber(aRespondWithLineNumber),
+ mRespondWithColumnNumber(aRespondWithColumnNumber) {}
+};
+
+class FinishResponse final : public Runnable {
+ nsMainThreadPtrHandle<nsIInterceptedChannel> mChannel;
+
+ public:
+ explicit FinishResponse(
+ nsMainThreadPtrHandle<nsIInterceptedChannel>& aChannel)
+ : Runnable("dom::FinishResponse"), mChannel(aChannel) {}
+
+ NS_IMETHOD
+ Run() override {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ nsresult rv = mChannel->FinishSynthesizedResponse();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ mChannel->CancelInterception(NS_ERROR_INTERCEPTION_FAILED);
+ return NS_OK;
+ }
+
+ return rv;
+ }
+};
+
+class BodyCopyHandle final : public nsIInterceptedBodyCallback {
+ UniquePtr<RespondWithClosure> mClosure;
+
+ ~BodyCopyHandle() = default;
+
+ public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+
+ explicit BodyCopyHandle(UniquePtr<RespondWithClosure>&& aClosure)
+ : mClosure(std::move(aClosure)) {}
+
+ NS_IMETHOD
+ BodyComplete(nsresult aRv) override {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ nsCOMPtr<nsIRunnable> event;
+ if (NS_WARN_IF(NS_FAILED(aRv))) {
+ ::AsyncLog(
+ mClosure->mInterceptedChannel, mClosure->mRespondWithScriptSpec,
+ mClosure->mRespondWithLineNumber, mClosure->mRespondWithColumnNumber,
+ "InterceptionFailedWithURL"_ns, mClosure->mRequestURL);
+ event = new CancelChannelRunnable(mClosure->mInterceptedChannel,
+ mClosure->mRegistration,
+ NS_ERROR_INTERCEPTION_FAILED);
+ } else {
+ event = new FinishResponse(mClosure->mInterceptedChannel);
+ }
+
+ mClosure.reset();
+
+ event->Run();
+
+ return NS_OK;
+ }
+};
+
+NS_IMPL_ISUPPORTS(BodyCopyHandle, nsIInterceptedBodyCallback)
+
+class StartResponse final : public Runnable {
+ nsMainThreadPtrHandle<nsIInterceptedChannel> mChannel;
+ SafeRefPtr<InternalResponse> mInternalResponse;
+ ChannelInfo mWorkerChannelInfo;
+ const nsCString mScriptSpec;
+ const nsCString mResponseURLSpec;
+ UniquePtr<RespondWithClosure> mClosure;
+
+ public:
+ StartResponse(nsMainThreadPtrHandle<nsIInterceptedChannel>& aChannel,
+ SafeRefPtr<InternalResponse> aInternalResponse,
+ const ChannelInfo& aWorkerChannelInfo,
+ const nsACString& aScriptSpec,
+ const nsACString& aResponseURLSpec,
+ UniquePtr<RespondWithClosure>&& aClosure)
+ : Runnable("dom::StartResponse"),
+ mChannel(aChannel),
+ mInternalResponse(std::move(aInternalResponse)),
+ mWorkerChannelInfo(aWorkerChannelInfo),
+ mScriptSpec(aScriptSpec),
+ mResponseURLSpec(aResponseURLSpec),
+ mClosure(std::move(aClosure)) {}
+
+ NS_IMETHOD
+ Run() override {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ nsCOMPtr<nsIChannel> underlyingChannel;
+ nsresult rv = mChannel->GetChannel(getter_AddRefs(underlyingChannel));
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(underlyingChannel, NS_ERROR_UNEXPECTED);
+ nsCOMPtr<nsILoadInfo> loadInfo = underlyingChannel->LoadInfo();
+
+ if (!CSPPermitsResponse(loadInfo)) {
+ mChannel->CancelInterception(NS_ERROR_CONTENT_BLOCKED);
+ return NS_OK;
+ }
+
+ ChannelInfo channelInfo;
+ if (mInternalResponse->GetChannelInfo().IsInitialized()) {
+ channelInfo = mInternalResponse->GetChannelInfo();
+ } else {
+ // We are dealing with a synthesized response here, so fall back to the
+ // channel info for the worker script.
+ channelInfo = mWorkerChannelInfo;
+ }
+ rv = mChannel->SetChannelInfo(&channelInfo);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ mChannel->CancelInterception(NS_ERROR_INTERCEPTION_FAILED);
+ return NS_OK;
+ }
+
+ rv = mChannel->SynthesizeStatus(
+ mInternalResponse->GetUnfilteredStatus(),
+ mInternalResponse->GetUnfilteredStatusText());
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ mChannel->CancelInterception(NS_ERROR_INTERCEPTION_FAILED);
+ return NS_OK;
+ }
+
+ AutoTArray<InternalHeaders::Entry, 5> entries;
+ mInternalResponse->UnfilteredHeaders()->GetEntries(entries);
+ for (uint32_t i = 0; i < entries.Length(); ++i) {
+ mChannel->SynthesizeHeader(entries[i].mName, entries[i].mValue);
+ }
+
+ auto castLoadInfo = static_cast<mozilla::net::LoadInfo*>(loadInfo.get());
+ castLoadInfo->SynthesizeServiceWorkerTainting(
+ mInternalResponse->GetTainting());
+
+ // Get the preferred alternative data type of outter channel
+ nsAutoCString preferredAltDataType(""_ns);
+ nsCOMPtr<nsICacheInfoChannel> outerChannel =
+ do_QueryInterface(underlyingChannel);
+ if (outerChannel &&
+ !outerChannel->PreferredAlternativeDataTypes().IsEmpty()) {
+ // TODO: handle multiple types properly.
+ preferredAltDataType.Assign(
+ outerChannel->PreferredAlternativeDataTypes()[0].type());
+ }
+
+ // Get the alternative data type saved in the InternalResponse
+ nsAutoCString altDataType;
+ nsCOMPtr<nsICacheInfoChannel> cacheInfoChannel =
+ mInternalResponse->TakeCacheInfoChannel().get();
+ if (cacheInfoChannel) {
+ cacheInfoChannel->GetAlternativeDataType(altDataType);
+ }
+
+ nsCOMPtr<nsIInputStream> body;
+ if (preferredAltDataType.Equals(altDataType)) {
+ body = mInternalResponse->TakeAlternativeBody();
+ }
+ if (!body) {
+ mInternalResponse->GetUnfilteredBody(getter_AddRefs(body));
+ } else {
+ Telemetry::ScalarAdd(Telemetry::ScalarID::SW_ALTERNATIVE_BODY_USED_COUNT,
+ 1);
+ }
+
+ RefPtr<BodyCopyHandle> copyHandle;
+ copyHandle = new BodyCopyHandle(std::move(mClosure));
+
+ rv = mChannel->StartSynthesizedResponse(body, copyHandle, cacheInfoChannel,
+ mResponseURLSpec,
+ mInternalResponse->IsRedirected());
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ mChannel->CancelInterception(NS_ERROR_INTERCEPTION_FAILED);
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIObserverService> obsService = services::GetObserverService();
+ if (obsService) {
+ obsService->NotifyObservers(
+ underlyingChannel, "service-worker-synthesized-response", nullptr);
+ }
+
+ return rv;
+ }
+
+ bool CSPPermitsResponse(nsILoadInfo* aLoadInfo) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aLoadInfo);
+ nsresult rv;
+ nsCOMPtr<nsIURI> uri;
+ nsCString url = mInternalResponse->GetUnfilteredURL();
+ if (url.IsEmpty()) {
+ // Synthetic response. The buck stops at the worker script.
+ url = mScriptSpec;
+ }
+ rv = NS_NewURI(getter_AddRefs(uri), url);
+ NS_ENSURE_SUCCESS(rv, false);
+ int16_t decision = nsIContentPolicy::ACCEPT;
+ rv = NS_CheckContentLoadPolicy(uri, aLoadInfo, ""_ns, &decision);
+ NS_ENSURE_SUCCESS(rv, false);
+ return decision == nsIContentPolicy::ACCEPT;
+ }
+};
+
+class RespondWithHandler final : public PromiseNativeHandler {
+ nsMainThreadPtrHandle<nsIInterceptedChannel> mInterceptedChannel;
+ nsMainThreadPtrHandle<ServiceWorkerRegistrationInfo> mRegistration;
+ const RequestMode mRequestMode;
+ const RequestRedirect mRequestRedirectMode;
+#ifdef DEBUG
+ const bool mIsClientRequest;
+#endif
+ const nsCString mScriptSpec;
+ const nsString mRequestURL;
+ const nsCString mRequestFragment;
+ const nsCString mRespondWithScriptSpec;
+ const uint32_t mRespondWithLineNumber;
+ const uint32_t mRespondWithColumnNumber;
+ bool mRequestWasHandled;
+
+ public:
+ NS_DECL_ISUPPORTS
+
+ RespondWithHandler(
+ nsMainThreadPtrHandle<nsIInterceptedChannel>& aChannel,
+ nsMainThreadPtrHandle<ServiceWorkerRegistrationInfo>& aRegistration,
+ RequestMode aRequestMode, bool aIsClientRequest,
+ RequestRedirect aRedirectMode, const nsACString& aScriptSpec,
+ const nsAString& aRequestURL, const nsACString& aRequestFragment,
+ const nsACString& aRespondWithScriptSpec, uint32_t aRespondWithLineNumber,
+ uint32_t aRespondWithColumnNumber)
+ : mInterceptedChannel(aChannel),
+ mRegistration(aRegistration),
+ mRequestMode(aRequestMode),
+ mRequestRedirectMode(aRedirectMode)
+#ifdef DEBUG
+ ,
+ mIsClientRequest(aIsClientRequest)
+#endif
+ ,
+ mScriptSpec(aScriptSpec),
+ mRequestURL(aRequestURL),
+ mRequestFragment(aRequestFragment),
+ mRespondWithScriptSpec(aRespondWithScriptSpec),
+ mRespondWithLineNumber(aRespondWithLineNumber),
+ mRespondWithColumnNumber(aRespondWithColumnNumber),
+ mRequestWasHandled(false) {
+ }
+
+ void ResolvedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue,
+ ErrorResult& aRv) override;
+
+ void RejectedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue,
+ ErrorResult& aRv) override;
+
+ void CancelRequest(nsresult aStatus);
+
+ void AsyncLog(const nsACString& aMessageName,
+ const nsTArray<nsString>& aParams) {
+ ::AsyncLog(mInterceptedChannel, mRespondWithScriptSpec,
+ mRespondWithLineNumber, mRespondWithColumnNumber, aMessageName,
+ aParams);
+ }
+
+ void AsyncLog(const nsACString& aSourceSpec, uint32_t aLine, uint32_t aColumn,
+ const nsACString& aMessageName,
+ const nsTArray<nsString>& aParams) {
+ ::AsyncLog(mInterceptedChannel, aSourceSpec, aLine, aColumn, aMessageName,
+ aParams);
+ }
+
+ private:
+ ~RespondWithHandler() {
+ if (!mRequestWasHandled) {
+ ::AsyncLog(mInterceptedChannel, mRespondWithScriptSpec,
+ mRespondWithLineNumber, mRespondWithColumnNumber,
+ "InterceptionFailedWithURL"_ns, mRequestURL);
+ CancelRequest(NS_ERROR_INTERCEPTION_FAILED);
+ }
+ }
+};
+
+class MOZ_STACK_CLASS AutoCancel {
+ RefPtr<RespondWithHandler> mOwner;
+ nsCString mSourceSpec;
+ uint32_t mLine;
+ uint32_t mColumn;
+ nsCString mMessageName;
+ nsTArray<nsString> mParams;
+
+ public:
+ AutoCancel(RespondWithHandler* aOwner, const nsString& aRequestURL)
+ : mOwner(aOwner),
+ mLine(0),
+ mColumn(0),
+ mMessageName("InterceptionFailedWithURL"_ns) {
+ mParams.AppendElement(aRequestURL);
+ }
+
+ ~AutoCancel() {
+ if (mOwner) {
+ if (mSourceSpec.IsEmpty()) {
+ mOwner->AsyncLog(mMessageName, mParams);
+ } else {
+ mOwner->AsyncLog(mSourceSpec, mLine, mColumn, mMessageName, mParams);
+ }
+ mOwner->CancelRequest(NS_ERROR_INTERCEPTION_FAILED);
+ }
+ }
+
+ // This function steals the error message from a ErrorResult.
+ void SetCancelErrorResult(JSContext* aCx, ErrorResult& aRv) {
+ MOZ_DIAGNOSTIC_ASSERT(aRv.Failed());
+ MOZ_DIAGNOSTIC_ASSERT(!JS_IsExceptionPending(aCx));
+
+ // Storing the error as exception in the JSContext.
+ if (!aRv.MaybeSetPendingException(aCx)) {
+ return;
+ }
+
+ MOZ_ASSERT(!aRv.Failed());
+
+ // Let's take the pending exception.
+ JS::ExceptionStack exnStack(aCx);
+ if (!JS::StealPendingExceptionStack(aCx, &exnStack)) {
+ return;
+ }
+
+ // Converting the exception in a JS::ErrorReportBuilder.
+ JS::ErrorReportBuilder report(aCx);
+ if (!report.init(aCx, exnStack, JS::ErrorReportBuilder::WithSideEffects)) {
+ JS_ClearPendingException(aCx);
+ return;
+ }
+
+ MOZ_ASSERT(mOwner);
+ MOZ_ASSERT(mMessageName.EqualsLiteral("InterceptionFailedWithURL"));
+ MOZ_ASSERT(mParams.Length() == 1);
+
+ // Let's store the error message here.
+ mMessageName.Assign(report.toStringResult().c_str());
+ mParams.Clear();
+ }
+
+ template <typename... Params>
+ void SetCancelMessage(const nsACString& aMessageName, Params&&... aParams) {
+ MOZ_ASSERT(mOwner);
+ MOZ_ASSERT(mMessageName.EqualsLiteral("InterceptionFailedWithURL"));
+ MOZ_ASSERT(mParams.Length() == 1);
+ mMessageName = aMessageName;
+ mParams.Clear();
+ StringArrayAppender::Append(mParams, sizeof...(Params),
+ std::forward<Params>(aParams)...);
+ }
+
+ template <typename... Params>
+ void SetCancelMessageAndLocation(const nsACString& aSourceSpec,
+ uint32_t aLine, uint32_t aColumn,
+ const nsACString& aMessageName,
+ Params&&... aParams) {
+ MOZ_ASSERT(mOwner);
+ MOZ_ASSERT(mMessageName.EqualsLiteral("InterceptionFailedWithURL"));
+ MOZ_ASSERT(mParams.Length() == 1);
+
+ mSourceSpec = aSourceSpec;
+ mLine = aLine;
+ mColumn = aColumn;
+
+ mMessageName = aMessageName;
+ mParams.Clear();
+ StringArrayAppender::Append(mParams, sizeof...(Params),
+ std::forward<Params>(aParams)...);
+ }
+
+ void Reset() { mOwner = nullptr; }
+};
+
+NS_IMPL_ISUPPORTS0(RespondWithHandler)
+
+void RespondWithHandler::ResolvedCallback(JSContext* aCx,
+ JS::Handle<JS::Value> aValue,
+ ErrorResult& aRv) {
+ AutoCancel autoCancel(this, mRequestURL);
+
+ if (!aValue.isObject()) {
+ NS_WARNING(
+ "FetchEvent::RespondWith was passed a promise resolved to a non-Object "
+ "value");
+
+ nsCString sourceSpec;
+ uint32_t line = 0;
+ uint32_t column = 0;
+ nsString valueString;
+ nsContentUtils::ExtractErrorValues(aCx, aValue, sourceSpec, &line, &column,
+ valueString);
+
+ autoCancel.SetCancelMessageAndLocation(sourceSpec, line, column,
+ "InterceptedNonResponseWithURL"_ns,
+ mRequestURL, valueString);
+ return;
+ }
+
+ RefPtr<Response> response;
+ nsresult rv = UNWRAP_OBJECT(Response, &aValue.toObject(), response);
+ if (NS_FAILED(rv)) {
+ nsCString sourceSpec;
+ uint32_t line = 0;
+ uint32_t column = 0;
+ nsString valueString;
+ nsContentUtils::ExtractErrorValues(aCx, aValue, sourceSpec, &line, &column,
+ valueString);
+
+ autoCancel.SetCancelMessageAndLocation(sourceSpec, line, column,
+ "InterceptedNonResponseWithURL"_ns,
+ mRequestURL, valueString);
+ return;
+ }
+
+ WorkerPrivate* worker = GetCurrentThreadWorkerPrivate();
+ MOZ_ASSERT(worker);
+ worker->AssertIsOnWorkerThread();
+
+ // Section "HTTP Fetch", step 3.3:
+ // If one of the following conditions is true, return a network error:
+ // * response's type is "error".
+ // * request's mode is not "no-cors" and response's type is "opaque".
+ // * request's redirect mode is not "manual" and response's type is
+ // "opaqueredirect".
+ // * request's redirect mode is not "follow" and response's url list
+ // has more than one item.
+
+ if (response->Type() == ResponseType::Error) {
+ autoCancel.SetCancelMessage("InterceptedErrorResponseWithURL"_ns,
+ mRequestURL);
+ return;
+ }
+
+ MOZ_ASSERT_IF(mIsClientRequest, mRequestMode == RequestMode::Same_origin ||
+ mRequestMode == RequestMode::Navigate);
+
+ if (response->Type() == ResponseType::Opaque &&
+ mRequestMode != RequestMode::No_cors) {
+ NS_ConvertASCIItoUTF16 modeString(
+ RequestModeValues::GetString(mRequestMode));
+
+ autoCancel.SetCancelMessage("BadOpaqueInterceptionRequestModeWithURL"_ns,
+ mRequestURL, modeString);
+ return;
+ }
+
+ if (mRequestRedirectMode != RequestRedirect::Manual &&
+ response->Type() == ResponseType::Opaqueredirect) {
+ autoCancel.SetCancelMessage("BadOpaqueRedirectInterceptionWithURL"_ns,
+ mRequestURL);
+ return;
+ }
+
+ if (mRequestRedirectMode != RequestRedirect::Follow &&
+ response->Redirected()) {
+ autoCancel.SetCancelMessage("BadRedirectModeInterceptionWithURL"_ns,
+ mRequestURL);
+ return;
+ }
+
+ {
+ ErrorResult error;
+ bool bodyUsed = response->GetBodyUsed(error);
+ error.WouldReportJSException();
+ if (NS_WARN_IF(error.Failed())) {
+ autoCancel.SetCancelErrorResult(aCx, error);
+ return;
+ }
+ if (NS_WARN_IF(bodyUsed)) {
+ autoCancel.SetCancelMessage("InterceptedUsedResponseWithURL"_ns,
+ mRequestURL);
+ return;
+ }
+ }
+
+ SafeRefPtr<InternalResponse> ir = response->GetInternalResponse();
+ if (NS_WARN_IF(!ir)) {
+ return;
+ }
+
+ // An extra safety check to make sure our invariant that opaque and cors
+ // responses always have a URL does not break.
+ if (NS_WARN_IF((response->Type() == ResponseType::Opaque ||
+ response->Type() == ResponseType::Cors) &&
+ ir->GetUnfilteredURL().IsEmpty())) {
+ MOZ_DIAGNOSTIC_ASSERT(false, "Cors or opaque Response without a URL");
+ return;
+ }
+
+ if (mRequestMode == RequestMode::Same_origin &&
+ response->Type() == ResponseType::Cors) {
+ Telemetry::ScalarAdd(Telemetry::ScalarID::SW_CORS_RES_FOR_SO_REQ_COUNT, 1);
+
+ // XXXtt: Will have a pref to enable the quirk response in bug 1419684.
+ // The variadic template provided by StringArrayAppender requires exactly
+ // an nsString.
+ NS_ConvertUTF8toUTF16 responseURL(ir->GetUnfilteredURL());
+ autoCancel.SetCancelMessage("CorsResponseForSameOriginRequest"_ns,
+ mRequestURL, responseURL);
+ return;
+ }
+
+ // Propagate the URL to the content if the request mode is not "navigate".
+ // Note that, we only reflect the final URL if the response.redirected is
+ // false. We propagate all the URLs if the response.redirected is true.
+ nsCString responseURL;
+ if (mRequestMode != RequestMode::Navigate) {
+ responseURL = ir->GetUnfilteredURL();
+
+ // Similar to how we apply the request fragment to redirects automatically
+ // we also want to apply it automatically when propagating the response
+ // URL from a service worker interception. Currently response.url strips
+ // the fragment, so this will never conflict with an existing fragment
+ // on the response. In the future we will have to check for a response
+ // fragment and avoid overriding in that case.
+ if (!mRequestFragment.IsEmpty() && !responseURL.IsEmpty()) {
+ MOZ_ASSERT(!responseURL.Contains('#'));
+ responseURL.Append("#"_ns);
+ responseURL.Append(mRequestFragment);
+ }
+ }
+
+ UniquePtr<RespondWithClosure> closure(new RespondWithClosure(
+ mInterceptedChannel, mRegistration, mRequestURL, mRespondWithScriptSpec,
+ mRespondWithLineNumber, mRespondWithColumnNumber));
+
+ nsCOMPtr<nsIRunnable> startRunnable = new StartResponse(
+ mInterceptedChannel, ir.clonePtr(), worker->GetChannelInfo(), mScriptSpec,
+ responseURL, std::move(closure));
+
+ nsCOMPtr<nsIInputStream> body;
+ ir->GetUnfilteredBody(getter_AddRefs(body));
+ // Errors and redirects may not have a body.
+ if (body) {
+ ErrorResult error;
+ response->SetBodyUsed(aCx, error);
+ error.WouldReportJSException();
+ if (NS_WARN_IF(error.Failed())) {
+ autoCancel.SetCancelErrorResult(aCx, error);
+ return;
+ }
+ }
+
+ MOZ_ALWAYS_SUCCEEDS(worker->DispatchToMainThread(startRunnable.forget()));
+
+ MOZ_ASSERT(!closure);
+ autoCancel.Reset();
+ mRequestWasHandled = true;
+}
+
+void RespondWithHandler::RejectedCallback(JSContext* aCx,
+ JS::Handle<JS::Value> aValue,
+ ErrorResult& aRv) {
+ nsCString sourceSpec = mRespondWithScriptSpec;
+ uint32_t line = mRespondWithLineNumber;
+ uint32_t column = mRespondWithColumnNumber;
+ nsString valueString;
+
+ nsContentUtils::ExtractErrorValues(aCx, aValue, sourceSpec, &line, &column,
+ valueString);
+
+ ::AsyncLog(mInterceptedChannel, sourceSpec, line, column,
+ "InterceptionRejectedResponseWithURL"_ns, mRequestURL,
+ valueString);
+
+ CancelRequest(NS_ERROR_INTERCEPTION_FAILED);
+}
+
+void RespondWithHandler::CancelRequest(nsresult aStatus) {
+ nsCOMPtr<nsIRunnable> runnable =
+ new CancelChannelRunnable(mInterceptedChannel, mRegistration, aStatus);
+ // Note, this may run off the worker thread during worker termination.
+ WorkerPrivate* worker = GetCurrentThreadWorkerPrivate();
+ if (worker) {
+ MOZ_ALWAYS_SUCCEEDS(worker->DispatchToMainThread(runnable.forget()));
+ } else {
+ MOZ_ALWAYS_SUCCEEDS(NS_DispatchToMainThread(runnable.forget()));
+ }
+ mRequestWasHandled = true;
+}
+
+} // namespace
+
+void FetchEvent::RespondWith(JSContext* aCx, Promise& aArg, ErrorResult& aRv) {
+ if (!GetDispatchFlag() || mWaitToRespond) {
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return;
+ }
+
+ // Record where respondWith() was called in the script so we can include the
+ // information in any error reporting. We should be guaranteed not to get
+ // a file:// string here because service workers require http/https.
+ nsCString spec;
+ uint32_t line = 0;
+ uint32_t column = 0;
+ nsJSUtils::GetCallingLocation(aCx, spec, &line, &column);
+
+ SafeRefPtr<InternalRequest> ir = mRequest->GetInternalRequest();
+
+ nsAutoCString requestURL;
+ ir->GetURL(requestURL);
+
+ StopImmediatePropagation();
+ mWaitToRespond = true;
+
+ if (mChannel) {
+ RefPtr<RespondWithHandler> handler = new RespondWithHandler(
+ mChannel, mRegistration, mRequest->Mode(), ir->IsClientRequest(),
+ mRequest->Redirect(), mScriptSpec, NS_ConvertUTF8toUTF16(requestURL),
+ ir->GetFragment(), spec, line, column);
+
+ aArg.AppendNativeHandler(handler);
+ // mRespondWithHandler can be nullptr for self-dispatched FetchEvent.
+ } else if (mRespondWithHandler) {
+ mRespondWithHandler->RespondWithCalledAt(spec, line, column);
+ aArg.AppendNativeHandler(mRespondWithHandler);
+ mRespondWithHandler = nullptr;
+ }
+
+ if (!WaitOnPromise(aArg)) {
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ }
+}
+
+void FetchEvent::PreventDefault(JSContext* aCx, CallerType aCallerType) {
+ MOZ_ASSERT(aCx);
+ MOZ_ASSERT(aCallerType != CallerType::System,
+ "Since when do we support system-principal service workers?");
+
+ if (mPreventDefaultScriptSpec.IsEmpty()) {
+ // Note when the FetchEvent might have been canceled by script, but don't
+ // actually log the location until we are sure it matters. This is
+ // determined in ServiceWorkerPrivate.cpp. We only remember the first
+ // call to preventDefault() as its the most likely to have actually canceled
+ // the event.
+ nsJSUtils::GetCallingLocation(aCx, mPreventDefaultScriptSpec,
+ &mPreventDefaultLineNumber,
+ &mPreventDefaultColumnNumber);
+ }
+
+ Event::PreventDefault(aCx, aCallerType);
+}
+
+void FetchEvent::ReportCanceled() {
+ MOZ_ASSERT(!mPreventDefaultScriptSpec.IsEmpty());
+
+ SafeRefPtr<InternalRequest> ir = mRequest->GetInternalRequest();
+ nsAutoCString url;
+ ir->GetURL(url);
+
+ // The variadic template provided by StringArrayAppender requires exactly
+ // an nsString.
+ NS_ConvertUTF8toUTF16 requestURL(url);
+ // nsString requestURL;
+ // CopyUTF8toUTF16(url, requestURL);
+
+ if (mChannel) {
+ ::AsyncLog(mChannel.get(), mPreventDefaultScriptSpec,
+ mPreventDefaultLineNumber, mPreventDefaultColumnNumber,
+ "InterceptionCanceledWithURL"_ns, requestURL);
+ // mRespondWithHandler could be nullptr for self-dispatched FetchEvent.
+ } else if (mRespondWithHandler) {
+ mRespondWithHandler->ReportCanceled(mPreventDefaultScriptSpec,
+ mPreventDefaultLineNumber,
+ mPreventDefaultColumnNumber);
+ mRespondWithHandler = nullptr;
+ }
+}
+
+namespace {
+
+class WaitUntilHandler final : public PromiseNativeHandler {
+ WorkerPrivate* mWorkerPrivate;
+ const nsCString mScope;
+ nsString mSourceSpec;
+ uint32_t mLine;
+ uint32_t mColumn;
+ nsString mRejectValue;
+
+ ~WaitUntilHandler() = default;
+
+ public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+
+ WaitUntilHandler(WorkerPrivate* aWorkerPrivate, JSContext* aCx)
+ : mWorkerPrivate(aWorkerPrivate),
+ mScope(mWorkerPrivate->ServiceWorkerScope()),
+ mLine(0),
+ mColumn(0) {
+ mWorkerPrivate->AssertIsOnWorkerThread();
+
+ // Save the location of the waitUntil() call itself as a fallback
+ // in case the rejection value does not contain any location info.
+ nsJSUtils::GetCallingLocation(aCx, mSourceSpec, &mLine, &mColumn);
+ }
+
+ void ResolvedCallback(JSContext* aCx, JS::Handle<JS::Value> aValu,
+ ErrorResult& aRve) override {
+ // do nothing, we are only here to report errors
+ }
+
+ void RejectedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue,
+ ErrorResult& aRv) override {
+ mWorkerPrivate->AssertIsOnWorkerThread();
+
+ nsString spec;
+ uint32_t line = 0;
+ uint32_t column = 0;
+ nsContentUtils::ExtractErrorValues(aCx, aValue, spec, &line, &column,
+ mRejectValue);
+
+ // only use the extracted location if we found one
+ if (!spec.IsEmpty()) {
+ mSourceSpec = spec;
+ mLine = line;
+ mColumn = column;
+ }
+
+ MOZ_ALWAYS_SUCCEEDS(mWorkerPrivate->DispatchToMainThread(
+ NewRunnableMethod("WaitUntilHandler::ReportOnMainThread", this,
+ &WaitUntilHandler::ReportOnMainThread)));
+ }
+
+ void ReportOnMainThread() {
+ MOZ_ASSERT(NS_IsMainThread());
+ RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance();
+ if (!swm) {
+ // browser shutdown
+ return;
+ }
+
+ // TODO: Make the error message a localized string. (bug 1222720)
+ nsString message;
+ message.AppendLiteral(
+ "Service worker event waitUntil() was passed a "
+ "promise that rejected with '");
+ message.Append(mRejectValue);
+ message.AppendLiteral("'.");
+
+ // Note, there is a corner case where this won't report to the window
+ // that triggered the error. Consider a navigation fetch event that
+ // rejects waitUntil() without holding respondWith() open. In this case
+ // there is no controlling document yet, the window did call .register()
+ // because there is no documeny yet, and the navigation is no longer
+ // being intercepted.
+
+ swm->ReportToAllClients(mScope, message, mSourceSpec, u""_ns, mLine,
+ mColumn, nsIScriptError::errorFlag);
+ }
+};
+
+NS_IMPL_ISUPPORTS0(WaitUntilHandler)
+
+} // anonymous namespace
+
+ExtendableEvent::ExtensionsHandler::~ExtensionsHandler() {
+ MOZ_ASSERT(!mExtendableEvent);
+}
+
+bool ExtendableEvent::ExtensionsHandler::GetDispatchFlag() const {
+ // mExtendableEvent should set itself as nullptr in its destructor, and we
+ // can't be dispatching an event that doesn't exist, so this should work for
+ // as long as it's not needed to determine whether the event is still alive,
+ // which seems unlikely.
+ if (!mExtendableEvent) {
+ return false;
+ }
+
+ return mExtendableEvent->GetDispatchFlag();
+}
+
+void ExtendableEvent::ExtensionsHandler::SetExtendableEvent(
+ const ExtendableEvent* const aExtendableEvent) {
+ mExtendableEvent = aExtendableEvent;
+}
+
+NS_IMPL_ADDREF_INHERITED(FetchEvent, ExtendableEvent)
+NS_IMPL_RELEASE_INHERITED(FetchEvent, ExtendableEvent)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(FetchEvent)
+NS_INTERFACE_MAP_END_INHERITING(ExtendableEvent)
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(FetchEvent, ExtendableEvent, mRequest,
+ mHandled, mPreloadResponse)
+
+ExtendableEvent::ExtendableEvent(EventTarget* aOwner)
+ : Event(aOwner, nullptr, nullptr) {}
+
+bool ExtendableEvent::WaitOnPromise(Promise& aPromise) {
+ if (!mExtensionsHandler) {
+ return false;
+ }
+ return mExtensionsHandler->WaitOnPromise(aPromise);
+}
+
+void ExtendableEvent::SetKeepAliveHandler(
+ ExtensionsHandler* aExtensionsHandler) {
+ MOZ_ASSERT(!mExtensionsHandler);
+ WorkerPrivate* worker = GetCurrentThreadWorkerPrivate();
+ MOZ_ASSERT(worker);
+ worker->AssertIsOnWorkerThread();
+ mExtensionsHandler = aExtensionsHandler;
+ mExtensionsHandler->SetExtendableEvent(this);
+}
+
+void ExtendableEvent::WaitUntil(JSContext* aCx, Promise& aPromise,
+ ErrorResult& aRv) {
+ MOZ_ASSERT(!NS_IsMainThread());
+
+ if (!WaitOnPromise(aPromise)) {
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return;
+ }
+
+ // Append our handler to each waitUntil promise separately so we
+ // can record the location in script where waitUntil was called.
+ RefPtr<WaitUntilHandler> handler =
+ new WaitUntilHandler(GetCurrentThreadWorkerPrivate(), aCx);
+ aPromise.AppendNativeHandler(handler);
+}
+
+NS_IMPL_ADDREF_INHERITED(ExtendableEvent, Event)
+NS_IMPL_RELEASE_INHERITED(ExtendableEvent, Event)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ExtendableEvent)
+NS_INTERFACE_MAP_END_INHERITING(Event)
+
+namespace {
+nsresult ExtractBytesFromUSVString(const nsAString& aStr,
+ nsTArray<uint8_t>& aBytes) {
+ MOZ_ASSERT(aBytes.IsEmpty());
+ auto encoder = UTF_8_ENCODING->NewEncoder();
+ CheckedInt<size_t> needed =
+ encoder->MaxBufferLengthFromUTF16WithoutReplacement(aStr.Length());
+ if (NS_WARN_IF(!needed.isValid() ||
+ !aBytes.SetLength(needed.value(), fallible))) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ uint32_t result;
+ size_t read;
+ size_t written;
+ // Do not use structured binding lest deal with [-Werror=unused-variable]
+ std::tie(result, read, written) =
+ encoder->EncodeFromUTF16WithoutReplacement(aStr, aBytes, true);
+ MOZ_ASSERT(result == kInputEmpty);
+ MOZ_ASSERT(read == aStr.Length());
+ aBytes.TruncateLength(written);
+ return NS_OK;
+}
+
+nsresult ExtractBytesFromData(
+ const OwningArrayBufferViewOrArrayBufferOrUSVString& aDataInit,
+ nsTArray<uint8_t>& aBytes) {
+ if (aDataInit.IsArrayBufferView()) {
+ const ArrayBufferView& view = aDataInit.GetAsArrayBufferView();
+ if (NS_WARN_IF(!PushUtil::CopyArrayBufferViewToArray(view, aBytes))) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ return NS_OK;
+ }
+ if (aDataInit.IsArrayBuffer()) {
+ const ArrayBuffer& buffer = aDataInit.GetAsArrayBuffer();
+ if (NS_WARN_IF(!PushUtil::CopyArrayBufferToArray(buffer, aBytes))) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ return NS_OK;
+ }
+ if (aDataInit.IsUSVString()) {
+ return ExtractBytesFromUSVString(aDataInit.GetAsUSVString(), aBytes);
+ }
+ MOZ_ASSERT_UNREACHABLE("Unexpected push message data");
+ return NS_ERROR_FAILURE;
+}
+} // namespace
+
+PushMessageData::PushMessageData(nsIGlobalObject* aOwner,
+ nsTArray<uint8_t>&& aBytes)
+ : mOwner(aOwner), mBytes(std::move(aBytes)) {}
+
+PushMessageData::~PushMessageData() = default;
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(PushMessageData, mOwner)
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(PushMessageData)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(PushMessageData)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(PushMessageData)
+ NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+JSObject* PushMessageData::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return mozilla::dom::PushMessageData_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+void PushMessageData::Json(JSContext* cx, JS::MutableHandle<JS::Value> aRetval,
+ ErrorResult& aRv) {
+ if (NS_FAILED(EnsureDecodedText())) {
+ aRv.Throw(NS_ERROR_DOM_UNKNOWN_ERR);
+ return;
+ }
+ BodyUtil::ConsumeJson(cx, aRetval, mDecodedText, aRv);
+}
+
+void PushMessageData::Text(nsAString& aData) {
+ if (NS_SUCCEEDED(EnsureDecodedText())) {
+ aData = mDecodedText;
+ }
+}
+
+void PushMessageData::ArrayBuffer(JSContext* cx,
+ JS::MutableHandle<JSObject*> aRetval,
+ ErrorResult& aRv) {
+ uint8_t* data = GetContentsCopy();
+ if (data) {
+ BodyUtil::ConsumeArrayBuffer(cx, aRetval, mBytes.Length(), data, aRv);
+ }
+}
+
+already_AddRefed<mozilla::dom::Blob> PushMessageData::Blob(ErrorResult& aRv) {
+ uint8_t* data = GetContentsCopy();
+ if (data) {
+ RefPtr<mozilla::dom::Blob> blob =
+ BodyUtil::ConsumeBlob(mOwner, u""_ns, mBytes.Length(), data, aRv);
+ if (blob) {
+ return blob.forget();
+ }
+ }
+ return nullptr;
+}
+
+nsresult PushMessageData::EnsureDecodedText() {
+ if (mBytes.IsEmpty() || !mDecodedText.IsEmpty()) {
+ return NS_OK;
+ }
+ nsresult rv = BodyUtil::ConsumeText(
+ mBytes.Length(), reinterpret_cast<uint8_t*>(mBytes.Elements()),
+ mDecodedText);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ mDecodedText.Truncate();
+ return rv;
+ }
+ return NS_OK;
+}
+
+uint8_t* PushMessageData::GetContentsCopy() {
+ uint32_t length = mBytes.Length();
+ void* data = malloc(length);
+ if (!data) {
+ return nullptr;
+ }
+ memcpy(data, mBytes.Elements(), length);
+ return reinterpret_cast<uint8_t*>(data);
+}
+
+PushEvent::PushEvent(EventTarget* aOwner) : ExtendableEvent(aOwner) {}
+
+already_AddRefed<PushEvent> PushEvent::Constructor(
+ mozilla::dom::EventTarget* aOwner, const nsAString& aType,
+ const PushEventInit& aOptions, ErrorResult& aRv) {
+ RefPtr<PushEvent> e = new PushEvent(aOwner);
+ bool trusted = e->Init(aOwner);
+ e->InitEvent(aType, aOptions.mBubbles, aOptions.mCancelable);
+ e->SetTrusted(trusted);
+ e->SetComposed(aOptions.mComposed);
+ if (aOptions.mData.WasPassed()) {
+ nsTArray<uint8_t> bytes;
+ nsresult rv = ExtractBytesFromData(aOptions.mData.Value(), bytes);
+ if (NS_FAILED(rv)) {
+ aRv.Throw(rv);
+ return nullptr;
+ }
+ e->mData = new PushMessageData(aOwner->GetOwnerGlobal(), std::move(bytes));
+ }
+ return e.forget();
+}
+
+NS_IMPL_ADDREF_INHERITED(PushEvent, ExtendableEvent)
+NS_IMPL_RELEASE_INHERITED(PushEvent, ExtendableEvent)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(PushEvent)
+NS_INTERFACE_MAP_END_INHERITING(ExtendableEvent)
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(PushEvent, ExtendableEvent, mData)
+
+JSObject* PushEvent::WrapObjectInternal(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return mozilla::dom::PushEvent_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+ExtendableMessageEvent::ExtendableMessageEvent(EventTarget* aOwner)
+ : ExtendableEvent(aOwner), mData(JS::UndefinedValue()) {
+ mozilla::HoldJSObjects(this);
+}
+
+ExtendableMessageEvent::~ExtendableMessageEvent() { DropJSObjects(this); }
+
+void ExtendableMessageEvent::GetData(JSContext* aCx,
+ JS::MutableHandle<JS::Value> aData,
+ ErrorResult& aRv) {
+ aData.set(mData);
+ if (!JS_WrapValue(aCx, aData)) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ }
+}
+
+void ExtendableMessageEvent::GetSource(
+ Nullable<OwningClientOrServiceWorkerOrMessagePort>& aValue) const {
+ if (mClient) {
+ aValue.SetValue().SetAsClient() = mClient;
+ } else if (mServiceWorker) {
+ aValue.SetValue().SetAsServiceWorker() = mServiceWorker;
+ } else if (mMessagePort) {
+ aValue.SetValue().SetAsMessagePort() = mMessagePort;
+ } else {
+ // nullptr source is possible for manually constructed event
+ aValue.SetNull();
+ }
+}
+
+/* static */
+already_AddRefed<ExtendableMessageEvent> ExtendableMessageEvent::Constructor(
+ const GlobalObject& aGlobal, const nsAString& aType,
+ const ExtendableMessageEventInit& aOptions) {
+ nsCOMPtr<EventTarget> t = do_QueryInterface(aGlobal.GetAsSupports());
+ return Constructor(t, aType, aOptions);
+}
+
+/* static */
+already_AddRefed<ExtendableMessageEvent> ExtendableMessageEvent::Constructor(
+ mozilla::dom::EventTarget* aEventTarget, const nsAString& aType,
+ const ExtendableMessageEventInit& aOptions) {
+ RefPtr<ExtendableMessageEvent> event =
+ new ExtendableMessageEvent(aEventTarget);
+
+ event->InitEvent(aType, aOptions.mBubbles, aOptions.mCancelable);
+ bool trusted = event->Init(aEventTarget);
+ event->SetTrusted(trusted);
+
+ event->mData = aOptions.mData;
+ event->mOrigin = aOptions.mOrigin;
+ event->mLastEventId = aOptions.mLastEventId;
+
+ if (!aOptions.mSource.IsNull()) {
+ if (aOptions.mSource.Value().IsClient()) {
+ event->mClient = aOptions.mSource.Value().GetAsClient();
+ } else if (aOptions.mSource.Value().IsServiceWorker()) {
+ event->mServiceWorker = aOptions.mSource.Value().GetAsServiceWorker();
+ } else if (aOptions.mSource.Value().IsMessagePort()) {
+ event->mMessagePort = aOptions.mSource.Value().GetAsMessagePort();
+ }
+ }
+
+ event->mPorts.AppendElements(aOptions.mPorts);
+ return event.forget();
+}
+
+void ExtendableMessageEvent::GetPorts(nsTArray<RefPtr<MessagePort>>& aPorts) {
+ aPorts = mPorts.Clone();
+}
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(ExtendableMessageEvent)
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(ExtendableMessageEvent, Event)
+ tmp->mData.setUndefined();
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mClient)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mServiceWorker)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mMessagePort)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mPorts)
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(ExtendableMessageEvent, Event)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mClient)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mServiceWorker)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mMessagePort)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPorts)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(ExtendableMessageEvent, Event)
+ NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mData)
+NS_IMPL_CYCLE_COLLECTION_TRACE_END
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ExtendableMessageEvent)
+NS_INTERFACE_MAP_END_INHERITING(Event)
+
+NS_IMPL_ADDREF_INHERITED(ExtendableMessageEvent, Event)
+NS_IMPL_RELEASE_INHERITED(ExtendableMessageEvent, Event)
+
+} // namespace mozilla::dom
diff --git a/dom/serviceworkers/ServiceWorkerEvents.h b/dom/serviceworkers/ServiceWorkerEvents.h
new file mode 100644
index 0000000000..2003c8afe9
--- /dev/null
+++ b/dom/serviceworkers/ServiceWorkerEvents.h
@@ -0,0 +1,309 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_serviceworkerevents_h__
+#define mozilla_dom_serviceworkerevents_h__
+
+#include "mozilla/Attributes.h"
+#include "mozilla/dom/Event.h"
+#include "mozilla/dom/ExtendableEventBinding.h"
+#include "mozilla/dom/ExtendableMessageEventBinding.h"
+#include "mozilla/dom/FetchEventBinding.h"
+#include "mozilla/dom/File.h"
+#include "mozilla/dom/Promise.h"
+#include "mozilla/dom/Response.h"
+#include "mozilla/dom/WorkerCommon.h"
+
+#include "nsProxyRelease.h"
+#include "nsContentUtils.h"
+
+class nsIInterceptedChannel;
+
+namespace mozilla::dom {
+
+class Blob;
+class Client;
+class FetchEventOp;
+class MessagePort;
+struct PushEventInit;
+class Request;
+class ResponseOrPromise;
+class ServiceWorker;
+class ServiceWorkerRegistrationInfo;
+
+// Defined in ServiceWorker.cpp
+bool ServiceWorkerVisible(JSContext* aCx, JSObject* aObj);
+
+class CancelChannelRunnable final : public Runnable {
+ nsMainThreadPtrHandle<nsIInterceptedChannel> mChannel;
+ nsMainThreadPtrHandle<ServiceWorkerRegistrationInfo> mRegistration;
+ const nsresult mStatus;
+
+ public:
+ CancelChannelRunnable(
+ nsMainThreadPtrHandle<nsIInterceptedChannel>& aChannel,
+ nsMainThreadPtrHandle<ServiceWorkerRegistrationInfo>& aRegistration,
+ nsresult aStatus);
+
+ NS_IMETHOD Run() override;
+};
+
+enum ExtendableEventResult { Rejected = 0, Resolved };
+
+class ExtendableEventCallback {
+ public:
+ virtual void FinishedWithResult(ExtendableEventResult aResult) = 0;
+
+ NS_INLINE_DECL_PURE_VIRTUAL_REFCOUNTING
+};
+
+class ExtendableEvent : public Event {
+ public:
+ class ExtensionsHandler {
+ friend class ExtendableEvent;
+
+ public:
+ virtual bool WaitOnPromise(Promise& aPromise) = 0;
+
+ NS_INLINE_DECL_PURE_VIRTUAL_REFCOUNTING
+
+ protected:
+ virtual ~ExtensionsHandler();
+
+ // Also returns false if the owning ExtendableEvent is destroyed.
+ bool GetDispatchFlag() const;
+
+ private:
+ // Only the owning ExtendableEvent is allowed to set this data.
+ void SetExtendableEvent(const ExtendableEvent* const aExtendableEvent);
+
+ MOZ_NON_OWNING_REF const ExtendableEvent* mExtendableEvent = nullptr;
+ };
+
+ private:
+ RefPtr<ExtensionsHandler> mExtensionsHandler;
+
+ protected:
+ bool GetDispatchFlag() const { return mEvent->mFlags.mIsBeingDispatched; }
+
+ bool WaitOnPromise(Promise& aPromise);
+
+ explicit ExtendableEvent(mozilla::dom::EventTarget* aOwner);
+
+ ~ExtendableEvent() {
+ if (mExtensionsHandler) {
+ mExtensionsHandler->SetExtendableEvent(nullptr);
+ }
+ };
+
+ public:
+ NS_DECL_ISUPPORTS_INHERITED
+
+ void SetKeepAliveHandler(ExtensionsHandler* aExtensionsHandler);
+
+ virtual JSObject* WrapObjectInternal(
+ JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override {
+ return mozilla::dom::ExtendableEvent_Binding::Wrap(aCx, this, aGivenProto);
+ }
+
+ static already_AddRefed<ExtendableEvent> Constructor(
+ mozilla::dom::EventTarget* aOwner, const nsAString& aType,
+ const EventInit& aOptions) {
+ RefPtr<ExtendableEvent> e = new ExtendableEvent(aOwner);
+ bool trusted = e->Init(aOwner);
+ e->InitEvent(aType, aOptions.mBubbles, aOptions.mCancelable);
+ e->SetTrusted(trusted);
+ e->SetComposed(aOptions.mComposed);
+ return e.forget();
+ }
+
+ static already_AddRefed<ExtendableEvent> Constructor(
+ const GlobalObject& aGlobal, const nsAString& aType,
+ const EventInit& aOptions) {
+ nsCOMPtr<EventTarget> target = do_QueryInterface(aGlobal.GetAsSupports());
+ return Constructor(target, aType, aOptions);
+ }
+
+ void WaitUntil(JSContext* aCx, Promise& aPromise, ErrorResult& aRv);
+
+ virtual ExtendableEvent* AsExtendableEvent() override { return this; }
+};
+
+class FetchEvent final : public ExtendableEvent {
+ RefPtr<FetchEventOp> mRespondWithHandler;
+ nsMainThreadPtrHandle<nsIInterceptedChannel> mChannel;
+ nsMainThreadPtrHandle<ServiceWorkerRegistrationInfo> mRegistration;
+ RefPtr<Request> mRequest;
+ RefPtr<Promise> mHandled;
+ RefPtr<Promise> mPreloadResponse;
+ nsCString mScriptSpec;
+ nsCString mPreventDefaultScriptSpec;
+ nsString mClientId;
+ nsString mResultingClientId;
+ uint32_t mPreventDefaultLineNumber;
+ uint32_t mPreventDefaultColumnNumber;
+ bool mWaitToRespond;
+
+ protected:
+ explicit FetchEvent(EventTarget* aOwner);
+ ~FetchEvent();
+
+ public:
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(FetchEvent, ExtendableEvent)
+
+ virtual JSObject* WrapObjectInternal(
+ JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override {
+ return FetchEvent_Binding::Wrap(aCx, this, aGivenProto);
+ }
+
+ void PostInit(
+ nsMainThreadPtrHandle<nsIInterceptedChannel>& aChannel,
+ nsMainThreadPtrHandle<ServiceWorkerRegistrationInfo>& aRegistration,
+ const nsACString& aScriptSpec);
+
+ void PostInit(const nsACString& aScriptSpec,
+ RefPtr<FetchEventOp> aRespondWithHandler);
+
+ static already_AddRefed<FetchEvent> Constructor(
+ const GlobalObject& aGlobal, const nsAString& aType,
+ const FetchEventInit& aOptions);
+
+ bool WaitToRespond() const { return mWaitToRespond; }
+
+ Request* Request_() const {
+ MOZ_ASSERT(mRequest);
+ return mRequest;
+ }
+
+ void GetClientId(nsAString& aClientId) const { aClientId = mClientId; }
+
+ void GetResultingClientId(nsAString& aResultingClientId) const {
+ aResultingClientId = mResultingClientId;
+ }
+
+ Promise* Handled() const { return mHandled; }
+
+ Promise* PreloadResponse() const { return mPreloadResponse; }
+
+ void RespondWith(JSContext* aCx, Promise& aArg, ErrorResult& aRv);
+
+ // Pull in the Event version of PreventDefault so we don't get
+ // shadowing warnings.
+ using Event::PreventDefault;
+ void PreventDefault(JSContext* aCx, CallerType aCallerType) override;
+
+ void ReportCanceled();
+};
+
+class PushMessageData final : public nsISupports, public nsWrapperCache {
+ public:
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(PushMessageData)
+
+ virtual JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ nsIGlobalObject* GetParentObject() const { return mOwner; }
+
+ void Json(JSContext* cx, JS::MutableHandle<JS::Value> aRetval,
+ ErrorResult& aRv);
+ void Text(nsAString& aData);
+ void ArrayBuffer(JSContext* cx, JS::MutableHandle<JSObject*> aRetval,
+ ErrorResult& aRv);
+ already_AddRefed<mozilla::dom::Blob> Blob(ErrorResult& aRv);
+
+ PushMessageData(nsIGlobalObject* aOwner, nsTArray<uint8_t>&& aBytes);
+
+ private:
+ nsCOMPtr<nsIGlobalObject> mOwner;
+ nsTArray<uint8_t> mBytes;
+ nsString mDecodedText;
+ ~PushMessageData();
+
+ nsresult EnsureDecodedText();
+ uint8_t* GetContentsCopy();
+};
+
+class PushEvent final : public ExtendableEvent {
+ RefPtr<PushMessageData> mData;
+
+ protected:
+ explicit PushEvent(mozilla::dom::EventTarget* aOwner);
+ ~PushEvent() = default;
+
+ public:
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(PushEvent, ExtendableEvent)
+
+ virtual JSObject* WrapObjectInternal(
+ JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override;
+
+ static already_AddRefed<PushEvent> Constructor(
+ mozilla::dom::EventTarget* aOwner, const nsAString& aType,
+ const PushEventInit& aOptions, ErrorResult& aRv);
+
+ static already_AddRefed<PushEvent> Constructor(const GlobalObject& aGlobal,
+ const nsAString& aType,
+ const PushEventInit& aOptions,
+ ErrorResult& aRv) {
+ nsCOMPtr<EventTarget> owner = do_QueryInterface(aGlobal.GetAsSupports());
+ return Constructor(owner, aType, aOptions, aRv);
+ }
+
+ PushMessageData* GetData() const { return mData; }
+};
+
+class ExtendableMessageEvent final : public ExtendableEvent {
+ JS::Heap<JS::Value> mData;
+ nsString mOrigin;
+ nsString mLastEventId;
+ RefPtr<Client> mClient;
+ RefPtr<ServiceWorker> mServiceWorker;
+ RefPtr<MessagePort> mMessagePort;
+ nsTArray<RefPtr<MessagePort>> mPorts;
+
+ protected:
+ explicit ExtendableMessageEvent(EventTarget* aOwner);
+ ~ExtendableMessageEvent();
+
+ public:
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_INHERITED(ExtendableMessageEvent,
+ ExtendableEvent)
+
+ virtual JSObject* WrapObjectInternal(
+ JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override {
+ return mozilla::dom::ExtendableMessageEvent_Binding::Wrap(aCx, this,
+ aGivenProto);
+ }
+
+ static already_AddRefed<ExtendableMessageEvent> Constructor(
+ mozilla::dom::EventTarget* aOwner, const nsAString& aType,
+ const ExtendableMessageEventInit& aOptions);
+
+ static already_AddRefed<ExtendableMessageEvent> Constructor(
+ const GlobalObject& aGlobal, const nsAString& aType,
+ const ExtendableMessageEventInit& aOptions);
+
+ void GetData(JSContext* aCx, JS::MutableHandle<JS::Value> aData,
+ ErrorResult& aRv);
+
+ void GetSource(
+ Nullable<OwningClientOrServiceWorkerOrMessagePort>& aValue) const;
+
+ void GetOrigin(nsAString& aOrigin) const { aOrigin = mOrigin; }
+
+ void GetLastEventId(nsAString& aLastEventId) const {
+ aLastEventId = mLastEventId;
+ }
+
+ void GetPorts(nsTArray<RefPtr<MessagePort>>& aPorts);
+};
+
+} // namespace mozilla::dom
+
+#endif /* mozilla_dom_serviceworkerevents_h__ */
diff --git a/dom/serviceworkers/ServiceWorkerIPCUtils.h b/dom/serviceworkers/ServiceWorkerIPCUtils.h
new file mode 100644
index 0000000000..bd867f877a
--- /dev/null
+++ b/dom/serviceworkers/ServiceWorkerIPCUtils.h
@@ -0,0 +1,35 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#ifndef _mozilla_dom_ServiceWorkerIPCUtils_h
+#define _mozilla_dom_ServiceWorkerIPCUtils_h
+
+#include "ipc/EnumSerializer.h"
+
+// Undo X11/X.h's definition of None
+#undef None
+
+#include "mozilla/dom/ServiceWorkerBinding.h"
+#include "mozilla/dom/ServiceWorkerRegistrationBinding.h"
+
+namespace IPC {
+
+template <>
+struct ParamTraits<mozilla::dom::ServiceWorkerState>
+ : public ContiguousEnumSerializer<
+ mozilla::dom::ServiceWorkerState,
+ mozilla::dom::ServiceWorkerState::Parsed,
+ mozilla::dom::ServiceWorkerState::EndGuard_> {};
+
+template <>
+struct ParamTraits<mozilla::dom::ServiceWorkerUpdateViaCache>
+ : public ContiguousEnumSerializer<
+ mozilla::dom::ServiceWorkerUpdateViaCache,
+ mozilla::dom::ServiceWorkerUpdateViaCache::Imports,
+ mozilla::dom::ServiceWorkerUpdateViaCache::EndGuard_> {};
+
+} // namespace IPC
+
+#endif // _mozilla_dom_ServiceWorkerIPCUtils_h
diff --git a/dom/serviceworkers/ServiceWorkerInfo.cpp b/dom/serviceworkers/ServiceWorkerInfo.cpp
new file mode 100644
index 0000000000..9998cfed6b
--- /dev/null
+++ b/dom/serviceworkers/ServiceWorkerInfo.cpp
@@ -0,0 +1,286 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "ServiceWorkerInfo.h"
+
+#include "ServiceWorkerUtils.h"
+#include "ServiceWorkerPrivate.h"
+#include "ServiceWorkerScriptCache.h"
+#include "mozilla/dom/ClientIPCTypes.h"
+#include "mozilla/dom/ClientState.h"
+#include "mozilla/dom/RemoteWorkerTypes.h"
+#include "mozilla/dom/WorkerPrivate.h"
+
+namespace mozilla::dom {
+
+using mozilla::ipc::PrincipalInfo;
+
+static_assert(nsIServiceWorkerInfo::STATE_PARSED ==
+ static_cast<uint16_t>(ServiceWorkerState::Parsed),
+ "ServiceWorkerState enumeration value should match state values "
+ "from nsIServiceWorkerInfo.");
+static_assert(nsIServiceWorkerInfo::STATE_INSTALLING ==
+ static_cast<uint16_t>(ServiceWorkerState::Installing),
+ "ServiceWorkerState enumeration value should match state values "
+ "from nsIServiceWorkerInfo.");
+static_assert(nsIServiceWorkerInfo::STATE_INSTALLED ==
+ static_cast<uint16_t>(ServiceWorkerState::Installed),
+ "ServiceWorkerState enumeration value should match state values "
+ "from nsIServiceWorkerInfo.");
+static_assert(nsIServiceWorkerInfo::STATE_ACTIVATING ==
+ static_cast<uint16_t>(ServiceWorkerState::Activating),
+ "ServiceWorkerState enumeration value should match state values "
+ "from nsIServiceWorkerInfo.");
+static_assert(nsIServiceWorkerInfo::STATE_ACTIVATED ==
+ static_cast<uint16_t>(ServiceWorkerState::Activated),
+ "ServiceWorkerState enumeration value should match state values "
+ "from nsIServiceWorkerInfo.");
+static_assert(nsIServiceWorkerInfo::STATE_REDUNDANT ==
+ static_cast<uint16_t>(ServiceWorkerState::Redundant),
+ "ServiceWorkerState enumeration value should match state values "
+ "from nsIServiceWorkerInfo.");
+static_assert(nsIServiceWorkerInfo::STATE_UNKNOWN ==
+ ServiceWorkerStateValues::Count,
+ "ServiceWorkerState enumeration value should match state values "
+ "from nsIServiceWorkerInfo.");
+
+NS_IMPL_ISUPPORTS(ServiceWorkerInfo, nsIServiceWorkerInfo)
+
+NS_IMETHODIMP
+ServiceWorkerInfo::GetId(nsAString& aId) {
+ MOZ_ASSERT(NS_IsMainThread());
+ aId = mWorkerPrivateId;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+ServiceWorkerInfo::GetScriptSpec(nsAString& aScriptSpec) {
+ MOZ_ASSERT(NS_IsMainThread());
+ CopyUTF8toUTF16(mDescriptor.ScriptURL(), aScriptSpec);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+ServiceWorkerInfo::GetCacheName(nsAString& aCacheName) {
+ MOZ_ASSERT(NS_IsMainThread());
+ aCacheName = mCacheName;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+ServiceWorkerInfo::GetState(uint16_t* aState) {
+ MOZ_ASSERT(aState);
+ MOZ_ASSERT(NS_IsMainThread());
+ *aState = static_cast<uint16_t>(State());
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+ServiceWorkerInfo::GetDebugger(nsIWorkerDebugger** aResult) {
+ if (NS_WARN_IF(!aResult)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ return mServiceWorkerPrivate->GetDebugger(aResult);
+}
+
+NS_IMETHODIMP
+ServiceWorkerInfo::GetHandlesFetchEvents(bool* aValue) {
+ MOZ_ASSERT(aValue);
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (mHandlesFetch == Unknown) {
+ return NS_ERROR_FAILURE;
+ }
+
+ *aValue = HandlesFetch();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+ServiceWorkerInfo::GetInstalledTime(PRTime* _retval) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(_retval);
+ *_retval = mInstalledTime;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+ServiceWorkerInfo::GetActivatedTime(PRTime* _retval) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(_retval);
+ *_retval = mActivatedTime;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+ServiceWorkerInfo::GetRedundantTime(PRTime* _retval) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(_retval);
+ *_retval = mRedundantTime;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+ServiceWorkerInfo::GetNavigationFaultCount(uint32_t* aNavigationFaultCount) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aNavigationFaultCount);
+ *aNavigationFaultCount = mNavigationFaultCount;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+ServiceWorkerInfo::GetTestingInjectCancellation(
+ nsresult* aTestingInjectCancellation) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aTestingInjectCancellation);
+ *aTestingInjectCancellation = mTestingInjectCancellation;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+ServiceWorkerInfo::SetTestingInjectCancellation(
+ nsresult aTestingInjectCancellation) {
+ MOZ_ASSERT(NS_IsMainThread());
+ mTestingInjectCancellation = aTestingInjectCancellation;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+ServiceWorkerInfo::AttachDebugger() {
+ return mServiceWorkerPrivate->AttachDebugger();
+}
+
+NS_IMETHODIMP
+ServiceWorkerInfo::DetachDebugger() {
+ return mServiceWorkerPrivate->DetachDebugger();
+}
+
+void ServiceWorkerInfo::UpdateState(ServiceWorkerState aState) {
+ MOZ_ASSERT(NS_IsMainThread());
+#ifdef DEBUG
+ // Any state can directly transition to redundant, but everything else is
+ // ordered.
+ if (aState != ServiceWorkerState::Redundant) {
+ MOZ_ASSERT_IF(State() == ServiceWorkerState::EndGuard_,
+ aState == ServiceWorkerState::Installing);
+ MOZ_ASSERT_IF(State() == ServiceWorkerState::Installing,
+ aState == ServiceWorkerState::Installed);
+ MOZ_ASSERT_IF(State() == ServiceWorkerState::Installed,
+ aState == ServiceWorkerState::Activating);
+ MOZ_ASSERT_IF(State() == ServiceWorkerState::Activating,
+ aState == ServiceWorkerState::Activated);
+ }
+ // Activated can only go to redundant.
+ MOZ_ASSERT_IF(State() == ServiceWorkerState::Activated,
+ aState == ServiceWorkerState::Redundant);
+#endif
+ // Flush any pending functional events to the worker when it transitions to
+ // the activated state.
+ // TODO: Do we care that these events will race with the propagation of the
+ // state change?
+ if (State() != aState) {
+ mServiceWorkerPrivate->UpdateState(aState);
+ }
+ mDescriptor.SetState(aState);
+ if (State() == ServiceWorkerState::Redundant) {
+ serviceWorkerScriptCache::PurgeCache(mPrincipal, mCacheName);
+ mServiceWorkerPrivate->NoteDeadServiceWorkerInfo();
+ }
+}
+
+ServiceWorkerInfo::ServiceWorkerInfo(nsIPrincipal* aPrincipal,
+ const nsACString& aScope,
+ uint64_t aRegistrationId,
+ uint64_t aRegistrationVersion,
+ const nsACString& aScriptSpec,
+ const nsAString& aCacheName,
+ nsLoadFlags aImportsLoadFlags)
+ : mPrincipal(aPrincipal),
+ mDescriptor(GetNextID(), aRegistrationId, aRegistrationVersion,
+ aPrincipal, aScope, aScriptSpec, ServiceWorkerState::Parsed),
+ mCacheName(aCacheName),
+ mWorkerPrivateId(ComputeWorkerPrivateId()),
+ mImportsLoadFlags(aImportsLoadFlags),
+ mCreationTime(PR_Now()),
+ mCreationTimeStamp(TimeStamp::Now()),
+ mInstalledTime(0),
+ mActivatedTime(0),
+ mRedundantTime(0),
+ mServiceWorkerPrivate(new ServiceWorkerPrivate(this)),
+ mSkipWaitingFlag(false),
+ mHandlesFetch(Unknown),
+ mNavigationFaultCount(0),
+ mTestingInjectCancellation(NS_OK) {
+ MOZ_ASSERT(XRE_GetProcessType() == GeckoProcessType_Default);
+ MOZ_ASSERT(mPrincipal);
+ // cache origin attributes so we can use them off main thread
+ mOriginAttributes = mPrincipal->OriginAttributesRef();
+ MOZ_ASSERT(!mDescriptor.ScriptURL().IsEmpty());
+ MOZ_ASSERT(!mCacheName.IsEmpty());
+ MOZ_ASSERT(!mWorkerPrivateId.IsEmpty());
+
+ // Scripts of a service worker should always be loaded bypass service workers.
+ // Otherwise, we might not be able to update a service worker correctly, if
+ // there is a service worker generating the script.
+ MOZ_DIAGNOSTIC_ASSERT(mImportsLoadFlags &
+ nsIChannel::LOAD_BYPASS_SERVICE_WORKER);
+}
+
+ServiceWorkerInfo::~ServiceWorkerInfo() {
+ MOZ_ASSERT(mServiceWorkerPrivate);
+ mServiceWorkerPrivate->NoteDeadServiceWorkerInfo();
+}
+
+static uint64_t gServiceWorkerInfoCurrentID = 0;
+
+uint64_t ServiceWorkerInfo::GetNextID() const {
+ return ++gServiceWorkerInfoCurrentID;
+}
+
+void ServiceWorkerInfo::PostMessage(RefPtr<ServiceWorkerCloneData>&& aData,
+ const ClientInfo& aClientInfo,
+ const ClientState& aClientState) {
+ mServiceWorkerPrivate->SendMessageEvent(
+ std::move(aData),
+ ClientInfoAndState(aClientInfo.ToIPC(), aClientState.ToIPC()));
+}
+
+void ServiceWorkerInfo::UpdateInstalledTime() {
+ MOZ_ASSERT(State() == ServiceWorkerState::Installed);
+ MOZ_ASSERT(mInstalledTime == 0);
+
+ mInstalledTime =
+ mCreationTime +
+ static_cast<PRTime>(
+ (TimeStamp::Now() - mCreationTimeStamp).ToMicroseconds());
+}
+
+void ServiceWorkerInfo::UpdateActivatedTime() {
+ MOZ_ASSERT(State() == ServiceWorkerState::Activated);
+ MOZ_ASSERT(mActivatedTime == 0);
+
+ mActivatedTime =
+ mCreationTime +
+ static_cast<PRTime>(
+ (TimeStamp::Now() - mCreationTimeStamp).ToMicroseconds());
+}
+
+void ServiceWorkerInfo::UpdateRedundantTime() {
+ MOZ_ASSERT(State() == ServiceWorkerState::Redundant);
+ MOZ_ASSERT(mRedundantTime == 0);
+
+ mRedundantTime =
+ mCreationTime +
+ static_cast<PRTime>(
+ (TimeStamp::Now() - mCreationTimeStamp).ToMicroseconds());
+}
+
+void ServiceWorkerInfo::SetRegistrationVersion(uint64_t aVersion) {
+ mDescriptor.SetRegistrationVersion(aVersion);
+}
+
+} // namespace mozilla::dom
diff --git a/dom/serviceworkers/ServiceWorkerInfo.h b/dom/serviceworkers/ServiceWorkerInfo.h
new file mode 100644
index 0000000000..03a9eb6aff
--- /dev/null
+++ b/dom/serviceworkers/ServiceWorkerInfo.h
@@ -0,0 +1,183 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_serviceworkerinfo_h
+#define mozilla_dom_serviceworkerinfo_h
+
+#include "MainThreadUtils.h"
+#include "mozilla/dom/ServiceWorkerBinding.h" // For ServiceWorkerState
+#include "mozilla/dom/ServiceWorkerDescriptor.h"
+#include "mozilla/dom/WorkerCommon.h"
+#include "mozilla/OriginAttributes.h"
+#include "mozilla/TimeStamp.h"
+#include "nsIServiceWorkerManager.h"
+
+namespace mozilla::dom {
+
+class ClientInfoAndState;
+class ClientState;
+class ServiceWorkerCloneData;
+class ServiceWorkerPrivate;
+
+/*
+ * Wherever the spec treats a worker instance and a description of said worker
+ * as the same thing; i.e. "Resolve foo with
+ * _GetNewestWorker(serviceWorkerRegistration)", we represent the description
+ * by this class and spawn a ServiceWorker in the right global when required.
+ */
+class ServiceWorkerInfo final : public nsIServiceWorkerInfo {
+ private:
+ nsCOMPtr<nsIPrincipal> mPrincipal;
+ ServiceWorkerDescriptor mDescriptor;
+ const nsString mCacheName;
+ OriginAttributes mOriginAttributes;
+ const nsString mWorkerPrivateId;
+
+ // This LoadFlags is only applied to imported scripts, since the main script
+ // has already been downloaded when performing the bytecheck. This LoadFlag is
+ // composed of three parts:
+ // 1. nsIChannel::LOAD_BYPASS_SERVICE_WORKER
+ // 2. (Optional) nsIRequest::VALIDATE_ALWAYS
+ // depends on ServiceWorkerUpdateViaCache of its registration.
+ // 3. (optional) nsIRequest::LOAD_BYPASS_CACHE
+ // depends on whether the update timer is expired.
+ const nsLoadFlags mImportsLoadFlags;
+
+ // Timestamp to track SW's state
+ PRTime mCreationTime;
+ TimeStamp mCreationTimeStamp;
+
+ // The time of states are 0, if SW has not reached that state yet. Besides, we
+ // update each of them after UpdateState() is called in SWRegistrationInfo.
+ PRTime mInstalledTime;
+ PRTime mActivatedTime;
+ PRTime mRedundantTime;
+
+ RefPtr<ServiceWorkerPrivate> mServiceWorkerPrivate;
+ bool mSkipWaitingFlag;
+
+ enum { Unknown, Enabled, Disabled } mHandlesFetch;
+
+ uint32_t mNavigationFaultCount;
+
+ // Testing helper to trigger fetch event cancellation when not NS_OK.
+ // See `nsIServiceWorkerInfo::testingInjectCancellation`.
+ nsresult mTestingInjectCancellation;
+
+ ~ServiceWorkerInfo();
+
+ // Generates a unique id for the service worker, with zero being treated as
+ // invalid.
+ uint64_t GetNextID() const;
+
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSISERVICEWORKERINFO
+
+ void PostMessage(RefPtr<ServiceWorkerCloneData>&& aData,
+ const ClientInfo& aClientInfo,
+ const ClientState& aClientState);
+
+ class ServiceWorkerPrivate* WorkerPrivate() const {
+ MOZ_ASSERT(mServiceWorkerPrivate);
+ return mServiceWorkerPrivate;
+ }
+
+ nsIPrincipal* Principal() const { return mPrincipal; }
+
+ const nsCString& ScriptSpec() const { return mDescriptor.ScriptURL(); }
+
+ const nsCString& Scope() const { return mDescriptor.Scope(); }
+
+ bool SkipWaitingFlag() const {
+ MOZ_ASSERT(NS_IsMainThread());
+ return mSkipWaitingFlag;
+ }
+
+ void SetSkipWaitingFlag() {
+ MOZ_ASSERT(NS_IsMainThread());
+ mSkipWaitingFlag = true;
+ }
+
+ void ReportNavigationFault() {
+ MOZ_ASSERT(NS_IsMainThread());
+ mNavigationFaultCount++;
+ }
+
+ ServiceWorkerInfo(nsIPrincipal* aPrincipal, const nsACString& aScope,
+ uint64_t aRegistrationId, uint64_t aRegistrationVersion,
+ const nsACString& aScriptSpec, const nsAString& aCacheName,
+ nsLoadFlags aLoadFlags);
+
+ ServiceWorkerState State() const { return mDescriptor.State(); }
+
+ const OriginAttributes& GetOriginAttributes() const {
+ return mOriginAttributes;
+ }
+
+ const nsString& CacheName() const { return mCacheName; }
+
+ nsLoadFlags GetImportsLoadFlags() const { return mImportsLoadFlags; }
+
+ uint64_t ID() const { return mDescriptor.Id(); }
+
+ const ServiceWorkerDescriptor& Descriptor() const { return mDescriptor; }
+
+ nsresult TestingInjectCancellation() { return mTestingInjectCancellation; }
+
+ void UpdateState(ServiceWorkerState aState);
+
+ // Only used to set initial state when loading from disk!
+ void SetActivateStateUncheckedWithoutEvent(ServiceWorkerState aState) {
+ MOZ_ASSERT(NS_IsMainThread());
+ mDescriptor.SetState(aState);
+ }
+
+ void SetHandlesFetch(bool aHandlesFetch) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_DIAGNOSTIC_ASSERT(mHandlesFetch == Unknown);
+ mHandlesFetch = aHandlesFetch ? Enabled : Disabled;
+ mDescriptor.SetHandlesFetch(aHandlesFetch);
+ }
+
+ void SetRegistrationVersion(uint64_t aVersion);
+
+ bool HandlesFetch() const {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_DIAGNOSTIC_ASSERT(mHandlesFetch != Unknown);
+ return mHandlesFetch != Disabled;
+ }
+
+ void UpdateInstalledTime();
+
+ void UpdateActivatedTime();
+
+ void UpdateRedundantTime();
+
+ int64_t GetInstalledTime() const { return mInstalledTime; }
+
+ void SetInstalledTime(const int64_t aTime) {
+ if (aTime == 0) {
+ return;
+ }
+
+ mInstalledTime = aTime;
+ }
+
+ int64_t GetActivatedTime() const { return mActivatedTime; }
+
+ void SetActivatedTime(const int64_t aTime) {
+ if (aTime == 0) {
+ return;
+ }
+
+ mActivatedTime = aTime;
+ }
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_serviceworkerinfo_h
diff --git a/dom/serviceworkers/ServiceWorkerInterceptController.cpp b/dom/serviceworkers/ServiceWorkerInterceptController.cpp
new file mode 100644
index 0000000000..87fdf82af7
--- /dev/null
+++ b/dom/serviceworkers/ServiceWorkerInterceptController.cpp
@@ -0,0 +1,173 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "ServiceWorkerInterceptController.h"
+
+#include "mozilla/BasePrincipal.h"
+#include "mozilla/StaticPrefs_dom.h"
+#include "mozilla/StaticPrefs_privacy.h"
+#include "mozilla/StorageAccess.h"
+#include "mozilla/StoragePrincipalHelper.h"
+#include "mozilla/dom/CanonicalBrowsingContext.h"
+#include "mozilla/dom/InternalRequest.h"
+#include "mozilla/net/HttpBaseChannel.h"
+#include "nsCOMPtr.h"
+#include "nsContentUtils.h"
+#include "nsIChannel.h"
+#include "nsICookieJarSettings.h"
+#include "ServiceWorkerManager.h"
+#include "nsIPrincipal.h"
+#include "nsQueryObject.h"
+
+namespace mozilla::dom {
+
+namespace {
+bool IsWithinObjectOrEmbed(const nsCOMPtr<nsILoadInfo>& loadInfo) {
+ RefPtr<BrowsingContext> browsingContext;
+ loadInfo->GetTargetBrowsingContext(getter_AddRefs(browsingContext));
+
+ for (BrowsingContext* cur = browsingContext.get(); cur;
+ cur = cur->GetParent()) {
+ if (cur->IsEmbedderTypeObjectOrEmbed()) {
+ return true;
+ }
+ }
+
+ return false;
+}
+} // namespace
+
+NS_IMPL_ISUPPORTS(ServiceWorkerInterceptController,
+ nsINetworkInterceptController)
+
+NS_IMETHODIMP
+ServiceWorkerInterceptController::ShouldPrepareForIntercept(
+ nsIURI* aURI, nsIChannel* aChannel, bool* aShouldIntercept) {
+ *aShouldIntercept = false;
+
+ nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo();
+
+ // Block interception if the request's destination is within an object or
+ // embed element.
+ if (IsWithinObjectOrEmbed(loadInfo)) {
+ return NS_OK;
+ }
+
+ RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance();
+
+ // For subresource requests we base our decision solely on the client's
+ // controller value. Any settings that would have blocked service worker
+ // access should have been set before the initial navigation created the
+ // window.
+ if (!nsContentUtils::IsNonSubresourceRequest(aChannel)) {
+ const Maybe<ServiceWorkerDescriptor>& controller =
+ loadInfo->GetController();
+
+ // If the controller doesn't handle fetch events, return false
+ if (!controller.isSome()) {
+ return NS_OK;
+ }
+
+ *aShouldIntercept = controller.ref().HandlesFetch();
+
+ // The service worker has no fetch event handler, try to schedule a
+ // soft-update through ServiceWorkerRegistrationInfo.
+ // Get ServiceWorkerRegistrationInfo by the ServiceWorkerInfo's principal
+ // and scope
+ if (!*aShouldIntercept && swm) {
+ nsCOMPtr<nsIPrincipal> principal =
+ controller.ref().GetPrincipal().unwrap();
+ RefPtr<ServiceWorkerRegistrationInfo> registration =
+ swm->GetRegistration(principal, controller.ref().Scope());
+ // Could not get ServiceWorkerRegistration here if unregister is
+ // executed before getting here.
+ if (NS_WARN_IF(!registration)) {
+ return NS_OK;
+ }
+ registration->MaybeScheduleTimeCheckAndUpdate();
+ }
+
+ RefPtr<net::HttpBaseChannel> httpChannel = do_QueryObject(aChannel);
+
+ if (httpChannel &&
+ httpChannel->GetRequestHead()->HasHeader(net::nsHttp::Range)) {
+ RequestMode requestMode =
+ InternalRequest::MapChannelToRequestMode(aChannel);
+ bool mayLoad = nsContentUtils::CheckMayLoad(
+ loadInfo->GetLoadingPrincipal(), aChannel,
+ /*allowIfInheritsPrincipal*/ false);
+ if (requestMode == RequestMode::No_cors && !mayLoad) {
+ *aShouldIntercept = false;
+ }
+ }
+
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIPrincipal> principal;
+ nsresult rv = StoragePrincipalHelper::GetPrincipal(
+ aChannel,
+ StaticPrefs::privacy_partition_serviceWorkers()
+ ? StoragePrincipalHelper::eForeignPartitionedPrincipal
+ : StoragePrincipalHelper::eRegularPrincipal,
+ getter_AddRefs(principal));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // First check with the ServiceWorkerManager for a matching service worker.
+ if (!swm || !swm->IsAvailable(principal, aURI, aChannel)) {
+ return NS_OK;
+ }
+
+ // Check if we're in a secure context, unless service worker testing is
+ // enabled.
+ if (!nsContentUtils::ComputeIsSecureContext(aChannel) &&
+ !StaticPrefs::dom_serviceWorkers_testing_enabled()) {
+ return NS_OK;
+ }
+
+ // Then check to see if we are allowed to control the window.
+ // It is important to check for the availability of the service worker first
+ // to avoid showing warnings about the use of third-party cookies in the UI
+ // unnecessarily when no service worker is being accessed.
+ auto storageAccess = StorageAllowedForChannel(aChannel);
+ if (storageAccess != StorageAccess::eAllow) {
+ if (!StaticPrefs::privacy_partition_serviceWorkers()) {
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsICookieJarSettings> cookieJarSettings;
+ loadInfo->GetCookieJarSettings(getter_AddRefs(cookieJarSettings));
+
+ if (!StoragePartitioningEnabled(storageAccess, cookieJarSettings)) {
+ return NS_OK;
+ }
+ }
+
+ *aShouldIntercept = true;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+ServiceWorkerInterceptController::ChannelIntercepted(
+ nsIInterceptedChannel* aChannel) {
+ // Note, do not cancel the interception here. The caller will try to
+ // ResetInterception() on error.
+
+ RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance();
+ if (!swm) {
+ return NS_ERROR_FAILURE;
+ }
+
+ ErrorResult error;
+ swm->DispatchFetchEvent(aChannel, error);
+ if (NS_WARN_IF(error.Failed())) {
+ return error.StealNSResult();
+ }
+
+ return NS_OK;
+}
+
+} // namespace mozilla::dom
diff --git a/dom/serviceworkers/ServiceWorkerInterceptController.h b/dom/serviceworkers/ServiceWorkerInterceptController.h
new file mode 100644
index 0000000000..32113291ef
--- /dev/null
+++ b/dom/serviceworkers/ServiceWorkerInterceptController.h
@@ -0,0 +1,25 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_serviceworkerinterceptcontroller_h
+#define mozilla_dom_serviceworkerinterceptcontroller_h
+
+#include "nsINetworkInterceptController.h"
+
+namespace mozilla::dom {
+
+class ServiceWorkerInterceptController final
+ : public nsINetworkInterceptController {
+ ~ServiceWorkerInterceptController() = default;
+
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSINETWORKINTERCEPTCONTROLLER
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_serviceworkerinterceptcontroller_h
diff --git a/dom/serviceworkers/ServiceWorkerJob.cpp b/dom/serviceworkers/ServiceWorkerJob.cpp
new file mode 100644
index 0000000000..980d48b66c
--- /dev/null
+++ b/dom/serviceworkers/ServiceWorkerJob.cpp
@@ -0,0 +1,220 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "ServiceWorkerJob.h"
+
+#include "mozilla/dom/WorkerCommon.h"
+#include "nsIPrincipal.h"
+#include "nsProxyRelease.h"
+#include "nsThreadUtils.h"
+#include "ServiceWorkerManager.h"
+
+namespace mozilla::dom {
+
+ServiceWorkerJob::Type ServiceWorkerJob::GetType() const { return mType; }
+
+ServiceWorkerJob::State ServiceWorkerJob::GetState() const { return mState; }
+
+bool ServiceWorkerJob::Canceled() const { return mCanceled; }
+
+bool ServiceWorkerJob::ResultCallbacksInvoked() const {
+ return mResultCallbacksInvoked;
+}
+
+bool ServiceWorkerJob::IsEquivalentTo(ServiceWorkerJob* aJob) const {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aJob);
+ return mType == aJob->mType && mScope.Equals(aJob->mScope) &&
+ mScriptSpec.Equals(aJob->mScriptSpec) &&
+ mPrincipal->Equals(aJob->mPrincipal);
+}
+
+void ServiceWorkerJob::AppendResultCallback(Callback* aCallback) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_DIAGNOSTIC_ASSERT(mState != State::Finished);
+ MOZ_DIAGNOSTIC_ASSERT(aCallback);
+ MOZ_DIAGNOSTIC_ASSERT(mFinalCallback != aCallback);
+ MOZ_ASSERT(!mResultCallbackList.Contains(aCallback));
+ MOZ_DIAGNOSTIC_ASSERT(!mResultCallbacksInvoked);
+ mResultCallbackList.AppendElement(aCallback);
+}
+
+void ServiceWorkerJob::StealResultCallbacksFrom(ServiceWorkerJob* aJob) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aJob);
+ MOZ_ASSERT(aJob->mState == State::Initial);
+
+ // Take the callbacks from the other job immediately to avoid the
+ // any possibility of them existing on both jobs at once.
+ nsTArray<RefPtr<Callback>> callbackList =
+ std::move(aJob->mResultCallbackList);
+
+ for (RefPtr<Callback>& callback : callbackList) {
+ // Use AppendResultCallback() so that assertion checking is performed on
+ // each callback.
+ AppendResultCallback(callback);
+ }
+}
+
+void ServiceWorkerJob::Start(Callback* aFinalCallback) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_DIAGNOSTIC_ASSERT(!mCanceled);
+
+ MOZ_DIAGNOSTIC_ASSERT(aFinalCallback);
+ MOZ_DIAGNOSTIC_ASSERT(!mFinalCallback);
+ MOZ_ASSERT(!mResultCallbackList.Contains(aFinalCallback));
+ mFinalCallback = aFinalCallback;
+
+ MOZ_DIAGNOSTIC_ASSERT(mState == State::Initial);
+ mState = State::Started;
+
+ nsCOMPtr<nsIRunnable> runnable = NewRunnableMethod(
+ "ServiceWorkerJob::AsyncExecute", this, &ServiceWorkerJob::AsyncExecute);
+
+ // We may have to wait for the PBackground actor to be initialized
+ // before proceeding. We should always be able to get a ServiceWorkerManager,
+ // however, since Start() should not be called during shutdown.
+ RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance();
+ if (!swm) {
+ // browser shutdown
+ return;
+ }
+
+ // Otherwise start asynchronously. We should never run a job synchronously.
+ MOZ_ALWAYS_TRUE(NS_SUCCEEDED(NS_DispatchToMainThread(runnable.forget())));
+}
+
+void ServiceWorkerJob::Cancel() {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(!mCanceled);
+ mCanceled = true;
+
+ if (GetState() != State::Started) {
+ MOZ_ASSERT(GetState() == State::Initial);
+
+ ErrorResult error(NS_ERROR_DOM_ABORT_ERR);
+ InvokeResultCallbacks(error);
+
+ // The callbacks might not consume the error, which is fine.
+ error.SuppressException();
+ }
+}
+
+ServiceWorkerJob::ServiceWorkerJob(Type aType, nsIPrincipal* aPrincipal,
+ const nsACString& aScope,
+ nsCString aScriptSpec)
+ : mType(aType),
+ mPrincipal(aPrincipal),
+ mScope(aScope),
+ mScriptSpec(std::move(aScriptSpec)),
+ mState(State::Initial),
+ mCanceled(false),
+ mResultCallbacksInvoked(false) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(mPrincipal);
+ MOZ_ASSERT(!mScope.IsEmpty());
+
+ // Empty script URL if and only if this is an unregister job.
+ MOZ_ASSERT((mType == Type::Unregister) == mScriptSpec.IsEmpty());
+}
+
+ServiceWorkerJob::~ServiceWorkerJob() {
+ MOZ_ASSERT(NS_IsMainThread());
+ // Jobs must finish or never be started. Destroying an actively running
+ // job is an error.
+ MOZ_ASSERT(mState != State::Started);
+ MOZ_ASSERT_IF(mState == State::Finished, mResultCallbacksInvoked);
+}
+
+void ServiceWorkerJob::InvokeResultCallbacks(ErrorResult& aRv) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_DIAGNOSTIC_ASSERT(mState != State::Finished);
+ MOZ_DIAGNOSTIC_ASSERT_IF(mState == State::Initial, Canceled());
+
+ MOZ_DIAGNOSTIC_ASSERT(!mResultCallbacksInvoked);
+ mResultCallbacksInvoked = true;
+
+ nsTArray<RefPtr<Callback>> callbackList = std::move(mResultCallbackList);
+
+ for (RefPtr<Callback>& callback : callbackList) {
+ // The callback might consume an exception on the ErrorResult, so we need
+ // to clone in order to maintain the error for the next callback.
+ ErrorResult rv;
+ aRv.CloneTo(rv);
+
+ if (GetState() == State::Started) {
+ callback->JobFinished(this, rv);
+ } else {
+ callback->JobDiscarded(rv);
+ }
+
+ // The callback might not consume the error.
+ rv.SuppressException();
+ }
+}
+
+void ServiceWorkerJob::InvokeResultCallbacks(nsresult aRv) {
+ ErrorResult converted(aRv);
+ InvokeResultCallbacks(converted);
+}
+
+void ServiceWorkerJob::Finish(ErrorResult& aRv) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ // Avoid double-completion because it can result on operating on cleaned
+ // up data. This should not happen, though, so also assert to try to
+ // narrow down the causes.
+ MOZ_DIAGNOSTIC_ASSERT(mState == State::Started);
+ if (mState != State::Started) {
+ return;
+ }
+
+ // Ensure that we only surface SecurityErr, TypeErr or InvalidStateErr to
+ // script.
+ if (aRv.Failed() && !aRv.ErrorCodeIs(NS_ERROR_DOM_SECURITY_ERR) &&
+ !aRv.ErrorCodeIs(NS_ERROR_INTERNAL_ERRORRESULT_TYPEERROR) &&
+ !aRv.ErrorCodeIs(NS_ERROR_DOM_INVALID_STATE_ERR)) {
+ // Remove the old error code so we can replace it with a TypeError.
+ aRv.SuppressException();
+
+ // Throw the type error with a generic error message. We use a stack
+ // reference to bypass the normal static analysis for "return right after
+ // throwing", since it's not the right check here: this ErrorResult came in
+ // pre-thrown.
+ ErrorResult& rv = aRv;
+ rv.ThrowTypeError<MSG_SW_INSTALL_ERROR>(mScriptSpec, mScope);
+ }
+
+ // The final callback may drop the last ref to this object.
+ RefPtr<ServiceWorkerJob> kungFuDeathGrip = this;
+
+ if (!mResultCallbacksInvoked) {
+ InvokeResultCallbacks(aRv);
+ }
+
+ mState = State::Finished;
+
+ MOZ_DIAGNOSTIC_ASSERT(mFinalCallback);
+ if (mFinalCallback) {
+ mFinalCallback->JobFinished(this, aRv);
+ mFinalCallback = nullptr;
+ }
+
+ // The callback might not consume the error.
+ aRv.SuppressException();
+
+ // Async release this object to ensure that our caller methods complete
+ // as well.
+ NS_ReleaseOnMainThread("ServiceWorkerJobProxyRunnable",
+ kungFuDeathGrip.forget(), true /* always proxy */);
+}
+
+void ServiceWorkerJob::Finish(nsresult aRv) {
+ ErrorResult converted(aRv);
+ Finish(converted);
+}
+
+} // namespace mozilla::dom
diff --git a/dom/serviceworkers/ServiceWorkerJob.h b/dom/serviceworkers/ServiceWorkerJob.h
new file mode 100644
index 0000000000..70eed04636
--- /dev/null
+++ b/dom/serviceworkers/ServiceWorkerJob.h
@@ -0,0 +1,126 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_serviceworkerjob_h
+#define mozilla_dom_serviceworkerjob_h
+
+#include "nsCOMPtr.h"
+#include "nsString.h"
+#include "nsTArray.h"
+
+class nsIPrincipal;
+
+namespace mozilla {
+
+class ErrorResult;
+
+namespace dom {
+
+class ServiceWorkerJob {
+ public:
+ // Implement this interface to receive notification when a job completes or
+ // is discarded.
+ class Callback {
+ public:
+ // Called once when the job completes. If the job is started, then this
+ // will be called. If a job is never executed due to browser shutdown,
+ // then this method will never be called. This method is always called
+ // on the main thread asynchronously after Start() completes.
+ virtual void JobFinished(ServiceWorkerJob* aJob, ErrorResult& aStatus) = 0;
+
+ // If the job has not started and will never start, then this will be
+ // called; either JobFinished or JobDiscarded will be called, but not both.
+ virtual void JobDiscarded(ErrorResult& aStatus) = 0;
+
+ NS_INLINE_DECL_PURE_VIRTUAL_REFCOUNTING
+ };
+
+ enum class Type { Register, Update, Unregister };
+
+ enum class State { Initial, Started, Finished };
+
+ Type GetType() const;
+
+ State GetState() const;
+
+ // Determine if the job has been canceled. This does not change the
+ // current State, but indicates that the job should progress to Finished
+ // as soon as possible.
+ bool Canceled() const;
+
+ // Determine if the result callbacks have already been called. This is
+ // equivalent to the spec checked to see if the job promise has settled.
+ bool ResultCallbacksInvoked() const;
+
+ bool IsEquivalentTo(ServiceWorkerJob* aJob) const;
+
+ // Add a callback that will be invoked when the job's result is available.
+ // Some job types will invoke this before the job is actually finished.
+ // If an early callback does not occur, then it will be called automatically
+ // when Finish() is called. These callbacks will be invoked while the job
+ // state is Started.
+ void AppendResultCallback(Callback* aCallback);
+
+ // This takes ownership of any result callbacks associated with the given job
+ // and then appends them to this job's callback list.
+ void StealResultCallbacksFrom(ServiceWorkerJob* aJob);
+
+ // Start the job. All work will be performed asynchronously on
+ // the main thread. The Finish() method must be called exactly
+ // once after this point. A final callback must be provided. It
+ // will be invoked after all other callbacks have been processed.
+ void Start(Callback* aFinalCallback);
+
+ // Set an internal flag indicating that a started job should finish as
+ // soon as possible.
+ void Cancel();
+
+ protected:
+ ServiceWorkerJob(Type aType, nsIPrincipal* aPrincipal,
+ const nsACString& aScope, nsCString aScriptSpec);
+
+ virtual ~ServiceWorkerJob();
+
+ // Invoke the result callbacks immediately. The job must be in the
+ // Started state or be canceled and in the Initial state. The callbacks are
+ // cleared after being invoked, so subsequent method calls have no effect.
+ void InvokeResultCallbacks(ErrorResult& aRv);
+
+ // Convenience method that converts to ErrorResult and calls real method.
+ void InvokeResultCallbacks(nsresult aRv);
+
+ // Indicate that the job has completed. The must be called exactly
+ // once after Start() has initiated job execution. It may not be
+ // called until Start() has returned.
+ void Finish(ErrorResult& aRv);
+
+ // Convenience method that converts to ErrorResult and calls real method.
+ void Finish(nsresult aRv);
+
+ // Specific job types should define AsyncExecute to begin their work.
+ // All errors and successes must result in Finish() being called.
+ virtual void AsyncExecute() = 0;
+
+ const Type mType;
+ nsCOMPtr<nsIPrincipal> mPrincipal;
+ const nsCString mScope;
+ const nsCString mScriptSpec;
+
+ private:
+ RefPtr<Callback> mFinalCallback;
+ nsTArray<RefPtr<Callback>> mResultCallbackList;
+ State mState;
+ bool mCanceled;
+ bool mResultCallbacksInvoked;
+
+ public:
+ NS_INLINE_DECL_REFCOUNTING(ServiceWorkerJob)
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_serviceworkerjob_h
diff --git a/dom/serviceworkers/ServiceWorkerJobQueue.cpp b/dom/serviceworkers/ServiceWorkerJobQueue.cpp
new file mode 100644
index 0000000000..497265249a
--- /dev/null
+++ b/dom/serviceworkers/ServiceWorkerJobQueue.cpp
@@ -0,0 +1,120 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "ServiceWorkerJobQueue.h"
+
+#include "nsThreadUtils.h"
+#include "ServiceWorkerJob.h"
+#include "mozilla/dom/WorkerCommon.h"
+
+namespace mozilla::dom {
+
+class ServiceWorkerJobQueue::Callback final
+ : public ServiceWorkerJob::Callback {
+ RefPtr<ServiceWorkerJobQueue> mQueue;
+
+ ~Callback() = default;
+
+ public:
+ explicit Callback(ServiceWorkerJobQueue* aQueue) : mQueue(aQueue) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(mQueue);
+ }
+
+ virtual void JobFinished(ServiceWorkerJob* aJob,
+ ErrorResult& aStatus) override {
+ MOZ_ASSERT(NS_IsMainThread());
+ mQueue->JobFinished(aJob);
+ }
+
+ virtual void JobDiscarded(ErrorResult&) override {
+ // no-op; nothing to do.
+ }
+
+ NS_INLINE_DECL_REFCOUNTING(ServiceWorkerJobQueue::Callback, override)
+};
+
+ServiceWorkerJobQueue::~ServiceWorkerJobQueue() {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(mJobList.IsEmpty());
+}
+
+void ServiceWorkerJobQueue::JobFinished(ServiceWorkerJob* aJob) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aJob);
+
+ // XXX There are some corner cases where jobs can double-complete. Until
+ // we track all these down we do a non-fatal assert in debug builds and
+ // a runtime check to verify the queue is in the correct state.
+ NS_ASSERTION(!mJobList.IsEmpty(),
+ "Job queue should contain the job that just completed.");
+ NS_ASSERTION(mJobList.SafeElementAt(0, nullptr) == aJob,
+ "Job queue should contain the job that just completed.");
+ if (NS_WARN_IF(mJobList.SafeElementAt(0, nullptr) != aJob)) {
+ return;
+ }
+
+ mJobList.RemoveElementAt(0);
+
+ if (mJobList.IsEmpty()) {
+ return;
+ }
+
+ RunJob();
+}
+
+void ServiceWorkerJobQueue::RunJob() {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(!mJobList.IsEmpty());
+ MOZ_ASSERT(mJobList[0]->GetState() == ServiceWorkerJob::State::Initial);
+
+ RefPtr<Callback> callback = new Callback(this);
+ mJobList[0]->Start(callback);
+}
+
+ServiceWorkerJobQueue::ServiceWorkerJobQueue() {
+ MOZ_ASSERT(NS_IsMainThread());
+}
+
+void ServiceWorkerJobQueue::ScheduleJob(ServiceWorkerJob* aJob) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aJob);
+ MOZ_ASSERT(!mJobList.Contains(aJob));
+
+ if (mJobList.IsEmpty()) {
+ mJobList.AppendElement(aJob);
+ RunJob();
+ return;
+ }
+
+ MOZ_ASSERT(mJobList[0]->GetState() == ServiceWorkerJob::State::Started);
+
+ RefPtr<ServiceWorkerJob>& tailJob = mJobList[mJobList.Length() - 1];
+ if (!tailJob->ResultCallbacksInvoked() && aJob->IsEquivalentTo(tailJob)) {
+ tailJob->StealResultCallbacksFrom(aJob);
+ return;
+ }
+
+ mJobList.AppendElement(aJob);
+}
+
+void ServiceWorkerJobQueue::CancelAll() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ for (RefPtr<ServiceWorkerJob>& job : mJobList) {
+ job->Cancel();
+ }
+
+ // Remove jobs that are queued but not started since they should never
+ // run after being canceled. This means throwing away all jobs except
+ // for the job at the front of the list.
+ if (!mJobList.IsEmpty()) {
+ MOZ_ASSERT(mJobList[0]->GetState() == ServiceWorkerJob::State::Started);
+ mJobList.TruncateLength(1);
+ }
+}
+
+} // namespace mozilla::dom
diff --git a/dom/serviceworkers/ServiceWorkerJobQueue.h b/dom/serviceworkers/ServiceWorkerJobQueue.h
new file mode 100644
index 0000000000..9e51aefe4f
--- /dev/null
+++ b/dom/serviceworkers/ServiceWorkerJobQueue.h
@@ -0,0 +1,40 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_serviceworkerjobqueue_h
+#define mozilla_dom_serviceworkerjobqueue_h
+
+#include "mozilla/RefPtr.h"
+#include "nsTArray.h"
+
+namespace mozilla::dom {
+
+class ServiceWorkerJob;
+
+class ServiceWorkerJobQueue final {
+ class Callback;
+
+ nsTArray<RefPtr<ServiceWorkerJob>> mJobList;
+
+ ~ServiceWorkerJobQueue();
+
+ void JobFinished(ServiceWorkerJob* aJob);
+
+ void RunJob();
+
+ public:
+ ServiceWorkerJobQueue();
+
+ void ScheduleJob(ServiceWorkerJob* aJob);
+
+ void CancelAll();
+
+ NS_INLINE_DECL_REFCOUNTING(ServiceWorkerJobQueue)
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_serviceworkerjobqueue_h
diff --git a/dom/serviceworkers/ServiceWorkerManager.cpp b/dom/serviceworkers/ServiceWorkerManager.cpp
new file mode 100644
index 0000000000..6b1bb43398
--- /dev/null
+++ b/dom/serviceworkers/ServiceWorkerManager.cpp
@@ -0,0 +1,3364 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "ServiceWorkerManager.h"
+
+#include <algorithm>
+
+#include "nsCOMPtr.h"
+#include "nsICookieJarSettings.h"
+#include "nsIHttpChannel.h"
+#include "nsIHttpChannelInternal.h"
+#include "nsINamed.h"
+#include "nsINetworkInterceptController.h"
+#include "nsIMutableArray.h"
+#include "nsIPrincipal.h"
+#include "nsITimer.h"
+#include "nsIUploadChannel2.h"
+#include "nsServiceManagerUtils.h"
+#include "nsDebug.h"
+#include "nsIPermissionManager.h"
+#include "nsXULAppAPI.h"
+
+#include "jsapi.h"
+
+#include "mozilla/AppShutdown.h"
+#include "mozilla/BasePrincipal.h"
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/ErrorNames.h"
+#include "mozilla/LoadContext.h"
+#include "mozilla/MozPromise.h"
+#include "mozilla/Result.h"
+#include "mozilla/ResultExtensions.h"
+#include "mozilla/Telemetry.h"
+#include "mozilla/dom/BindingUtils.h"
+#include "mozilla/dom/ClientHandle.h"
+#include "mozilla/dom/ClientManager.h"
+#include "mozilla/dom/ClientSource.h"
+#include "mozilla/dom/ConsoleUtils.h"
+#include "mozilla/dom/ContentParent.h"
+#include "mozilla/dom/ErrorEvent.h"
+#include "mozilla/dom/Headers.h"
+#include "mozilla/dom/InternalHeaders.h"
+#include "mozilla/dom/Navigator.h"
+#include "mozilla/dom/NotificationEvent.h"
+#include "mozilla/dom/Promise.h"
+#include "mozilla/dom/PromiseNativeHandler.h"
+#include "mozilla/dom/Request.h"
+#include "mozilla/dom/RootedDictionary.h"
+#include "mozilla/dom/TypedArray.h"
+#include "mozilla/dom/SharedWorker.h"
+#include "mozilla/dom/WorkerPrivate.h"
+#include "mozilla/dom/WorkerRunnable.h"
+#include "mozilla/dom/WorkerScope.h"
+#include "mozilla/extensions/WebExtensionPolicy.h"
+#include "mozilla/ipc/BackgroundChild.h"
+#include "mozilla/ipc/PBackgroundChild.h"
+#include "mozilla/ipc/PBackgroundSharedTypes.h"
+#include "mozilla/dom/ScriptLoader.h"
+#include "mozilla/PermissionManager.h"
+#include "mozilla/ScopeExit.h"
+#include "mozilla/StaticPrefs_extensions.h"
+#include "mozilla/StaticPrefs_privacy.h"
+#include "mozilla/StoragePrincipalHelper.h"
+#include "mozilla/Unused.h"
+#include "mozilla/EnumSet.h"
+
+#include "nsComponentManagerUtils.h"
+#include "nsContentUtils.h"
+#include "nsIDUtils.h"
+#include "nsNetUtil.h"
+#include "nsProxyRelease.h"
+#include "nsQueryObject.h"
+#include "nsTArray.h"
+
+#include "ServiceWorker.h"
+#include "ServiceWorkerContainer.h"
+#include "ServiceWorkerInfo.h"
+#include "ServiceWorkerJobQueue.h"
+#include "ServiceWorkerManagerChild.h"
+#include "ServiceWorkerPrivate.h"
+#include "ServiceWorkerRegisterJob.h"
+#include "ServiceWorkerRegistrar.h"
+#include "ServiceWorkerRegistration.h"
+#include "ServiceWorkerScriptCache.h"
+#include "ServiceWorkerShutdownBlocker.h"
+#include "ServiceWorkerEvents.h"
+#include "ServiceWorkerUnregisterJob.h"
+#include "ServiceWorkerUpdateJob.h"
+#include "ServiceWorkerUtils.h"
+#include "ServiceWorkerQuotaUtils.h"
+
+#ifdef PostMessage
+# undef PostMessage
+#endif
+
+mozilla::LazyLogModule sWorkerTelemetryLog("WorkerTelemetry");
+
+#ifdef LOG
+# undef LOG
+#endif
+#define LOG(_args) MOZ_LOG(sWorkerTelemetryLog, LogLevel::Debug, _args);
+
+using namespace mozilla;
+using namespace mozilla::dom;
+using namespace mozilla::ipc;
+
+namespace mozilla::dom {
+
+// Counts the number of registered ServiceWorkers, and the number that
+// handle Fetch, for reporting in Telemetry
+uint32_t gServiceWorkersRegistered = 0;
+uint32_t gServiceWorkersRegisteredFetch = 0;
+
+static_assert(
+ nsIHttpChannelInternal::REDIRECT_MODE_FOLLOW ==
+ static_cast<uint32_t>(RequestRedirect::Follow),
+ "RequestRedirect enumeration value should make Necko Redirect mode value.");
+static_assert(
+ nsIHttpChannelInternal::REDIRECT_MODE_ERROR ==
+ static_cast<uint32_t>(RequestRedirect::Error),
+ "RequestRedirect enumeration value should make Necko Redirect mode value.");
+static_assert(
+ nsIHttpChannelInternal::REDIRECT_MODE_MANUAL ==
+ static_cast<uint32_t>(RequestRedirect::Manual),
+ "RequestRedirect enumeration value should make Necko Redirect mode value.");
+static_assert(
+ 3 == RequestRedirectValues::Count,
+ "RequestRedirect enumeration value should make Necko Redirect mode value.");
+
+static_assert(
+ nsIHttpChannelInternal::FETCH_CACHE_MODE_DEFAULT ==
+ static_cast<uint32_t>(RequestCache::Default),
+ "RequestCache enumeration value should match Necko Cache mode value.");
+static_assert(
+ nsIHttpChannelInternal::FETCH_CACHE_MODE_NO_STORE ==
+ static_cast<uint32_t>(RequestCache::No_store),
+ "RequestCache enumeration value should match Necko Cache mode value.");
+static_assert(
+ nsIHttpChannelInternal::FETCH_CACHE_MODE_RELOAD ==
+ static_cast<uint32_t>(RequestCache::Reload),
+ "RequestCache enumeration value should match Necko Cache mode value.");
+static_assert(
+ nsIHttpChannelInternal::FETCH_CACHE_MODE_NO_CACHE ==
+ static_cast<uint32_t>(RequestCache::No_cache),
+ "RequestCache enumeration value should match Necko Cache mode value.");
+static_assert(
+ nsIHttpChannelInternal::FETCH_CACHE_MODE_FORCE_CACHE ==
+ static_cast<uint32_t>(RequestCache::Force_cache),
+ "RequestCache enumeration value should match Necko Cache mode value.");
+static_assert(
+ nsIHttpChannelInternal::FETCH_CACHE_MODE_ONLY_IF_CACHED ==
+ static_cast<uint32_t>(RequestCache::Only_if_cached),
+ "RequestCache enumeration value should match Necko Cache mode value.");
+static_assert(
+ 6 == RequestCacheValues::Count,
+ "RequestCache enumeration value should match Necko Cache mode value.");
+
+static_assert(static_cast<uint16_t>(ServiceWorkerUpdateViaCache::Imports) ==
+ nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_IMPORTS,
+ "nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_*"
+ " should match ServiceWorkerUpdateViaCache enumeration.");
+static_assert(static_cast<uint16_t>(ServiceWorkerUpdateViaCache::All) ==
+ nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_ALL,
+ "nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_*"
+ " should match ServiceWorkerUpdateViaCache enumeration.");
+static_assert(static_cast<uint16_t>(ServiceWorkerUpdateViaCache::None) ==
+ nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_NONE,
+ "nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_*"
+ " should match ServiceWorkerUpdateViaCache enumeration.");
+
+static StaticRefPtr<ServiceWorkerManager> gInstance;
+
+namespace {
+
+nsresult PopulateRegistrationData(
+ nsIPrincipal* aPrincipal,
+ const ServiceWorkerRegistrationInfo* aRegistration,
+ ServiceWorkerRegistrationData& aData) {
+ MOZ_ASSERT(aPrincipal);
+ MOZ_ASSERT(aRegistration);
+
+ if (NS_WARN_IF(!BasePrincipal::Cast(aPrincipal)->IsContentPrincipal())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsresult rv = PrincipalToPrincipalInfo(aPrincipal, &aData.principal());
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ aData.scope() = aRegistration->Scope();
+
+ // TODO: When bug 1426401 is implemented we will need to handle more
+ // than just the active worker here.
+ RefPtr<ServiceWorkerInfo> active = aRegistration->GetActive();
+ MOZ_ASSERT(active);
+ if (NS_WARN_IF(!active)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ aData.currentWorkerURL() = active->ScriptSpec();
+ aData.cacheName() = active->CacheName();
+ aData.currentWorkerHandlesFetch() = active->HandlesFetch();
+
+ aData.currentWorkerInstalledTime() = active->GetInstalledTime();
+ aData.currentWorkerActivatedTime() = active->GetActivatedTime();
+
+ aData.updateViaCache() =
+ static_cast<uint32_t>(aRegistration->GetUpdateViaCache());
+
+ aData.lastUpdateTime() = aRegistration->GetLastUpdateTime();
+
+ aData.navigationPreloadState() = aRegistration->GetNavigationPreloadState();
+
+ MOZ_ASSERT(ServiceWorkerRegistrationDataIsValid(aData));
+
+ return NS_OK;
+}
+
+class TeardownRunnable final : public Runnable {
+ public:
+ explicit TeardownRunnable(ServiceWorkerManagerChild* aActor)
+ : Runnable("dom::ServiceWorkerManager::TeardownRunnable"),
+ mActor(aActor) {
+ MOZ_ASSERT(mActor);
+ }
+
+ NS_IMETHOD Run() override {
+ MOZ_ASSERT(mActor);
+ PServiceWorkerManagerChild::Send__delete__(mActor);
+ return NS_OK;
+ }
+
+ private:
+ ~TeardownRunnable() = default;
+
+ RefPtr<ServiceWorkerManagerChild> mActor;
+};
+
+constexpr char kFinishShutdownTopic[] = "profile-before-change-qm";
+
+already_AddRefed<nsIAsyncShutdownClient> GetAsyncShutdownBarrier() {
+ AssertIsOnMainThread();
+
+ nsCOMPtr<nsIAsyncShutdownService> svc = services::GetAsyncShutdownService();
+ MOZ_ASSERT(svc);
+
+ nsCOMPtr<nsIAsyncShutdownClient> barrier;
+ DebugOnly<nsresult> rv =
+ svc->GetProfileChangeTeardown(getter_AddRefs(barrier));
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+
+ return barrier.forget();
+}
+
+Result<nsCOMPtr<nsIPrincipal>, nsresult> ScopeToPrincipal(
+ nsIURI* aScopeURI, const OriginAttributes& aOriginAttributes) {
+ MOZ_ASSERT(aScopeURI);
+
+ nsCOMPtr<nsIPrincipal> principal =
+ BasePrincipal::CreateContentPrincipal(aScopeURI, aOriginAttributes);
+ if (NS_WARN_IF(!principal)) {
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ return principal;
+}
+
+Result<nsCOMPtr<nsIPrincipal>, nsresult> ScopeToPrincipal(
+ const nsACString& aScope, const OriginAttributes& aOriginAttributes) {
+ MOZ_ASSERT(nsContentUtils::IsAbsoluteURL(aScope));
+
+ nsCOMPtr<nsIURI> scopeURI;
+ MOZ_TRY(NS_NewURI(getter_AddRefs(scopeURI), aScope));
+
+ return ScopeToPrincipal(scopeURI, aOriginAttributes);
+}
+
+} // namespace
+
+struct ServiceWorkerManager::RegistrationDataPerPrincipal final {
+ // Implements a container of keys for the "scope to registration map":
+ // https://w3c.github.io/ServiceWorker/#dfn-scope-to-registration-map
+ //
+ // where each key is an absolute URL.
+ //
+ // The properties of this map that the spec uses are
+ // 1) insertion,
+ // 2) removal,
+ // 3) iteration of scopes in FIFO order (excluding removed scopes),
+ // 4) and finding, for a given path, the maximal length scope which is a
+ // prefix of the path.
+ //
+ // Additionally, because this is a container of keys for a map, there
+ // shouldn't be duplicate scopes.
+ //
+ // The current implementation uses a dynamic array as the underlying
+ // container, which is not optimal for unbounded container sizes (all
+ // supported operations are in linear time) but may be superior for small
+ // container sizes.
+ //
+ // If this is proven to be too slow, the underlying storage should be replaced
+ // with a linked list of scopes in combination with an ordered map that maps
+ // scopes to linked list elements/iterators. This would reduce all of the
+ // above operations besides iteration (necessarily linear) to logarithmic
+ // time.
+ class ScopeContainer final : private nsTArray<nsCString> {
+ using Base = nsTArray<nsCString>;
+
+ public:
+ using Base::Contains;
+ using Base::IsEmpty;
+ using Base::Length;
+
+ // No using-declaration to avoid importing the non-const overload.
+ decltype(auto) operator[](Base::index_type aIndex) const {
+ return Base::operator[](aIndex);
+ }
+
+ void InsertScope(const nsACString& aScope) {
+ MOZ_DIAGNOSTIC_ASSERT(nsContentUtils::IsAbsoluteURL(aScope));
+
+ if (Contains(aScope)) {
+ return;
+ }
+
+ AppendElement(aScope);
+ }
+
+ void RemoveScope(const nsACString& aScope) {
+ MOZ_ALWAYS_TRUE(RemoveElement(aScope));
+ }
+
+ // Implements most of "Match Service Worker Registration":
+ // https://w3c.github.io/ServiceWorker/#scope-match-algorithm
+ Maybe<nsCString> MatchScope(const nsACString& aClientUrl) const {
+ Maybe<nsCString> match;
+
+ for (const nsCString& scope : *this) {
+ if (StringBeginsWith(aClientUrl, scope)) {
+ if (!match || scope.Length() > match->Length()) {
+ match = Some(scope);
+ }
+ }
+ }
+
+ // Step 7.2:
+ // "Assert: matchingScope’s origin and clientURL’s origin are same
+ // origin."
+ MOZ_DIAGNOSTIC_ASSERT_IF(match, IsSameOrigin(*match, aClientUrl));
+
+ return match;
+ }
+
+ private:
+ bool IsSameOrigin(const nsACString& aMatchingScope,
+ const nsACString& aClientUrl) const {
+ auto parseResult = ScopeToPrincipal(aMatchingScope, OriginAttributes());
+
+ if (NS_WARN_IF(parseResult.isErr())) {
+ return false;
+ }
+
+ auto scopePrincipal = parseResult.unwrap();
+
+ parseResult = ScopeToPrincipal(aClientUrl, OriginAttributes());
+
+ if (NS_WARN_IF(parseResult.isErr())) {
+ return false;
+ }
+
+ auto clientPrincipal = parseResult.unwrap();
+
+ bool equals = false;
+
+ if (NS_WARN_IF(
+ NS_FAILED(scopePrincipal->Equals(clientPrincipal, &equals)))) {
+ return false;
+ }
+
+ return equals;
+ }
+ };
+
+ ScopeContainer mScopeContainer;
+
+ // Scope to registration.
+ // The scope should be a fully qualified valid URL.
+ nsRefPtrHashtable<nsCStringHashKey, ServiceWorkerRegistrationInfo> mInfos;
+
+ // Maps scopes to job queues.
+ nsRefPtrHashtable<nsCStringHashKey, ServiceWorkerJobQueue> mJobQueues;
+
+ // Map scopes to scheduled update timers.
+ nsInterfaceHashtable<nsCStringHashKey, nsITimer> mUpdateTimers;
+
+ // The number of times we have done a quota usage check for this origin for
+ // mitigation purposes. See the docs on nsIServiceWorkerRegistrationInfo,
+ // where this value is exposed.
+ int32_t mQuotaUsageCheckCount = 0;
+};
+
+//////////////////////////
+// ServiceWorkerManager //
+//////////////////////////
+
+NS_IMPL_ADDREF(ServiceWorkerManager)
+NS_IMPL_RELEASE(ServiceWorkerManager)
+
+NS_INTERFACE_MAP_BEGIN(ServiceWorkerManager)
+ NS_INTERFACE_MAP_ENTRY(nsIServiceWorkerManager)
+ NS_INTERFACE_MAP_ENTRY(nsIObserver)
+ NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIServiceWorkerManager)
+NS_INTERFACE_MAP_END
+
+ServiceWorkerManager::ServiceWorkerManager()
+ : mActor(nullptr), mShuttingDown(false) {}
+
+ServiceWorkerManager::~ServiceWorkerManager() {
+ // The map will assert if it is not empty when destroyed.
+ mRegistrationInfos.Clear();
+
+ // This can happen if the browser is started up in ProfileManager mode, in
+ // which case XPCOM will startup and shutdown, but there won't be any
+ // profile-* topic notifications. The shutdown blocker expects to be in a
+ // NotAcceptingPromises state when it's destroyed, and this transition
+ // normally happens in the "profile-change-teardown" notification callback
+ // (which won't be called in ProfileManager mode).
+ if (!mShuttingDown && mShutdownBlocker) {
+ mShutdownBlocker->StopAcceptingPromises();
+ }
+}
+
+void ServiceWorkerManager::BlockShutdownOn(GenericNonExclusivePromise* aPromise,
+ uint32_t aShutdownStateId) {
+ AssertIsOnMainThread();
+
+ MOZ_ASSERT(mShutdownBlocker);
+ MOZ_ASSERT(aPromise);
+
+ mShutdownBlocker->WaitOnPromise(aPromise, aShutdownStateId);
+}
+
+void ServiceWorkerManager::Init(ServiceWorkerRegistrar* aRegistrar) {
+ // ServiceWorkers now only support parent intercept. In parent intercept
+ // mode, only the parent process ServiceWorkerManager has any state or does
+ // anything.
+ //
+ // It is our goal to completely eliminate support for content process
+ // ServiceWorkerManager instances and make getting a SWM instance trigger a
+ // fatal assertion. But until we've reached that point, we make
+ // initialization a no-op so that content process ServiceWorkerManager
+ // instances will simply have no state and no registrations.
+ if (!XRE_IsParentProcess()) {
+ return;
+ }
+
+ nsCOMPtr<nsIAsyncShutdownClient> shutdownBarrier = GetAsyncShutdownBarrier();
+
+ if (shutdownBarrier) {
+ mShutdownBlocker = ServiceWorkerShutdownBlocker::CreateAndRegisterOn(
+ *shutdownBarrier, *this);
+ MOZ_ASSERT(mShutdownBlocker);
+ }
+
+ MOZ_DIAGNOSTIC_ASSERT(aRegistrar);
+
+ nsTArray<ServiceWorkerRegistrationData> data;
+ aRegistrar->GetRegistrations(data);
+ LoadRegistrations(data);
+
+ PBackgroundChild* actorChild = BackgroundChild::GetOrCreateForCurrentThread();
+ if (NS_WARN_IF(!actorChild)) {
+ MaybeStartShutdown();
+ return;
+ }
+
+ PServiceWorkerManagerChild* actor =
+ actorChild->SendPServiceWorkerManagerConstructor();
+ if (!actor) {
+ MaybeStartShutdown();
+ return;
+ }
+
+ mActor = static_cast<ServiceWorkerManagerChild*>(actor);
+
+ mTelemetryLastChange = TimeStamp::Now();
+}
+
+void ServiceWorkerManager::RecordTelemetry(uint32_t aNumber, uint32_t aFetch) {
+ // Submit N value pairs to Telemetry for the time we were at those values
+ auto now = TimeStamp::Now();
+ // round down, with a minimum of 1 repeat. In theory this gives
+ // inaccuracy if there are frequent changes, but that's uncommon.
+ uint32_t repeats = (uint32_t)((now - mTelemetryLastChange).ToMilliseconds()) /
+ mTelemetryPeriodMs;
+ mTelemetryLastChange = now;
+ if (repeats == 0) {
+ repeats = 1;
+ }
+ nsCOMPtr<nsIRunnable> runnable = NS_NewRunnableFunction(
+ "ServiceWorkerTelemetryRunnable", [aNumber, aFetch, repeats]() {
+ LOG(("ServiceWorkers running: %u samples of %u/%u", repeats, aNumber,
+ aFetch));
+ // Don't allocate infinitely huge arrays if someone visits a SW site
+ // after a few months running. 1 month is about 500K repeats @ 5s
+ // sampling
+ uint32_t num_repeats = std::min(repeats, 1000000U); // 4MB max
+ nsTArray<uint32_t> values;
+
+ uint32_t* array = values.AppendElements(num_repeats);
+ for (uint32_t i = 0; i < num_repeats; i++) {
+ array[i] = aNumber;
+ }
+ Telemetry::Accumulate(Telemetry::SERVICE_WORKER_RUNNING, "All"_ns,
+ values);
+
+ for (uint32_t i = 0; i < num_repeats; i++) {
+ array[i] = aFetch;
+ }
+ Telemetry::Accumulate(Telemetry::SERVICE_WORKER_RUNNING, "Fetch"_ns,
+ values);
+ });
+ NS_DispatchBackgroundTask(runnable.forget(), nsIEventTarget::DISPATCH_NORMAL);
+}
+
+RefPtr<GenericErrorResultPromise> ServiceWorkerManager::StartControllingClient(
+ const ClientInfo& aClientInfo,
+ ServiceWorkerRegistrationInfo* aRegistrationInfo,
+ bool aControlClientHandle) {
+ MOZ_DIAGNOSTIC_ASSERT(aRegistrationInfo->GetActive());
+
+ // XXX We can't use a generic lambda (accepting auto&& entry) like elsewhere
+ // with WithEntryHandle, since we get linker errors then using clang+lld. This
+ // might be a toolchain issue?
+ return mControlledClients.WithEntryHandle(
+ aClientInfo.Id(),
+ [&](decltype(mControlledClients)::EntryHandle&& entry)
+ -> RefPtr<GenericErrorResultPromise> {
+ const RefPtr<ServiceWorkerManager> self = this;
+
+ const ServiceWorkerDescriptor& active =
+ aRegistrationInfo->GetActive()->Descriptor();
+
+ if (entry) {
+ const RefPtr<ServiceWorkerRegistrationInfo> old =
+ std::move(entry.Data()->mRegistrationInfo);
+
+ const RefPtr<GenericErrorResultPromise> promise =
+ aControlClientHandle
+ ? entry.Data()->mClientHandle->Control(active)
+ : GenericErrorResultPromise::CreateAndResolve(false,
+ __func__);
+
+ entry.Data()->mRegistrationInfo = aRegistrationInfo;
+
+ if (old != aRegistrationInfo) {
+ StopControllingRegistration(old);
+ aRegistrationInfo->StartControllingClient();
+ }
+
+ Telemetry::Accumulate(Telemetry::SERVICE_WORKER_CONTROLLED_DOCUMENTS,
+ 1);
+
+ // Always check to see if we failed to actually control the client. In
+ // that case remove the client from our list of controlled clients.
+ return promise->Then(
+ GetMainThreadSerialEventTarget(), __func__,
+ [](bool) {
+ // do nothing on success
+ return GenericErrorResultPromise::CreateAndResolve(true,
+ __func__);
+ },
+ [self, aClientInfo](const CopyableErrorResult& aRv) {
+ // failed to control, forget about this client
+ self->StopControllingClient(aClientInfo);
+ return GenericErrorResultPromise::CreateAndReject(aRv,
+ __func__);
+ });
+ }
+
+ RefPtr<ClientHandle> clientHandle = ClientManager::CreateHandle(
+ aClientInfo, GetMainThreadSerialEventTarget());
+
+ const RefPtr<GenericErrorResultPromise> promise =
+ aControlClientHandle
+ ? clientHandle->Control(active)
+ : GenericErrorResultPromise::CreateAndResolve(false, __func__);
+
+ aRegistrationInfo->StartControllingClient();
+
+ entry.Insert(
+ MakeUnique<ControlledClientData>(clientHandle, aRegistrationInfo));
+
+ clientHandle->OnDetach()->Then(
+ GetMainThreadSerialEventTarget(), __func__,
+ [self, aClientInfo] { self->StopControllingClient(aClientInfo); });
+
+ Telemetry::Accumulate(Telemetry::SERVICE_WORKER_CONTROLLED_DOCUMENTS,
+ 1);
+
+ // Always check to see if we failed to actually control the client. In
+ // that case removed the client from our list of controlled clients.
+ return promise->Then(
+ GetMainThreadSerialEventTarget(), __func__,
+ [](bool) {
+ // do nothing on success
+ return GenericErrorResultPromise::CreateAndResolve(true,
+ __func__);
+ },
+ [self, aClientInfo](const CopyableErrorResult& aRv) {
+ // failed to control, forget about this client
+ self->StopControllingClient(aClientInfo);
+ return GenericErrorResultPromise::CreateAndReject(aRv, __func__);
+ });
+ });
+}
+
+void ServiceWorkerManager::StopControllingClient(
+ const ClientInfo& aClientInfo) {
+ auto entry = mControlledClients.Lookup(aClientInfo.Id());
+ if (!entry) {
+ return;
+ }
+
+ RefPtr<ServiceWorkerRegistrationInfo> reg =
+ std::move(entry.Data()->mRegistrationInfo);
+
+ entry.Remove();
+
+ StopControllingRegistration(reg);
+}
+
+void ServiceWorkerManager::MaybeStartShutdown() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (mShuttingDown) {
+ return;
+ }
+
+ mShuttingDown = true;
+
+ for (const auto& dataPtr : mRegistrationInfos.Values()) {
+ for (const auto& timerEntry : dataPtr->mUpdateTimers.Values()) {
+ timerEntry->Cancel();
+ }
+ dataPtr->mUpdateTimers.Clear();
+
+ for (const auto& queueEntry : dataPtr->mJobQueues.Values()) {
+ queueEntry->CancelAll();
+ }
+ dataPtr->mJobQueues.Clear();
+
+ for (const auto& registrationEntry : dataPtr->mInfos.Values()) {
+ registrationEntry->ShutdownWorkers();
+ }
+
+ // ServiceWorkerCleanup may try to unregister registrations, so don't clear
+ // mInfos.
+ }
+
+ for (const auto& entry : mControlledClients.Values()) {
+ entry->mRegistrationInfo->ShutdownWorkers();
+ }
+
+ for (auto iter = mOrphanedRegistrations.iter(); !iter.done(); iter.next()) {
+ iter.get()->ShutdownWorkers();
+ }
+
+ if (mShutdownBlocker) {
+ mShutdownBlocker->StopAcceptingPromises();
+ }
+
+ nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
+ if (obs) {
+ obs->AddObserver(this, kFinishShutdownTopic, false);
+ return;
+ }
+
+ MaybeFinishShutdown();
+}
+
+void ServiceWorkerManager::MaybeFinishShutdown() {
+ nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
+ if (obs) {
+ obs->RemoveObserver(this, kFinishShutdownTopic);
+ }
+
+ if (!mActor) {
+ return;
+ }
+
+ mActor->ManagerShuttingDown();
+
+ RefPtr<TeardownRunnable> runnable = new TeardownRunnable(mActor);
+ nsresult rv = NS_DispatchToMainThread(runnable);
+ Unused << NS_WARN_IF(NS_FAILED(rv));
+ mActor = nullptr;
+
+ // This also submits final telemetry
+ ServiceWorkerPrivate::RunningShutdown();
+}
+
+class ServiceWorkerResolveWindowPromiseOnRegisterCallback final
+ : public ServiceWorkerJob::Callback {
+ public:
+ NS_INLINE_DECL_REFCOUNTING(
+ ServiceWorkerResolveWindowPromiseOnRegisterCallback, override)
+
+ virtual void JobFinished(ServiceWorkerJob* aJob,
+ ErrorResult& aStatus) override {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aJob);
+
+ if (aStatus.Failed()) {
+ mPromiseHolder.Reject(CopyableErrorResult(aStatus), __func__);
+ return;
+ }
+
+ MOZ_ASSERT(aJob->GetType() == ServiceWorkerJob::Type::Register);
+ RefPtr<ServiceWorkerRegisterJob> registerJob =
+ static_cast<ServiceWorkerRegisterJob*>(aJob);
+ RefPtr<ServiceWorkerRegistrationInfo> reg = registerJob->GetRegistration();
+
+ mPromiseHolder.Resolve(reg->Descriptor(), __func__);
+ }
+
+ virtual void JobDiscarded(ErrorResult& aStatus) override {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ mPromiseHolder.Reject(CopyableErrorResult(aStatus), __func__);
+ }
+
+ RefPtr<ServiceWorkerRegistrationPromise> Promise() {
+ MOZ_ASSERT(NS_IsMainThread());
+ return mPromiseHolder.Ensure(__func__);
+ }
+
+ private:
+ ~ServiceWorkerResolveWindowPromiseOnRegisterCallback() = default;
+
+ MozPromiseHolder<ServiceWorkerRegistrationPromise> mPromiseHolder;
+};
+
+NS_IMETHODIMP
+ServiceWorkerManager::RegisterForTest(nsIPrincipal* aPrincipal,
+ const nsAString& aScopeURL,
+ const nsAString& aScriptURL,
+ JSContext* aCx,
+ mozilla::dom::Promise** aPromise) {
+ nsIGlobalObject* global = xpc::CurrentNativeGlobal(aCx);
+ if (NS_WARN_IF(!global)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ ErrorResult erv;
+ RefPtr<Promise> outer = Promise::Create(global, erv);
+ if (NS_WARN_IF(erv.Failed())) {
+ return erv.StealNSResult();
+ }
+
+ if (!StaticPrefs::dom_serviceWorkers_testing_enabled()) {
+ outer->MaybeRejectWithAbortError(
+ "registerForTest only allowed when dom.serviceWorkers.testing.enabled "
+ "is true");
+ outer.forget(aPromise);
+ return NS_OK;
+ }
+
+ if (aPrincipal == nullptr) {
+ outer->MaybeRejectWithAbortError("Missing principal");
+ outer.forget(aPromise);
+ return NS_OK;
+ }
+
+ if (aScriptURL.IsEmpty()) {
+ outer->MaybeRejectWithAbortError("Missing script url");
+ outer.forget(aPromise);
+ return NS_OK;
+ }
+
+ if (aScopeURL.IsEmpty()) {
+ outer->MaybeRejectWithAbortError("Missing scope url");
+ outer.forget(aPromise);
+ return NS_OK;
+ }
+
+ // The ClientType isn't really used here, but ClientType::Window
+ // is the least bad choice since this is happening on the main thread.
+ Maybe<ClientInfo> clientInfo =
+ dom::ClientManager::CreateInfo(ClientType::Window, aPrincipal);
+
+ if (!clientInfo.isSome()) {
+ outer->MaybeRejectWithUnknownError("Error creating clientInfo");
+ outer.forget(aPromise);
+ return NS_OK;
+ }
+
+ auto scope = NS_ConvertUTF16toUTF8(aScopeURL);
+ auto scriptURL = NS_ConvertUTF16toUTF8(aScriptURL);
+
+ auto regPromise = Register(clientInfo.ref(), scope, scriptURL,
+ dom::ServiceWorkerUpdateViaCache::Imports);
+ const RefPtr<ServiceWorkerManager> self(this);
+ const nsCOMPtr<nsIPrincipal> principal(aPrincipal);
+ regPromise->Then(
+ GetMainThreadSerialEventTarget(), __func__,
+ [self, outer, principal,
+ scope](const ServiceWorkerRegistrationDescriptor& regDesc) {
+ RefPtr<ServiceWorkerRegistrationInfo> registration =
+ self->GetRegistration(principal, NS_ConvertUTF16toUTF8(scope));
+ if (registration) {
+ outer->MaybeResolve(registration);
+ } else {
+ outer->MaybeRejectWithUnknownError(
+ "Failed to retrieve ServiceWorkerRegistrationInfo");
+ }
+ },
+ [outer](const mozilla::CopyableErrorResult& err) {
+ CopyableErrorResult result(err);
+ outer->MaybeReject(std::move(result));
+ });
+
+ outer.forget(aPromise);
+
+ return NS_OK;
+}
+
+RefPtr<ServiceWorkerRegistrationPromise> ServiceWorkerManager::Register(
+ const ClientInfo& aClientInfo, const nsACString& aScopeURL,
+ const nsACString& aScriptURL, ServiceWorkerUpdateViaCache aUpdateViaCache) {
+ nsCOMPtr<nsIURI> scopeURI;
+ nsresult rv = NS_NewURI(getter_AddRefs(scopeURI), aScopeURL);
+ if (NS_FAILED(rv)) {
+ // Odd, since it was serialiazed from an nsIURI.
+ CopyableErrorResult err;
+ err.ThrowInvalidStateError("Scope URL cannot be parsed");
+ return ServiceWorkerRegistrationPromise::CreateAndReject(err, __func__);
+ }
+
+ nsCOMPtr<nsIURI> scriptURI;
+ rv = NS_NewURI(getter_AddRefs(scriptURI), aScriptURL);
+ if (NS_FAILED(rv)) {
+ // Odd, since it was serialiazed from an nsIURI.
+ CopyableErrorResult err;
+ err.ThrowInvalidStateError("Script URL cannot be parsed");
+ return ServiceWorkerRegistrationPromise::CreateAndReject(err, __func__);
+ }
+
+ IgnoredErrorResult err;
+ ServiceWorkerScopeAndScriptAreValid(aClientInfo, scopeURI, scriptURI, err);
+ if (err.Failed()) {
+ return ServiceWorkerRegistrationPromise::CreateAndReject(
+ CopyableErrorResult(std::move(err)), __func__);
+ }
+
+ // If the previous validation step passed then we must have a principal.
+ auto principalOrErr = aClientInfo.GetPrincipal();
+
+ if (NS_WARN_IF(principalOrErr.isErr())) {
+ return ServiceWorkerRegistrationPromise::CreateAndReject(
+ CopyableErrorResult(principalOrErr.unwrapErr()), __func__);
+ }
+
+ nsCOMPtr<nsIPrincipal> principal = principalOrErr.unwrap();
+ nsAutoCString scopeKey;
+ rv = PrincipalToScopeKey(principal, scopeKey);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return ServiceWorkerRegistrationPromise::CreateAndReject(
+ CopyableErrorResult(rv), __func__);
+ }
+
+ RefPtr<ServiceWorkerJobQueue> queue =
+ GetOrCreateJobQueue(scopeKey, aScopeURL);
+
+ RefPtr<ServiceWorkerResolveWindowPromiseOnRegisterCallback> cb =
+ new ServiceWorkerResolveWindowPromiseOnRegisterCallback();
+
+ RefPtr<ServiceWorkerRegisterJob> job = new ServiceWorkerRegisterJob(
+ principal, aScopeURL, aScriptURL,
+ static_cast<ServiceWorkerUpdateViaCache>(aUpdateViaCache));
+
+ job->AppendResultCallback(cb);
+ queue->ScheduleJob(job);
+
+ MOZ_ASSERT(NS_IsMainThread());
+
+ return cb->Promise();
+}
+
+/*
+ * Implements the async aspects of the getRegistrations algorithm.
+ */
+class GetRegistrationsRunnable final : public Runnable {
+ const ClientInfo mClientInfo;
+ RefPtr<ServiceWorkerRegistrationListPromise::Private> mPromise;
+
+ public:
+ explicit GetRegistrationsRunnable(const ClientInfo& aClientInfo)
+ : Runnable("dom::ServiceWorkerManager::GetRegistrationsRunnable"),
+ mClientInfo(aClientInfo),
+ mPromise(new ServiceWorkerRegistrationListPromise::Private(__func__)) {}
+
+ RefPtr<ServiceWorkerRegistrationListPromise> Promise() const {
+ return mPromise;
+ }
+
+ NS_IMETHOD
+ Run() override {
+ auto scopeExit = MakeScopeExit(
+ [&] { mPromise->Reject(NS_ERROR_DOM_INVALID_STATE_ERR, __func__); });
+
+ RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance();
+ if (!swm) {
+ return NS_OK;
+ }
+
+ auto principalOrErr = mClientInfo.GetPrincipal();
+ if (NS_WARN_IF(principalOrErr.isErr())) {
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIPrincipal> principal = principalOrErr.unwrap();
+
+ nsTArray<ServiceWorkerRegistrationDescriptor> array;
+
+ if (NS_WARN_IF(!BasePrincipal::Cast(principal)->IsContentPrincipal())) {
+ return NS_OK;
+ }
+
+ nsAutoCString scopeKey;
+ nsresult rv = swm->PrincipalToScopeKey(principal, scopeKey);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ ServiceWorkerManager::RegistrationDataPerPrincipal* data;
+ if (!swm->mRegistrationInfos.Get(scopeKey, &data)) {
+ scopeExit.release();
+ mPromise->Resolve(array, __func__);
+ return NS_OK;
+ }
+
+ for (uint32_t i = 0; i < data->mScopeContainer.Length(); ++i) {
+ RefPtr<ServiceWorkerRegistrationInfo> info =
+ data->mInfos.GetWeak(data->mScopeContainer[i]);
+
+ NS_ConvertUTF8toUTF16 scope(data->mScopeContainer[i]);
+
+ nsCOMPtr<nsIURI> scopeURI;
+ nsresult rv = NS_NewURI(getter_AddRefs(scopeURI), scope);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ break;
+ }
+
+ // Unfortunately we don't seem to have an obvious window id here; in
+ // particular ClientInfo does not have one, and neither do service worker
+ // registrations, as far as I can tell.
+ rv = principal->CheckMayLoadWithReporting(
+ scopeURI, false /* allowIfInheritsPrincipal */,
+ 0 /* innerWindowID */);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ continue;
+ }
+
+ array.AppendElement(info->Descriptor());
+ }
+
+ scopeExit.release();
+ mPromise->Resolve(array, __func__);
+
+ return NS_OK;
+ }
+};
+
+RefPtr<ServiceWorkerRegistrationListPromise>
+ServiceWorkerManager::GetRegistrations(const ClientInfo& aClientInfo) const {
+ RefPtr<GetRegistrationsRunnable> runnable =
+ new GetRegistrationsRunnable(aClientInfo);
+ MOZ_ALWAYS_SUCCEEDS(NS_DispatchToCurrentThread(runnable));
+ return runnable->Promise();
+}
+
+/*
+ * Implements the async aspects of the getRegistration algorithm.
+ */
+class GetRegistrationRunnable final : public Runnable {
+ const ClientInfo mClientInfo;
+ RefPtr<ServiceWorkerRegistrationPromise::Private> mPromise;
+ nsCString mURL;
+
+ public:
+ GetRegistrationRunnable(const ClientInfo& aClientInfo, const nsACString& aURL)
+ : Runnable("dom::ServiceWorkerManager::GetRegistrationRunnable"),
+ mClientInfo(aClientInfo),
+ mPromise(new ServiceWorkerRegistrationPromise::Private(__func__)),
+ mURL(aURL) {}
+
+ RefPtr<ServiceWorkerRegistrationPromise> Promise() const { return mPromise; }
+
+ NS_IMETHOD
+ Run() override {
+ RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance();
+ if (!swm) {
+ mPromise->Reject(NS_ERROR_DOM_INVALID_STATE_ERR, __func__);
+ return NS_OK;
+ }
+
+ auto principalOrErr = mClientInfo.GetPrincipal();
+ if (NS_WARN_IF(principalOrErr.isErr())) {
+ mPromise->Reject(NS_ERROR_DOM_INVALID_STATE_ERR, __func__);
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIPrincipal> principal = principalOrErr.unwrap();
+ nsCOMPtr<nsIURI> uri;
+ nsresult rv = NS_NewURI(getter_AddRefs(uri), mURL);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ mPromise->Reject(rv, __func__);
+ return NS_OK;
+ }
+
+ // Unfortunately we don't seem to have an obvious window id here; in
+ // particular ClientInfo does not have one, and neither do service worker
+ // registrations, as far as I can tell.
+ rv = principal->CheckMayLoadWithReporting(
+ uri, false /* allowIfInheritsPrincipal */, 0 /* innerWindowID */);
+ if (NS_FAILED(rv)) {
+ mPromise->Reject(NS_ERROR_DOM_SECURITY_ERR, __func__);
+ return NS_OK;
+ }
+
+ RefPtr<ServiceWorkerRegistrationInfo> registration =
+ swm->GetServiceWorkerRegistrationInfo(principal, uri);
+
+ if (!registration) {
+ // Reject with NS_OK means "not found".
+ mPromise->Reject(NS_OK, __func__);
+ return NS_OK;
+ }
+
+ mPromise->Resolve(registration->Descriptor(), __func__);
+
+ return NS_OK;
+ }
+};
+
+RefPtr<ServiceWorkerRegistrationPromise> ServiceWorkerManager::GetRegistration(
+ const ClientInfo& aClientInfo, const nsACString& aURL) const {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ RefPtr<GetRegistrationRunnable> runnable =
+ new GetRegistrationRunnable(aClientInfo, aURL);
+ MOZ_ALWAYS_SUCCEEDS(NS_DispatchToCurrentThread(runnable));
+
+ return runnable->Promise();
+}
+
+NS_IMETHODIMP
+ServiceWorkerManager::SendPushEvent(const nsACString& aOriginAttributes,
+ const nsACString& aScope,
+ const nsTArray<uint8_t>& aDataBytes,
+ uint8_t optional_argc) {
+ if (optional_argc == 1) {
+ // This does one copy here (while constructing the Maybe) and another when
+ // we end up copying into the SendPushEventRunnable. We could fix that to
+ // only do one copy by making things between here and there take
+ // Maybe<nsTArray<uint8_t>>&&, but then we'd need to copy before we know
+ // whether we really need to in PushMessageDispatcher::NotifyWorkers. Since
+ // in practice this only affects JS callers that pass data, and we don't
+ // have any right now, let's not worry about it.
+ return SendPushEvent(aOriginAttributes, aScope, u""_ns,
+ Some(aDataBytes.Clone()));
+ }
+ MOZ_ASSERT(optional_argc == 0);
+ return SendPushEvent(aOriginAttributes, aScope, u""_ns, Nothing());
+}
+
+nsresult ServiceWorkerManager::SendPushEvent(
+ const nsACString& aOriginAttributes, const nsACString& aScope,
+ const nsAString& aMessageId, const Maybe<nsTArray<uint8_t>>& aData) {
+ OriginAttributes attrs;
+ if (!attrs.PopulateFromSuffix(aOriginAttributes)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ nsCOMPtr<nsIPrincipal> principal;
+ MOZ_TRY_VAR(principal, ScopeToPrincipal(aScope, attrs));
+
+ // The registration handling a push notification must have an exact scope
+ // match. This will try to find an exact match, unlike how fetch may find the
+ // registration with the longest scope that's a prefix of the fetched URL.
+ RefPtr<ServiceWorkerRegistrationInfo> registration =
+ GetRegistration(principal, aScope);
+ if (NS_WARN_IF(!registration)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ MOZ_DIAGNOSTIC_ASSERT(registration->Scope().Equals(aScope));
+
+ ServiceWorkerInfo* serviceWorker = registration->GetActive();
+ if (NS_WARN_IF(!serviceWorker)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ return serviceWorker->WorkerPrivate()->SendPushEvent(aMessageId, aData,
+ registration);
+}
+
+NS_IMETHODIMP
+ServiceWorkerManager::SendPushSubscriptionChangeEvent(
+ const nsACString& aOriginAttributes, const nsACString& aScope) {
+ OriginAttributes attrs;
+ if (!attrs.PopulateFromSuffix(aOriginAttributes)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ ServiceWorkerInfo* info = GetActiveWorkerInfoForScope(attrs, aScope);
+ if (!info) {
+ return NS_ERROR_FAILURE;
+ }
+ return info->WorkerPrivate()->SendPushSubscriptionChangeEvent();
+}
+
+nsresult ServiceWorkerManager::SendNotificationEvent(
+ const nsAString& aEventName, const nsACString& aOriginSuffix,
+ const nsACString& aScope, const nsAString& aID, const nsAString& aTitle,
+ const nsAString& aDir, const nsAString& aLang, const nsAString& aBody,
+ const nsAString& aTag, const nsAString& aIcon, const nsAString& aData,
+ const nsAString& aBehavior) {
+ OriginAttributes attrs;
+ if (!attrs.PopulateFromSuffix(aOriginSuffix)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ ServiceWorkerInfo* info = GetActiveWorkerInfoForScope(attrs, aScope);
+ if (!info) {
+ return NS_ERROR_FAILURE;
+ }
+
+ ServiceWorkerPrivate* workerPrivate = info->WorkerPrivate();
+ return workerPrivate->SendNotificationEvent(
+ aEventName, aID, aTitle, aDir, aLang, aBody, aTag, aIcon, aData,
+ aBehavior, NS_ConvertUTF8toUTF16(aScope));
+}
+
+NS_IMETHODIMP
+ServiceWorkerManager::SendNotificationClickEvent(
+ const nsACString& aOriginSuffix, const nsACString& aScope,
+ const nsAString& aID, const nsAString& aTitle, const nsAString& aDir,
+ const nsAString& aLang, const nsAString& aBody, const nsAString& aTag,
+ const nsAString& aIcon, const nsAString& aData,
+ const nsAString& aBehavior) {
+ return SendNotificationEvent(nsLiteralString(NOTIFICATION_CLICK_EVENT_NAME),
+ aOriginSuffix, aScope, aID, aTitle, aDir, aLang,
+ aBody, aTag, aIcon, aData, aBehavior);
+}
+
+NS_IMETHODIMP
+ServiceWorkerManager::SendNotificationCloseEvent(
+ const nsACString& aOriginSuffix, const nsACString& aScope,
+ const nsAString& aID, const nsAString& aTitle, const nsAString& aDir,
+ const nsAString& aLang, const nsAString& aBody, const nsAString& aTag,
+ const nsAString& aIcon, const nsAString& aData,
+ const nsAString& aBehavior) {
+ return SendNotificationEvent(nsLiteralString(NOTIFICATION_CLOSE_EVENT_NAME),
+ aOriginSuffix, aScope, aID, aTitle, aDir, aLang,
+ aBody, aTag, aIcon, aData, aBehavior);
+}
+
+RefPtr<ServiceWorkerRegistrationPromise> ServiceWorkerManager::WhenReady(
+ const ClientInfo& aClientInfo) {
+ AssertIsOnMainThread();
+
+ for (auto& prd : mPendingReadyList) {
+ if (prd->mClientHandle->Info().Id() == aClientInfo.Id() &&
+ prd->mClientHandle->Info().PrincipalInfo() ==
+ aClientInfo.PrincipalInfo()) {
+ return prd->mPromise;
+ }
+ }
+
+ RefPtr<ServiceWorkerRegistrationInfo> reg =
+ GetServiceWorkerRegistrationInfo(aClientInfo);
+ if (reg && reg->GetActive()) {
+ return ServiceWorkerRegistrationPromise::CreateAndResolve(reg->Descriptor(),
+ __func__);
+ }
+
+ nsCOMPtr<nsISerialEventTarget> target = GetMainThreadSerialEventTarget();
+
+ RefPtr<ClientHandle> handle =
+ ClientManager::CreateHandle(aClientInfo, target);
+ mPendingReadyList.AppendElement(MakeUnique<PendingReadyData>(handle));
+
+ RefPtr<ServiceWorkerManager> self(this);
+ handle->OnDetach()->Then(target, __func__,
+ [self = std::move(self), aClientInfo] {
+ self->RemovePendingReadyPromise(aClientInfo);
+ });
+
+ return mPendingReadyList.LastElement()->mPromise;
+}
+
+void ServiceWorkerManager::CheckPendingReadyPromises() {
+ nsTArray<UniquePtr<PendingReadyData>> pendingReadyList =
+ std::move(mPendingReadyList);
+ for (uint32_t i = 0; i < pendingReadyList.Length(); ++i) {
+ UniquePtr<PendingReadyData> prd(std::move(pendingReadyList[i]));
+
+ RefPtr<ServiceWorkerRegistrationInfo> reg =
+ GetServiceWorkerRegistrationInfo(prd->mClientHandle->Info());
+
+ if (reg && reg->GetActive()) {
+ prd->mPromise->Resolve(reg->Descriptor(), __func__);
+ } else {
+ mPendingReadyList.AppendElement(std::move(prd));
+ }
+ }
+}
+
+void ServiceWorkerManager::RemovePendingReadyPromise(
+ const ClientInfo& aClientInfo) {
+ nsTArray<UniquePtr<PendingReadyData>> pendingReadyList =
+ std::move(mPendingReadyList);
+ for (uint32_t i = 0; i < pendingReadyList.Length(); ++i) {
+ UniquePtr<PendingReadyData> prd(std::move(pendingReadyList[i]));
+
+ if (prd->mClientHandle->Info().Id() == aClientInfo.Id() &&
+ prd->mClientHandle->Info().PrincipalInfo() ==
+ aClientInfo.PrincipalInfo()) {
+ prd->mPromise->Reject(NS_ERROR_DOM_ABORT_ERR, __func__);
+ } else {
+ mPendingReadyList.AppendElement(std::move(prd));
+ }
+ }
+}
+
+void ServiceWorkerManager::NoteInheritedController(
+ const ClientInfo& aClientInfo, const ServiceWorkerDescriptor& aController) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ auto principalOrErr = PrincipalInfoToPrincipal(aController.PrincipalInfo());
+
+ if (NS_WARN_IF(principalOrErr.isErr())) {
+ return;
+ }
+
+ nsCOMPtr<nsIPrincipal> principal = principalOrErr.unwrap();
+ nsCOMPtr<nsIURI> scope;
+ nsresult rv = NS_NewURI(getter_AddRefs(scope), aController.Scope());
+ NS_ENSURE_SUCCESS_VOID(rv);
+
+ RefPtr<ServiceWorkerRegistrationInfo> registration =
+ GetServiceWorkerRegistrationInfo(principal, scope);
+ NS_ENSURE_TRUE_VOID(registration);
+ NS_ENSURE_TRUE_VOID(registration->GetActive());
+
+ StartControllingClient(aClientInfo, registration,
+ false /* aControlClientHandle */);
+}
+
+ServiceWorkerInfo* ServiceWorkerManager::GetActiveWorkerInfoForScope(
+ const OriginAttributes& aOriginAttributes, const nsACString& aScope) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ nsCOMPtr<nsIURI> scopeURI;
+ nsresult rv = NS_NewURI(getter_AddRefs(scopeURI), aScope);
+ if (NS_FAILED(rv)) {
+ return nullptr;
+ }
+
+ auto result = ScopeToPrincipal(scopeURI, aOriginAttributes);
+ if (NS_WARN_IF(result.isErr())) {
+ return nullptr;
+ }
+
+ auto principal = result.unwrap();
+
+ RefPtr<ServiceWorkerRegistrationInfo> registration =
+ GetServiceWorkerRegistrationInfo(principal, scopeURI);
+ if (!registration) {
+ return nullptr;
+ }
+
+ return registration->GetActive();
+}
+
+namespace {
+
+class UnregisterJobCallback final : public ServiceWorkerJob::Callback {
+ nsCOMPtr<nsIServiceWorkerUnregisterCallback> mCallback;
+
+ ~UnregisterJobCallback() { MOZ_ASSERT(!mCallback); }
+
+ public:
+ explicit UnregisterJobCallback(nsIServiceWorkerUnregisterCallback* aCallback)
+ : mCallback(aCallback) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(mCallback);
+ }
+
+ void JobFinished(ServiceWorkerJob* aJob, ErrorResult& aStatus) override {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aJob);
+ MOZ_ASSERT(mCallback);
+
+ auto scopeExit = MakeScopeExit([&]() { mCallback = nullptr; });
+
+ if (aStatus.Failed()) {
+ mCallback->UnregisterFailed();
+ return;
+ }
+
+ MOZ_ASSERT(aJob->GetType() == ServiceWorkerJob::Type::Unregister);
+ RefPtr<ServiceWorkerUnregisterJob> unregisterJob =
+ static_cast<ServiceWorkerUnregisterJob*>(aJob);
+ mCallback->UnregisterSucceeded(unregisterJob->GetResult());
+ }
+
+ void JobDiscarded(ErrorResult&) override {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(mCallback);
+
+ mCallback->UnregisterFailed();
+ mCallback = nullptr;
+ }
+
+ NS_INLINE_DECL_REFCOUNTING(UnregisterJobCallback, override)
+};
+
+} // anonymous namespace
+
+NS_IMETHODIMP
+ServiceWorkerManager::Unregister(nsIPrincipal* aPrincipal,
+ nsIServiceWorkerUnregisterCallback* aCallback,
+ const nsAString& aScope) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (!aPrincipal) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsresult rv;
+
+// This is not accessible by content, and callers should always ensure scope is
+// a correct URI, so this is wrapped in DEBUG
+#ifdef DEBUG
+ nsCOMPtr<nsIURI> scopeURI;
+ rv = NS_NewURI(getter_AddRefs(scopeURI), aScope);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return NS_ERROR_DOM_SECURITY_ERR;
+ }
+#endif
+
+ nsAutoCString scopeKey;
+ rv = PrincipalToScopeKey(aPrincipal, scopeKey);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ NS_ConvertUTF16toUTF8 scope(aScope);
+ RefPtr<ServiceWorkerJobQueue> queue = GetOrCreateJobQueue(scopeKey, scope);
+
+ RefPtr<ServiceWorkerUnregisterJob> job = new ServiceWorkerUnregisterJob(
+ aPrincipal, scope, true /* send to parent */);
+
+ if (aCallback) {
+ RefPtr<UnregisterJobCallback> cb = new UnregisterJobCallback(aCallback);
+ job->AppendResultCallback(cb);
+ }
+
+ queue->ScheduleJob(job);
+ return NS_OK;
+}
+
+void ServiceWorkerManager::WorkerIsIdle(ServiceWorkerInfo* aWorker) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_DIAGNOSTIC_ASSERT(aWorker);
+
+ RefPtr<ServiceWorkerRegistrationInfo> reg =
+ GetRegistration(aWorker->Principal(), aWorker->Scope());
+ if (!reg) {
+ return;
+ }
+
+ if (reg->GetActive() != aWorker) {
+ return;
+ }
+
+ reg->TryToActivateAsync();
+}
+
+already_AddRefed<ServiceWorkerJobQueue>
+ServiceWorkerManager::GetOrCreateJobQueue(const nsACString& aKey,
+ const nsACString& aScope) {
+ MOZ_ASSERT(!aKey.IsEmpty());
+ ServiceWorkerManager::RegistrationDataPerPrincipal* data;
+ // XXX we could use WithEntryHandle here to avoid a hashtable lookup, except
+ // that leads to a false positive assertion, see bug 1370674 comment 7.
+ if (!mRegistrationInfos.Get(aKey, &data)) {
+ data = mRegistrationInfos
+ .InsertOrUpdate(aKey, MakeUnique<RegistrationDataPerPrincipal>())
+ .get();
+ }
+
+ RefPtr queue = data->mJobQueues.GetOrInsertNew(aScope);
+ return queue.forget();
+}
+
+/* static */
+already_AddRefed<ServiceWorkerManager> ServiceWorkerManager::GetInstance() {
+ if (!gInstance) {
+ RefPtr<ServiceWorkerRegistrar> swr;
+
+ // XXX: Substitute this with an assertion. See comment in Init.
+ if (XRE_IsParentProcess()) {
+ // Don't (re-)create the ServiceWorkerManager if we are already shutting
+ // down.
+ if (AppShutdown::IsInOrBeyond(ShutdownPhase::AppShutdownConfirmed)) {
+ return nullptr;
+ }
+ // Don't create the ServiceWorkerManager until the ServiceWorkerRegistrar
+ // is initialized.
+ swr = ServiceWorkerRegistrar::Get();
+ if (!swr) {
+ return nullptr;
+ }
+ }
+
+ MOZ_ASSERT(NS_IsMainThread());
+
+ gInstance = new ServiceWorkerManager();
+ gInstance->Init(swr);
+ ClearOnShutdown(&gInstance);
+ }
+ RefPtr<ServiceWorkerManager> copy = gInstance.get();
+ return copy.forget();
+}
+
+void ServiceWorkerManager::ReportToAllClients(
+ const nsCString& aScope, const nsString& aMessage,
+ const nsString& aFilename, const nsString& aLine, uint32_t aLineNumber,
+ uint32_t aColumnNumber, uint32_t aFlags) {
+ ConsoleUtils::ReportForServiceWorkerScope(
+ NS_ConvertUTF8toUTF16(aScope), aMessage, aFilename, aLineNumber,
+ aColumnNumber, ConsoleUtils::eError);
+}
+
+/* static */
+void ServiceWorkerManager::LocalizeAndReportToAllClients(
+ const nsCString& aScope, const char* aStringKey,
+ const nsTArray<nsString>& aParamArray, uint32_t aFlags,
+ const nsString& aFilename, const nsString& aLine, uint32_t aLineNumber,
+ uint32_t aColumnNumber) {
+ RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance();
+ if (!swm) {
+ return;
+ }
+
+ nsresult rv;
+ nsAutoString message;
+ rv = nsContentUtils::FormatLocalizedString(nsContentUtils::eDOM_PROPERTIES,
+ aStringKey, aParamArray, message);
+ if (NS_SUCCEEDED(rv)) {
+ swm->ReportToAllClients(aScope, message, aFilename, aLine, aLineNumber,
+ aColumnNumber, aFlags);
+ } else {
+ NS_WARNING("Failed to format and therefore report localized error.");
+ }
+}
+
+void ServiceWorkerManager::HandleError(
+ JSContext* aCx, nsIPrincipal* aPrincipal, const nsCString& aScope,
+ const nsString& aWorkerURL, const nsString& aMessage,
+ const nsString& aFilename, const nsString& aLine, uint32_t aLineNumber,
+ uint32_t aColumnNumber, uint32_t aFlags, JSExnType aExnType) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aPrincipal);
+
+ nsAutoCString scopeKey;
+ nsresult rv = PrincipalToScopeKey(aPrincipal, scopeKey);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+
+ ServiceWorkerManager::RegistrationDataPerPrincipal* data;
+ if (NS_WARN_IF(!mRegistrationInfos.Get(scopeKey, &data))) {
+ return;
+ }
+
+ // Always report any uncaught exceptions or errors to the console of
+ // each client.
+ ReportToAllClients(aScope, aMessage, aFilename, aLine, aLineNumber,
+ aColumnNumber, aFlags);
+}
+
+void ServiceWorkerManager::LoadRegistration(
+ const ServiceWorkerRegistrationData& aRegistration) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ auto principalOrErr = PrincipalInfoToPrincipal(aRegistration.principal());
+ if (NS_WARN_IF(principalOrErr.isErr())) {
+ return;
+ }
+ nsCOMPtr<nsIPrincipal> principal = principalOrErr.unwrap();
+
+ // Purge extensions registrations if they are disabled by prefs.
+ if (!StaticPrefs::extensions_backgroundServiceWorker_enabled_AtStartup()) {
+ nsCOMPtr<nsIURI> uri = principal->GetURI();
+
+ // We do check the URI scheme here because when this is going to run
+ // the extension may not have been loaded yet and the WebExtensionPolicy
+ // may not exist yet.
+ if (uri->SchemeIs("moz-extension")) {
+ const auto& cacheName = aRegistration.cacheName();
+ serviceWorkerScriptCache::PurgeCache(principal, cacheName);
+ return;
+ }
+ }
+
+ RefPtr<ServiceWorkerRegistrationInfo> registration =
+ GetRegistration(principal, aRegistration.scope());
+ if (!registration) {
+ registration =
+ CreateNewRegistration(aRegistration.scope(), principal,
+ static_cast<ServiceWorkerUpdateViaCache>(
+ aRegistration.updateViaCache()),
+ aRegistration.navigationPreloadState());
+ } else {
+ // If active worker script matches our expectations for a "current worker",
+ // then we are done. Since scripts with the same URL might have different
+ // contents such as updated scripts or scripts with different LoadFlags, we
+ // use the CacheName to judge whether the two scripts are identical, where
+ // the CacheName is an UUID generated when a new script is found.
+ if (registration->GetActive() &&
+ registration->GetActive()->CacheName() == aRegistration.cacheName()) {
+ // No needs for updates.
+ return;
+ }
+ }
+
+ registration->SetLastUpdateTime(aRegistration.lastUpdateTime());
+
+ nsLoadFlags importsLoadFlags = nsIChannel::LOAD_BYPASS_SERVICE_WORKER;
+ if (aRegistration.updateViaCache() !=
+ static_cast<uint16_t>(ServiceWorkerUpdateViaCache::None)) {
+ importsLoadFlags |= nsIRequest::VALIDATE_ALWAYS;
+ }
+
+ const nsCString& currentWorkerURL = aRegistration.currentWorkerURL();
+ if (!currentWorkerURL.IsEmpty()) {
+ registration->SetActive(new ServiceWorkerInfo(
+ registration->Principal(), registration->Scope(), registration->Id(),
+ registration->Version(), currentWorkerURL, aRegistration.cacheName(),
+ importsLoadFlags));
+ registration->GetActive()->SetHandlesFetch(
+ aRegistration.currentWorkerHandlesFetch());
+ registration->GetActive()->SetInstalledTime(
+ aRegistration.currentWorkerInstalledTime());
+ registration->GetActive()->SetActivatedTime(
+ aRegistration.currentWorkerActivatedTime());
+ }
+}
+
+void ServiceWorkerManager::LoadRegistrations(
+ const nsTArray<ServiceWorkerRegistrationData>& aRegistrations) {
+ MOZ_ASSERT(NS_IsMainThread());
+ uint32_t fetch = 0;
+ for (uint32_t i = 0, len = aRegistrations.Length(); i < len; ++i) {
+ LoadRegistration(aRegistrations[i]);
+ if (aRegistrations[i].currentWorkerHandlesFetch()) {
+ fetch++;
+ }
+ }
+ gServiceWorkersRegistered = aRegistrations.Length();
+ gServiceWorkersRegisteredFetch = fetch;
+ Telemetry::ScalarSet(Telemetry::ScalarID::SERVICEWORKER_REGISTRATIONS,
+ u"All"_ns, gServiceWorkersRegistered);
+ Telemetry::ScalarSet(Telemetry::ScalarID::SERVICEWORKER_REGISTRATIONS,
+ u"Fetch"_ns, gServiceWorkersRegisteredFetch);
+ LOG(("LoadRegistrations: %u, fetch %u\n", gServiceWorkersRegistered,
+ gServiceWorkersRegisteredFetch));
+}
+
+void ServiceWorkerManager::StoreRegistration(
+ nsIPrincipal* aPrincipal, ServiceWorkerRegistrationInfo* aRegistration) {
+ MOZ_ASSERT(aPrincipal);
+ MOZ_ASSERT(aRegistration);
+
+ if (mShuttingDown) {
+ return;
+ }
+
+ // Do not store a registration for addons that are not installed, not enabled
+ // or installed temporarily.
+ //
+ // If the dom.serviceWorkers.testing.persistTemporaryInstalledAddons is set
+ // to true, the registration for a temporary installed addon will still be
+ // persisted (only meant to be used to make it easier to test some particular
+ // scenario with a temporary installed addon which doesn't need to be signed
+ // to be installed on release channel builds).
+ if (aPrincipal->SchemeIs("moz-extension")) {
+ RefPtr<extensions::WebExtensionPolicy> addonPolicy =
+ BasePrincipal::Cast(aPrincipal)->AddonPolicy();
+ if (!addonPolicy || !addonPolicy->Active() ||
+ (addonPolicy->TemporarilyInstalled() &&
+ !StaticPrefs::
+ dom_serviceWorkers_testing_persistTemporarilyInstalledAddons())) {
+ return;
+ }
+ }
+
+ ServiceWorkerRegistrationData data;
+ nsresult rv = PopulateRegistrationData(aPrincipal, aRegistration, data);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+
+ PrincipalInfo principalInfo;
+ if (NS_WARN_IF(
+ NS_FAILED(PrincipalToPrincipalInfo(aPrincipal, &principalInfo)))) {
+ return;
+ }
+
+ mActor->SendRegister(data);
+}
+
+already_AddRefed<ServiceWorkerRegistrationInfo>
+ServiceWorkerManager::GetServiceWorkerRegistrationInfo(
+ const ClientInfo& aClientInfo) const {
+ auto principalOrErr = aClientInfo.GetPrincipal();
+ if (NS_WARN_IF(principalOrErr.isErr())) {
+ return nullptr;
+ }
+
+ nsCOMPtr<nsIPrincipal> principal = principalOrErr.unwrap();
+ nsCOMPtr<nsIURI> uri;
+ nsresult rv = NS_NewURI(getter_AddRefs(uri), aClientInfo.URL());
+ NS_ENSURE_SUCCESS(rv, nullptr);
+
+ return GetServiceWorkerRegistrationInfo(principal, uri);
+}
+
+already_AddRefed<ServiceWorkerRegistrationInfo>
+ServiceWorkerManager::GetServiceWorkerRegistrationInfo(nsIPrincipal* aPrincipal,
+ nsIURI* aURI) const {
+ MOZ_ASSERT(aPrincipal);
+ MOZ_ASSERT(aURI);
+
+ nsAutoCString scopeKey;
+ nsresult rv = PrincipalToScopeKey(aPrincipal, scopeKey);
+ if (NS_FAILED(rv)) {
+ return nullptr;
+ }
+
+ return GetServiceWorkerRegistrationInfo(scopeKey, aURI);
+}
+
+already_AddRefed<ServiceWorkerRegistrationInfo>
+ServiceWorkerManager::GetServiceWorkerRegistrationInfo(
+ const nsACString& aScopeKey, nsIURI* aURI) const {
+ MOZ_ASSERT(aURI);
+
+ nsAutoCString spec;
+ nsresult rv = aURI->GetSpec(spec);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return nullptr;
+ }
+
+ nsAutoCString scope;
+ RegistrationDataPerPrincipal* data;
+ if (!FindScopeForPath(aScopeKey, spec, &data, scope)) {
+ return nullptr;
+ }
+
+ MOZ_ASSERT(data);
+
+ RefPtr<ServiceWorkerRegistrationInfo> registration;
+ data->mInfos.Get(scope, getter_AddRefs(registration));
+ // ordered scopes and registrations better be in sync.
+ MOZ_ASSERT(registration);
+
+#ifdef DEBUG
+ nsAutoCString origin;
+ rv = registration->Principal()->GetOrigin(origin);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+ MOZ_ASSERT(origin.Equals(aScopeKey));
+#endif
+
+ return registration.forget();
+}
+
+/* static */
+nsresult ServiceWorkerManager::PrincipalToScopeKey(nsIPrincipal* aPrincipal,
+ nsACString& aKey) {
+ MOZ_ASSERT(aPrincipal);
+
+ if (!BasePrincipal::Cast(aPrincipal)->IsContentPrincipal()) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsresult rv = aPrincipal->GetOrigin(aKey);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ return NS_OK;
+}
+
+/* static */
+nsresult ServiceWorkerManager::PrincipalInfoToScopeKey(
+ const PrincipalInfo& aPrincipalInfo, nsACString& aKey) {
+ if (aPrincipalInfo.type() != PrincipalInfo::TContentPrincipalInfo) {
+ return NS_ERROR_FAILURE;
+ }
+
+ auto content = aPrincipalInfo.get_ContentPrincipalInfo();
+
+ nsAutoCString suffix;
+ content.attrs().CreateSuffix(suffix);
+
+ aKey = content.originNoSuffix();
+ aKey.Append(suffix);
+
+ return NS_OK;
+}
+
+/* static */
+void ServiceWorkerManager::AddScopeAndRegistration(
+ const nsACString& aScope, ServiceWorkerRegistrationInfo* aInfo) {
+ MOZ_ASSERT(aInfo);
+ MOZ_ASSERT(aInfo->Principal());
+ MOZ_ASSERT(!aInfo->IsUnregistered());
+
+ RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance();
+ if (!swm) {
+ // browser shutdown
+ return;
+ }
+
+ nsAutoCString scopeKey;
+ nsresult rv = swm->PrincipalToScopeKey(aInfo->Principal(), scopeKey);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+
+ MOZ_ASSERT(!scopeKey.IsEmpty());
+
+ auto* const data = swm->mRegistrationInfos.GetOrInsertNew(scopeKey);
+ data->mScopeContainer.InsertScope(aScope);
+ data->mInfos.InsertOrUpdate(aScope, RefPtr{aInfo});
+ swm->NotifyListenersOnRegister(aInfo);
+}
+
+/* static */
+bool ServiceWorkerManager::FindScopeForPath(
+ const nsACString& aScopeKey, const nsACString& aPath,
+ RegistrationDataPerPrincipal** aData, nsACString& aMatch) {
+ MOZ_ASSERT(aData);
+
+ RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance();
+
+ if (!swm || !swm->mRegistrationInfos.Get(aScopeKey, aData)) {
+ return false;
+ }
+
+ Maybe<nsCString> scope = (*aData)->mScopeContainer.MatchScope(aPath);
+
+ if (scope) {
+ // scope.isSome() will still truen true after this; we are just moving the
+ // string inside the Maybe, so the Maybe will contain an empty string.
+ aMatch = std::move(*scope);
+ }
+
+ return scope.isSome();
+}
+
+/* static */
+bool ServiceWorkerManager::HasScope(nsIPrincipal* aPrincipal,
+ const nsACString& aScope) {
+ RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance();
+ if (!swm) {
+ return false;
+ }
+
+ nsAutoCString scopeKey;
+ nsresult rv = PrincipalToScopeKey(aPrincipal, scopeKey);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return false;
+ }
+
+ RegistrationDataPerPrincipal* data;
+ if (!swm->mRegistrationInfos.Get(scopeKey, &data)) {
+ return false;
+ }
+
+ return data->mScopeContainer.Contains(aScope);
+}
+
+/* static */
+void ServiceWorkerManager::RemoveScopeAndRegistration(
+ ServiceWorkerRegistrationInfo* aRegistration) {
+ RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance();
+ if (!swm) {
+ return;
+ }
+
+ nsAutoCString scopeKey;
+ nsresult rv = swm->PrincipalToScopeKey(aRegistration->Principal(), scopeKey);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+
+ RegistrationDataPerPrincipal* data;
+ if (!swm->mRegistrationInfos.Get(scopeKey, &data)) {
+ return;
+ }
+
+ if (auto entry = data->mUpdateTimers.Lookup(aRegistration->Scope())) {
+ entry.Data()->Cancel();
+ entry.Remove();
+ }
+
+ // Verify there are no controlled clients for the purged registration.
+ for (auto iter = swm->mControlledClients.Iter(); !iter.Done(); iter.Next()) {
+ auto& reg = iter.UserData()->mRegistrationInfo;
+ if (reg->Scope().Equals(aRegistration->Scope()) &&
+ reg->Principal()->Equals(aRegistration->Principal()) &&
+ reg->IsCorrupt()) {
+ iter.Remove();
+ }
+ }
+
+ RefPtr<ServiceWorkerRegistrationInfo> info;
+ data->mInfos.Remove(aRegistration->Scope(), getter_AddRefs(info));
+ aRegistration->SetUnregistered();
+ data->mScopeContainer.RemoveScope(aRegistration->Scope());
+ swm->NotifyListenersOnUnregister(info);
+
+ swm->MaybeRemoveRegistrationInfo(scopeKey);
+}
+
+void ServiceWorkerManager::MaybeRemoveRegistrationInfo(
+ const nsACString& aScopeKey) {
+ if (auto entry = mRegistrationInfos.Lookup(aScopeKey)) {
+ if (entry.Data()->mScopeContainer.IsEmpty() &&
+ entry.Data()->mJobQueues.Count() == 0) {
+ entry.Remove();
+
+ // Need to reset the mQuotaUsageCheckCount, if
+ // RegistrationDataPerPrincipal:: mScopeContainer is empty. This
+ // RegistrationDataPerPrincipal might be reused, such that quota usage
+ // mitigation can be triggered for the new added registration.
+ } else if (entry.Data()->mScopeContainer.IsEmpty() &&
+ entry.Data()->mQuotaUsageCheckCount) {
+ entry.Data()->mQuotaUsageCheckCount = 0;
+ }
+ }
+}
+
+bool ServiceWorkerManager::StartControlling(
+ const ClientInfo& aClientInfo,
+ const ServiceWorkerDescriptor& aServiceWorker) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ auto principalOrErr =
+ PrincipalInfoToPrincipal(aServiceWorker.PrincipalInfo());
+
+ if (NS_WARN_IF(principalOrErr.isErr())) {
+ return false;
+ }
+
+ nsCOMPtr<nsIPrincipal> principal = principalOrErr.unwrap();
+
+ nsCOMPtr<nsIURI> scope;
+ nsresult rv = NS_NewURI(getter_AddRefs(scope), aServiceWorker.Scope());
+ NS_ENSURE_SUCCESS(rv, false);
+
+ RefPtr<ServiceWorkerRegistrationInfo> registration =
+ GetServiceWorkerRegistrationInfo(principal, scope);
+ NS_ENSURE_TRUE(registration, false);
+ NS_ENSURE_TRUE(registration->GetActive(), false);
+
+ StartControllingClient(aClientInfo, registration);
+
+ return true;
+}
+
+void ServiceWorkerManager::MaybeCheckNavigationUpdate(
+ const ClientInfo& aClientInfo) {
+ MOZ_ASSERT(NS_IsMainThread());
+ // We perform these success path navigation update steps when the
+ // document tells us its more or less done loading. This avoids
+ // slowing down page load and also lets pages consistently get
+ // updatefound events when they fire.
+ //
+ // 9.8.20 If respondWithEntered is false, then:
+ // 9.8.22 Else: (respondWith was entered and succeeded)
+ // If request is a non-subresource request, then: Invoke Soft Update
+ // algorithm.
+ ControlledClientData* data = mControlledClients.Get(aClientInfo.Id());
+ if (data && data->mRegistrationInfo) {
+ data->mRegistrationInfo->MaybeScheduleUpdate();
+ }
+}
+
+void ServiceWorkerManager::StopControllingRegistration(
+ ServiceWorkerRegistrationInfo* aRegistration) {
+ aRegistration->StopControllingClient();
+ if (aRegistration->IsControllingClients()) {
+ return;
+ }
+
+ if (aRegistration->IsUnregistered()) {
+ if (aRegistration->IsIdle()) {
+ aRegistration->Clear();
+ } else {
+ aRegistration->ClearWhenIdle();
+ }
+ return;
+ }
+
+ // We use to aggressively terminate the worker at this point, but it
+ // caused problems. There are more uses for a service worker than actively
+ // controlled documents. We need to let the worker naturally terminate
+ // in case its handling push events, message events, etc.
+ aRegistration->TryToActivateAsync();
+}
+
+NS_IMETHODIMP
+ServiceWorkerManager::GetScopeForUrl(nsIPrincipal* aPrincipal,
+ const nsAString& aUrl, nsAString& aScope) {
+ MOZ_ASSERT(aPrincipal);
+
+ nsCOMPtr<nsIURI> uri;
+ nsresult rv = NS_NewURI(getter_AddRefs(uri), aUrl);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return NS_ERROR_FAILURE;
+ }
+
+ RefPtr<ServiceWorkerRegistrationInfo> r =
+ GetServiceWorkerRegistrationInfo(aPrincipal, uri);
+ if (!r) {
+ return NS_ERROR_FAILURE;
+ }
+
+ CopyUTF8toUTF16(r->Scope(), aScope);
+ return NS_OK;
+}
+
+namespace {
+
+class ContinueDispatchFetchEventRunnable : public Runnable {
+ RefPtr<ServiceWorkerPrivate> mServiceWorkerPrivate;
+ nsCOMPtr<nsIInterceptedChannel> mChannel;
+ nsCOMPtr<nsILoadGroup> mLoadGroup;
+
+ public:
+ ContinueDispatchFetchEventRunnable(
+ ServiceWorkerPrivate* aServiceWorkerPrivate,
+ nsIInterceptedChannel* aChannel, nsILoadGroup* aLoadGroup)
+ : Runnable(
+ "dom::ServiceWorkerManager::ContinueDispatchFetchEventRunnable"),
+ mServiceWorkerPrivate(aServiceWorkerPrivate),
+ mChannel(aChannel),
+ mLoadGroup(aLoadGroup) {
+ MOZ_ASSERT(aServiceWorkerPrivate);
+ MOZ_ASSERT(aChannel);
+ }
+
+ void HandleError() {
+ MOZ_ASSERT(NS_IsMainThread());
+ NS_WARNING("Unexpected error while dispatching fetch event!");
+ nsresult rv = mChannel->ResetInterception(false);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("Failed to resume intercepted network request");
+ mChannel->CancelInterception(rv);
+ }
+ }
+
+ NS_IMETHOD
+ Run() override {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ nsCOMPtr<nsIChannel> channel;
+ nsresult rv = mChannel->GetChannel(getter_AddRefs(channel));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ HandleError();
+ return NS_OK;
+ }
+
+ // The channel might have encountered an unexpected error while ensuring
+ // the upload stream is cloneable. Check here and reset the interception
+ // if that happens.
+ nsresult status;
+ rv = channel->GetStatus(&status);
+ if (NS_WARN_IF(NS_FAILED(rv) || NS_FAILED(status))) {
+ HandleError();
+ return NS_OK;
+ }
+
+ nsString clientId;
+ nsString resultingClientId;
+ nsCOMPtr<nsILoadInfo> loadInfo = channel->LoadInfo();
+ Maybe<ClientInfo> clientInfo = loadInfo->GetClientInfo();
+ if (clientInfo.isSome()) {
+ clientId = NSID_TrimBracketsUTF16(clientInfo->Id());
+ }
+
+ // Having an initial or reserved client are mutually exclusive events:
+ // either an initial client is used upon navigating an about:blank
+ // iframe, or a new, reserved environment/client is created (e.g.
+ // upon a top-level navigation). See step 4 of
+ // https://html.spec.whatwg.org/#process-a-navigate-fetch as well as
+ // https://github.com/w3c/ServiceWorker/issues/1228#issuecomment-345132444
+ Maybe<ClientInfo> resulting = loadInfo->GetInitialClientInfo();
+
+ if (resulting.isNothing()) {
+ resulting = loadInfo->GetReservedClientInfo();
+ } else {
+ MOZ_ASSERT(loadInfo->GetReservedClientInfo().isNothing());
+ }
+
+ if (resulting.isSome()) {
+ resultingClientId = NSID_TrimBracketsUTF16(resulting->Id());
+ }
+
+ rv = mServiceWorkerPrivate->SendFetchEvent(mChannel, mLoadGroup, clientId,
+ resultingClientId);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ HandleError();
+ }
+
+ return NS_OK;
+ }
+};
+
+} // anonymous namespace
+
+void ServiceWorkerManager::DispatchFetchEvent(nsIInterceptedChannel* aChannel,
+ ErrorResult& aRv) {
+ MOZ_ASSERT(aChannel);
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(XRE_IsParentProcess());
+
+ nsCOMPtr<nsIChannel> internalChannel;
+ aRv = aChannel->GetChannel(getter_AddRefs(internalChannel));
+ if (NS_WARN_IF(aRv.Failed())) {
+ return;
+ }
+
+ nsCOMPtr<nsILoadGroup> loadGroup;
+ aRv = internalChannel->GetLoadGroup(getter_AddRefs(loadGroup));
+ if (NS_WARN_IF(aRv.Failed())) {
+ return;
+ }
+
+ nsCOMPtr<nsILoadInfo> loadInfo = internalChannel->LoadInfo();
+ RefPtr<ServiceWorkerInfo> serviceWorker;
+
+ if (!nsContentUtils::IsNonSubresourceRequest(internalChannel)) {
+ const Maybe<ServiceWorkerDescriptor>& controller =
+ loadInfo->GetController();
+ if (NS_WARN_IF(controller.isNothing())) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return;
+ }
+
+ RefPtr<ServiceWorkerRegistrationInfo> registration;
+ nsresult rv = GetClientRegistration(loadInfo->GetClientInfo().ref(),
+ getter_AddRefs(registration));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ aRv.Throw(rv);
+ return;
+ }
+
+ serviceWorker = registration->GetActive();
+ if (NS_WARN_IF(!serviceWorker) ||
+ NS_WARN_IF(serviceWorker->Descriptor().Id() != controller.ref().Id())) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return;
+ }
+ } else {
+ nsCOMPtr<nsIURI> uri;
+ aRv = aChannel->GetSecureUpgradedChannelURI(getter_AddRefs(uri));
+ if (NS_WARN_IF(aRv.Failed())) {
+ return;
+ }
+
+ // non-subresource request means the URI contains the principal
+ OriginAttributes attrs = loadInfo->GetOriginAttributes();
+ if (StaticPrefs::privacy_partition_serviceWorkers()) {
+ StoragePrincipalHelper::GetOriginAttributes(
+ internalChannel, attrs,
+ StoragePrincipalHelper::eForeignPartitionedPrincipal);
+ }
+
+ nsCOMPtr<nsIPrincipal> principal =
+ BasePrincipal::CreateContentPrincipal(uri, attrs);
+
+ RefPtr<ServiceWorkerRegistrationInfo> registration =
+ GetServiceWorkerRegistrationInfo(principal, uri);
+ if (NS_WARN_IF(!registration)) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return;
+ }
+
+ // While we only enter this method if IsAvailable() previously saw
+ // an active worker, it is possible for that worker to be removed
+ // before we get to this point. Therefore we must handle a nullptr
+ // active worker here.
+ serviceWorker = registration->GetActive();
+ if (NS_WARN_IF(!serviceWorker)) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return;
+ }
+
+ // If there is a reserved client it should be marked as controlled before
+ // the FetchEvent is dispatched.
+ Maybe<ClientInfo> clientInfo = loadInfo->GetReservedClientInfo();
+
+ // Also override the initial about:blank controller since the real
+ // network load may be intercepted by a different service worker. If
+ // the intial about:blank has a controller here its simply been
+ // inherited from its parent.
+ if (clientInfo.isNothing()) {
+ clientInfo = loadInfo->GetInitialClientInfo();
+
+ // TODO: We need to handle the case where the initial about:blank is
+ // controlled, but the final document load is not. Right now
+ // the spec does not really say what to do. There currently
+ // is no way for the controller to be cleared from a client in
+ // the spec or our implementation. We may want to force a
+ // new inner window to be created instead of reusing the
+ // initial about:blank global. See bug 1419620 and the spec
+ // issue here: https://github.com/w3c/ServiceWorker/issues/1232
+ }
+
+ if (clientInfo.isSome()) {
+ // ClientChannelHelper is not called for STS upgrades that get
+ // intercepted by a service worker when interception occurs in
+ // the content process. Therefore the reserved client is not
+ // properly cleared in that case leading to a situation where
+ // a ClientSource with an http:// principal is controlled by
+ // a ServiceWorker with an https:// principal.
+ //
+ // This does not occur when interception is handled by the
+ // simpler InterceptedHttpChannel approach in the parent.
+ //
+ // As a temporary work around check for this principal mismatch
+ // here and perform the ClientChannelHelper's replacement of
+ // reserved client automatically.
+ if (!XRE_IsParentProcess()) {
+ auto clientPrincipalOrErr = clientInfo.ref().GetPrincipal();
+
+ nsCOMPtr<nsIPrincipal> clientPrincipal;
+ if (clientPrincipalOrErr.isOk()) {
+ clientPrincipal = clientPrincipalOrErr.unwrap();
+ }
+
+ if (!clientPrincipal || !clientPrincipal->Equals(principal)) {
+ UniquePtr<ClientSource> reservedClient =
+ loadInfo->TakeReservedClientSource();
+
+ nsCOMPtr<nsISerialEventTarget> target =
+ reservedClient ? reservedClient->EventTarget()
+ : GetMainThreadSerialEventTarget();
+
+ reservedClient.reset();
+ reservedClient = ClientManager::CreateSource(ClientType::Window,
+ target, principal);
+
+ loadInfo->GiveReservedClientSource(std::move(reservedClient));
+
+ clientInfo = loadInfo->GetReservedClientInfo();
+ }
+ }
+
+ // First, attempt to mark the reserved client controlled directly. This
+ // will update the controlled status in the ClientManagerService in the
+ // parent. It will also eventually propagate back to the ClientSource.
+ StartControllingClient(clientInfo.ref(), registration);
+ }
+
+ uint32_t redirectMode = nsIHttpChannelInternal::REDIRECT_MODE_MANUAL;
+ nsCOMPtr<nsIHttpChannelInternal> http = do_QueryInterface(internalChannel);
+ MOZ_ALWAYS_SUCCEEDS(http->GetRedirectMode(&redirectMode));
+
+ // Synthetic redirects for non-subresource requests with a "follow"
+ // redirect mode may switch controllers. This is basically worker
+ // scripts right now. In this case we need to explicitly clear the
+ // controller to avoid assertions on the SetController() below.
+ if (redirectMode == nsIHttpChannelInternal::REDIRECT_MODE_FOLLOW) {
+ loadInfo->ClearController();
+ }
+
+ // But we also note the reserved state on the LoadInfo. This allows the
+ // ClientSource to be updated immediately after the nsIChannel starts.
+ // This is necessary to have the correct controller in place for immediate
+ // follow-on requests.
+ loadInfo->SetController(serviceWorker->Descriptor());
+ }
+
+ MOZ_DIAGNOSTIC_ASSERT(serviceWorker);
+
+ RefPtr<ContinueDispatchFetchEventRunnable> continueRunnable =
+ new ContinueDispatchFetchEventRunnable(serviceWorker->WorkerPrivate(),
+ aChannel, loadGroup);
+
+ // When this service worker was registered, we also sent down the permissions
+ // for the runnable. They should have arrived by now, but we still need to
+ // wait for them if they have not.
+ RefPtr<PermissionManager> permMgr = PermissionManager::GetInstance();
+ if (permMgr) {
+ permMgr->WhenPermissionsAvailable(serviceWorker->Principal(),
+ continueRunnable);
+ } else {
+ continueRunnable->HandleError();
+ }
+}
+
+bool ServiceWorkerManager::IsAvailable(nsIPrincipal* aPrincipal, nsIURI* aURI,
+ nsIChannel* aChannel) {
+ MOZ_ASSERT(aPrincipal);
+ MOZ_ASSERT(aURI);
+ MOZ_ASSERT(aChannel);
+
+ RefPtr<ServiceWorkerRegistrationInfo> registration =
+ GetServiceWorkerRegistrationInfo(aPrincipal, aURI);
+
+ if (!registration || !registration->GetActive()) {
+ return false;
+ }
+
+ // Checking if the matched service worker handles fetch events or not.
+ // If it does, directly return true and handle the client controlling logic
+ // in DispatchFetchEvent(). otherwise, do followings then return false.
+ // 1. Set the matched service worker as the controller of LoadInfo and
+ // correspoinding ClinetInfo
+ // 2. Maybe schedule a soft update
+ if (!registration->GetActive()->HandlesFetch()) {
+ // Checkin if the channel is not allowed for the service worker.
+ auto storageAccess = StorageAllowedForChannel(aChannel);
+ nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo();
+
+ if (storageAccess != StorageAccess::eAllow) {
+ if (!StaticPrefs::privacy_partition_serviceWorkers()) {
+ return false;
+ }
+
+ nsCOMPtr<nsICookieJarSettings> cookieJarSettings;
+ loadInfo->GetCookieJarSettings(getter_AddRefs(cookieJarSettings));
+
+ if (!StoragePartitioningEnabled(storageAccess, cookieJarSettings)) {
+ return false;
+ }
+ }
+
+ // ServiceWorkerInterceptController::ShouldPrepareForIntercept() handles the
+ // subresource cases. Must be non-subresource case here.
+ MOZ_ASSERT(nsContentUtils::IsNonSubresourceRequest(aChannel));
+
+ Maybe<ClientInfo> clientInfo = loadInfo->GetReservedClientInfo();
+ if (clientInfo.isNothing()) {
+ clientInfo = loadInfo->GetInitialClientInfo();
+ }
+
+ if (clientInfo.isSome()) {
+ StartControllingClient(clientInfo.ref(), registration);
+ }
+
+ uint32_t redirectMode = nsIHttpChannelInternal::REDIRECT_MODE_MANUAL;
+ nsCOMPtr<nsIHttpChannelInternal> http = do_QueryInterface(aChannel);
+ MOZ_ALWAYS_SUCCEEDS(http->GetRedirectMode(&redirectMode));
+
+ // Synthetic redirects for non-subresource requests with a "follow"
+ // redirect mode may switch controllers. This is basically worker
+ // scripts right now. In this case we need to explicitly clear the
+ // controller to avoid assertions on the SetController() below.
+ if (redirectMode == nsIHttpChannelInternal::REDIRECT_MODE_FOLLOW) {
+ loadInfo->ClearController();
+ }
+
+ loadInfo->SetController(registration->GetActive()->Descriptor());
+
+ // https://w3c.github.io/ServiceWorker/#on-fetch-request-algorithm 17.1
+ // try schedule a soft-update for non-subresource case.
+ registration->MaybeScheduleUpdate();
+ return false;
+ }
+ // Found a matching service worker which handles fetch events, return true.
+ return true;
+}
+
+nsresult ServiceWorkerManager::GetClientRegistration(
+ const ClientInfo& aClientInfo,
+ ServiceWorkerRegistrationInfo** aRegistrationInfo) {
+ ControlledClientData* data = mControlledClients.Get(aClientInfo.Id());
+ if (!data || !data->mRegistrationInfo) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ // If the document is controlled, the current worker MUST be non-null.
+ if (!data->mRegistrationInfo->GetActive()) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ RefPtr<ServiceWorkerRegistrationInfo> ref = data->mRegistrationInfo;
+ ref.forget(aRegistrationInfo);
+ return NS_OK;
+}
+
+int32_t ServiceWorkerManager::GetPrincipalQuotaUsageCheckCount(
+ nsIPrincipal* aPrincipal) {
+ nsAutoCString scopeKey;
+ nsresult rv = PrincipalToScopeKey(aPrincipal, scopeKey);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return -1;
+ }
+
+ RegistrationDataPerPrincipal* data;
+ if (!mRegistrationInfos.Get(scopeKey, &data)) {
+ return -1;
+ }
+
+ return data->mQuotaUsageCheckCount;
+}
+
+void ServiceWorkerManager::CheckPrincipalQuotaUsage(nsIPrincipal* aPrincipal,
+ const nsACString& aScope) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aPrincipal);
+
+ nsAutoCString scopeKey;
+ nsresult rv = PrincipalToScopeKey(aPrincipal, scopeKey);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+
+ RegistrationDataPerPrincipal* data;
+ if (!mRegistrationInfos.Get(scopeKey, &data)) {
+ return;
+ }
+
+ // Had already schedule a quota usage check.
+ if (data->mQuotaUsageCheckCount != 0) {
+ return;
+ }
+
+ ++data->mQuotaUsageCheckCount;
+
+ // Get the corresponding ServiceWorkerRegistrationInfo here. Unregisteration
+ // might be triggered later, should get it here before it be removed from
+ // data.mInfos, such that NotifyListenersOnQuotaCheckFinish() can notify the
+ // corresponding ServiceWorkerRegistrationInfo after asynchronous quota
+ // checking finish.
+ RefPtr<ServiceWorkerRegistrationInfo> info;
+ data->mInfos.Get(aScope, getter_AddRefs(info));
+ MOZ_ASSERT(info);
+
+ RefPtr<ServiceWorkerManager> self = this;
+
+ ClearQuotaUsageIfNeeded(aPrincipal, [self, info](bool aResult) {
+ MOZ_ASSERT(NS_IsMainThread());
+ self->NotifyListenersOnQuotaUsageCheckFinish(info);
+ });
+}
+
+void ServiceWorkerManager::SoftUpdate(const OriginAttributes& aOriginAttributes,
+ const nsACString& aScope) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (mShuttingDown) {
+ return;
+ }
+
+ SoftUpdateInternal(aOriginAttributes, aScope, nullptr);
+}
+
+namespace {
+
+class UpdateJobCallback final : public ServiceWorkerJob::Callback {
+ RefPtr<ServiceWorkerUpdateFinishCallback> mCallback;
+
+ ~UpdateJobCallback() { MOZ_ASSERT(!mCallback); }
+
+ public:
+ explicit UpdateJobCallback(ServiceWorkerUpdateFinishCallback* aCallback)
+ : mCallback(aCallback) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(mCallback);
+ }
+
+ void JobFinished(ServiceWorkerJob* aJob, ErrorResult& aStatus) override {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aJob);
+ MOZ_ASSERT(mCallback);
+
+ auto scopeExit = MakeScopeExit([&]() { mCallback = nullptr; });
+
+ if (aStatus.Failed()) {
+ mCallback->UpdateFailed(aStatus);
+ return;
+ }
+
+ MOZ_DIAGNOSTIC_ASSERT(aJob->GetType() == ServiceWorkerJob::Type::Update);
+ RefPtr<ServiceWorkerUpdateJob> updateJob =
+ static_cast<ServiceWorkerUpdateJob*>(aJob);
+ RefPtr<ServiceWorkerRegistrationInfo> reg = updateJob->GetRegistration();
+ mCallback->UpdateSucceeded(reg);
+ }
+
+ void JobDiscarded(ErrorResult& aStatus) override {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(mCallback);
+
+ mCallback->UpdateFailed(aStatus);
+ mCallback = nullptr;
+ }
+
+ NS_INLINE_DECL_REFCOUNTING(UpdateJobCallback, override)
+};
+
+} // anonymous namespace
+
+void ServiceWorkerManager::SoftUpdateInternal(
+ const OriginAttributes& aOriginAttributes, const nsACString& aScope,
+ ServiceWorkerUpdateFinishCallback* aCallback) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (mShuttingDown) {
+ return;
+ }
+
+ auto result = ScopeToPrincipal(aScope, aOriginAttributes);
+ if (NS_WARN_IF(result.isErr())) {
+ return;
+ }
+
+ auto principal = result.unwrap();
+
+ nsAutoCString scopeKey;
+ nsresult rv = PrincipalToScopeKey(principal, scopeKey);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+
+ RefPtr<ServiceWorkerRegistrationInfo> registration =
+ GetRegistration(scopeKey, aScope);
+ if (NS_WARN_IF(!registration)) {
+ return;
+ }
+
+ // "If registration's installing worker is not null, abort these steps."
+ if (registration->GetInstalling()) {
+ return;
+ }
+
+ // "Let newestWorker be the result of running Get Newest Worker algorithm
+ // passing registration as its argument.
+ // If newestWorker is null, abort these steps."
+ RefPtr<ServiceWorkerInfo> newest = registration->Newest();
+ if (!newest) {
+ return;
+ }
+
+ // "If the registration queue for registration is empty, invoke Update
+ // algorithm, or its equivalent, with client, registration as its argument."
+ // TODO(catalinb): We don't implement the force bypass cache flag.
+ // See: https://github.com/slightlyoff/ServiceWorker/issues/759
+ RefPtr<ServiceWorkerJobQueue> queue = GetOrCreateJobQueue(scopeKey, aScope);
+
+ RefPtr<ServiceWorkerUpdateJob> job = new ServiceWorkerUpdateJob(
+ principal, registration->Scope(), newest->ScriptSpec(),
+ registration->GetUpdateViaCache());
+
+ if (aCallback) {
+ RefPtr<UpdateJobCallback> cb = new UpdateJobCallback(aCallback);
+ job->AppendResultCallback(cb);
+ }
+
+ queue->ScheduleJob(job);
+}
+
+void ServiceWorkerManager::Update(
+ nsIPrincipal* aPrincipal, const nsACString& aScope,
+ nsCString aNewestWorkerScriptUrl,
+ ServiceWorkerUpdateFinishCallback* aCallback) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(!aNewestWorkerScriptUrl.IsEmpty());
+
+ UpdateInternal(aPrincipal, aScope, std::move(aNewestWorkerScriptUrl),
+ aCallback);
+}
+
+void ServiceWorkerManager::UpdateInternal(
+ nsIPrincipal* aPrincipal, const nsACString& aScope,
+ nsCString&& aNewestWorkerScriptUrl,
+ ServiceWorkerUpdateFinishCallback* aCallback) {
+ MOZ_ASSERT(aPrincipal);
+ MOZ_ASSERT(aCallback);
+ MOZ_ASSERT(!aNewestWorkerScriptUrl.IsEmpty());
+
+ nsAutoCString scopeKey;
+ nsresult rv = PrincipalToScopeKey(aPrincipal, scopeKey);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+
+ RefPtr<ServiceWorkerRegistrationInfo> registration =
+ GetRegistration(scopeKey, aScope);
+ if (NS_WARN_IF(!registration)) {
+ ErrorResult error;
+ error.ThrowTypeError<MSG_SW_UPDATE_BAD_REGISTRATION>(aScope, "uninstalled");
+ aCallback->UpdateFailed(error);
+
+ // In case the callback does not consume the exception
+ error.SuppressException();
+ return;
+ }
+
+ RefPtr<ServiceWorkerJobQueue> queue = GetOrCreateJobQueue(scopeKey, aScope);
+
+ // "Let job be the result of running Create Job with update, registration’s
+ // scope url, newestWorker’s script url, promise, and the context object’s
+ // relevant settings object."
+ RefPtr<ServiceWorkerUpdateJob> job = new ServiceWorkerUpdateJob(
+ aPrincipal, registration->Scope(), std::move(aNewestWorkerScriptUrl),
+ registration->GetUpdateViaCache());
+
+ RefPtr<UpdateJobCallback> cb = new UpdateJobCallback(aCallback);
+ job->AppendResultCallback(cb);
+
+ // "Invoke Schedule Job with job."
+ queue->ScheduleJob(job);
+}
+
+RefPtr<GenericErrorResultPromise> ServiceWorkerManager::MaybeClaimClient(
+ const ClientInfo& aClientInfo,
+ ServiceWorkerRegistrationInfo* aWorkerRegistration) {
+ MOZ_DIAGNOSTIC_ASSERT(aWorkerRegistration);
+
+ if (!aWorkerRegistration->GetActive()) {
+ CopyableErrorResult rv;
+ rv.ThrowInvalidStateError("Worker is not active");
+ return GenericErrorResultPromise::CreateAndReject(rv, __func__);
+ }
+
+ // Same origin check
+ auto principalOrErr = aClientInfo.GetPrincipal();
+
+ if (NS_WARN_IF(principalOrErr.isErr())) {
+ CopyableErrorResult rv;
+ rv.ThrowSecurityError("Could not extract client's principal");
+ return GenericErrorResultPromise::CreateAndReject(rv, __func__);
+ }
+
+ nsCOMPtr<nsIPrincipal> principal = principalOrErr.unwrap();
+ if (!aWorkerRegistration->Principal()->Equals(principal)) {
+ CopyableErrorResult rv;
+ rv.ThrowSecurityError("Worker is for a different origin");
+ return GenericErrorResultPromise::CreateAndReject(rv, __func__);
+ }
+
+ // The registration that should be controlling the client
+ RefPtr<ServiceWorkerRegistrationInfo> matchingRegistration =
+ GetServiceWorkerRegistrationInfo(aClientInfo);
+
+ // The registration currently controlling the client
+ RefPtr<ServiceWorkerRegistrationInfo> controllingRegistration;
+ GetClientRegistration(aClientInfo, getter_AddRefs(controllingRegistration));
+
+ if (aWorkerRegistration != matchingRegistration ||
+ aWorkerRegistration == controllingRegistration) {
+ return GenericErrorResultPromise::CreateAndResolve(true, __func__);
+ }
+
+ return StartControllingClient(aClientInfo, aWorkerRegistration);
+}
+
+RefPtr<GenericErrorResultPromise> ServiceWorkerManager::MaybeClaimClient(
+ const ClientInfo& aClientInfo,
+ const ServiceWorkerDescriptor& aServiceWorker) {
+ auto principalOrErr = aServiceWorker.GetPrincipal();
+ if (NS_WARN_IF(principalOrErr.isErr())) {
+ return GenericErrorResultPromise::CreateAndResolve(false, __func__);
+ }
+
+ nsCOMPtr<nsIPrincipal> principal = principalOrErr.unwrap();
+
+ RefPtr<ServiceWorkerRegistrationInfo> registration =
+ GetRegistration(principal, aServiceWorker.Scope());
+
+ // While ServiceWorkerManager is distributed across child processes its
+ // possible for us to sometimes get a claim for a new worker that has
+ // not propagated to this process yet. For now, simply note that we
+ // are done. The fix for this is to move the SWM to the parent process
+ // so there are no consistency errors.
+ if (NS_WARN_IF(!registration) || NS_WARN_IF(!registration->GetActive())) {
+ return GenericErrorResultPromise::CreateAndResolve(false, __func__);
+ }
+
+ return MaybeClaimClient(aClientInfo, registration);
+}
+
+void ServiceWorkerManager::UpdateClientControllers(
+ ServiceWorkerRegistrationInfo* aRegistration) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ RefPtr<ServiceWorkerInfo> activeWorker = aRegistration->GetActive();
+ MOZ_DIAGNOSTIC_ASSERT(activeWorker);
+
+ AutoTArray<RefPtr<ClientHandle>, 16> handleList;
+ for (const auto& client : mControlledClients.Values()) {
+ if (client->mRegistrationInfo != aRegistration) {
+ continue;
+ }
+
+ handleList.AppendElement(client->mClientHandle);
+ }
+
+ // Fire event after iterating mControlledClients is done to prevent
+ // modification by reentering from the event handlers during iteration.
+ for (auto& handle : handleList) {
+ RefPtr<GenericErrorResultPromise> p =
+ handle->Control(activeWorker->Descriptor());
+
+ RefPtr<ServiceWorkerManager> self = this;
+
+ // If we fail to control the client, then automatically remove it
+ // from our list of controlled clients.
+ p->Then(
+ GetMainThreadSerialEventTarget(), __func__,
+ [](bool) {
+ // do nothing on success
+ },
+ [self, clientInfo = handle->Info()](const CopyableErrorResult& aRv) {
+ // failed to control, forget about this client
+ self->StopControllingClient(clientInfo);
+ });
+ }
+}
+
+void ServiceWorkerManager::EvictFromBFCache(
+ ServiceWorkerRegistrationInfo* aRegistration) {
+ MOZ_ASSERT(NS_IsMainThread());
+ for (const auto& client : mControlledClients.Values()) {
+ if (client->mRegistrationInfo == aRegistration) {
+ client->mClientHandle->EvictFromBFCache();
+ }
+ }
+}
+
+already_AddRefed<ServiceWorkerRegistrationInfo>
+ServiceWorkerManager::GetRegistration(nsIPrincipal* aPrincipal,
+ const nsACString& aScope) const {
+ MOZ_ASSERT(aPrincipal);
+
+ nsAutoCString scopeKey;
+ nsresult rv = PrincipalToScopeKey(aPrincipal, scopeKey);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return nullptr;
+ }
+
+ return GetRegistration(scopeKey, aScope);
+}
+
+already_AddRefed<ServiceWorkerRegistrationInfo>
+ServiceWorkerManager::GetRegistration(const PrincipalInfo& aPrincipalInfo,
+ const nsACString& aScope) const {
+ nsAutoCString scopeKey;
+ nsresult rv = PrincipalInfoToScopeKey(aPrincipalInfo, scopeKey);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return nullptr;
+ }
+
+ return GetRegistration(scopeKey, aScope);
+}
+
+NS_IMETHODIMP
+ServiceWorkerManager::ReloadRegistrationsForTest() {
+ if (NS_WARN_IF(!StaticPrefs::dom_serviceWorkers_testing_enabled())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Let's keep it simple and fail if there are any controlled client,
+ // the test case can take care of making sure there is none when this
+ // method will be called.
+ if (NS_WARN_IF(!mControlledClients.IsEmpty())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ for (const auto& info : mRegistrationInfos.Values()) {
+ for (ServiceWorkerRegistrationInfo* reg : info->mInfos.Values()) {
+ MOZ_ASSERT(reg);
+ reg->ForceShutdown();
+ }
+ }
+
+ mRegistrationInfos.Clear();
+
+ nsTArray<ServiceWorkerRegistrationData> data;
+ RefPtr<ServiceWorkerRegistrar> swr = ServiceWorkerRegistrar::Get();
+ if (NS_WARN_IF(!swr->ReloadDataForTest())) {
+ return NS_ERROR_FAILURE;
+ }
+ swr->GetRegistrations(data);
+ LoadRegistrations(data);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+ServiceWorkerManager::RegisterForAddonPrincipal(nsIPrincipal* aPrincipal,
+ JSContext* aCx,
+ dom::Promise** aPromise) {
+ nsIGlobalObject* global = xpc::CurrentNativeGlobal(aCx);
+ if (NS_WARN_IF(!global)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ ErrorResult erv;
+ RefPtr<Promise> outer = Promise::Create(global, erv);
+ if (NS_WARN_IF(erv.Failed())) {
+ return erv.StealNSResult();
+ }
+
+ auto enabled =
+ StaticPrefs::extensions_backgroundServiceWorker_enabled_AtStartup();
+ if (!enabled) {
+ outer->MaybeRejectWithNotAllowedError(
+ "Disabled. extensions.backgroundServiceWorker.enabled is false");
+ outer.forget(aPromise);
+ return NS_OK;
+ }
+
+ MOZ_ASSERT(aPrincipal);
+ auto* addonPolicy = BasePrincipal::Cast(aPrincipal)->AddonPolicy();
+ if (!addonPolicy) {
+ outer->MaybeRejectWithNotAllowedError("Not an extension principal");
+ outer.forget(aPromise);
+ return NS_OK;
+ }
+
+ nsCString scope;
+ auto result = addonPolicy->GetURL(u""_ns);
+ if (result.isOk()) {
+ scope.Assign(NS_ConvertUTF16toUTF8(result.unwrap()));
+ } else {
+ outer->MaybeRejectWithUnknownError("Unable to resolve addon scope URL");
+ outer.forget(aPromise);
+ return NS_OK;
+ }
+
+ nsString scriptURL;
+ addonPolicy->GetBackgroundWorker(scriptURL);
+
+ if (scriptURL.IsEmpty()) {
+ outer->MaybeRejectWithNotFoundError("Missing background worker script url");
+ outer.forget(aPromise);
+ return NS_OK;
+ }
+
+ Maybe<ClientInfo> clientInfo =
+ dom::ClientManager::CreateInfo(ClientType::All, aPrincipal);
+
+ if (!clientInfo.isSome()) {
+ outer->MaybeRejectWithUnknownError("Error creating clientInfo");
+ outer.forget(aPromise);
+ return NS_OK;
+ }
+
+ auto regPromise =
+ Register(clientInfo.ref(), scope, NS_ConvertUTF16toUTF8(scriptURL),
+ dom::ServiceWorkerUpdateViaCache::Imports);
+ const RefPtr<ServiceWorkerManager> self(this);
+ const nsCOMPtr<nsIPrincipal> principal(aPrincipal);
+ regPromise->Then(
+ GetMainThreadSerialEventTarget(), __func__,
+ [self, outer, principal,
+ scope](const ServiceWorkerRegistrationDescriptor& regDesc) {
+ RefPtr<ServiceWorkerRegistrationInfo> registration =
+ self->GetRegistration(principal, scope);
+ if (registration) {
+ outer->MaybeResolve(registration);
+ } else {
+ outer->MaybeRejectWithUnknownError(
+ "Failed to retrieve ServiceWorkerRegistrationInfo");
+ }
+ },
+ [outer](const mozilla::CopyableErrorResult& err) {
+ CopyableErrorResult result(err);
+ outer->MaybeReject(std::move(result));
+ });
+
+ outer.forget(aPromise);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+ServiceWorkerManager::GetRegistrationForAddonPrincipal(
+ nsIPrincipal* aPrincipal, nsIServiceWorkerRegistrationInfo** aInfo) {
+ MOZ_ASSERT(aPrincipal);
+
+ MOZ_ASSERT(aPrincipal);
+ auto* addonPolicy = BasePrincipal::Cast(aPrincipal)->AddonPolicy();
+ if (!addonPolicy) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsCString scope;
+ auto result = addonPolicy->GetURL(u""_ns);
+ if (result.isOk()) {
+ scope.Assign(NS_ConvertUTF16toUTF8(result.unwrap()));
+ } else {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsCOMPtr<nsIURI> scopeURI;
+ nsresult rv = NS_NewURI(getter_AddRefs(scopeURI), scope);
+ if (NS_FAILED(rv)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ RefPtr<ServiceWorkerRegistrationInfo> info =
+ GetServiceWorkerRegistrationInfo(aPrincipal, scopeURI);
+ if (!info) {
+ aInfo = nullptr;
+ return NS_OK;
+ }
+ info.forget(aInfo);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+ServiceWorkerManager::WakeForExtensionAPIEvent(
+ const nsAString& aExtensionBaseURL, const nsAString& aAPINamespace,
+ const nsAString& aAPIEventName, JSContext* aCx, dom::Promise** aPromise) {
+ nsIGlobalObject* global = xpc::CurrentNativeGlobal(aCx);
+ if (NS_WARN_IF(!global)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ ErrorResult erv;
+ RefPtr<Promise> outer = Promise::Create(global, erv);
+ if (NS_WARN_IF(erv.Failed())) {
+ return erv.StealNSResult();
+ }
+
+ auto enabled =
+ StaticPrefs::extensions_backgroundServiceWorker_enabled_AtStartup();
+ if (!enabled) {
+ outer->MaybeRejectWithNotAllowedError(
+ "Disabled. extensions.backgroundServiceWorker.enabled is false");
+ outer.forget(aPromise);
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIURI> scopeURI;
+ nsresult rv = NS_NewURI(getter_AddRefs(scopeURI), aExtensionBaseURL);
+ if (NS_FAILED(rv)) {
+ outer->MaybeReject(rv);
+ outer.forget(aPromise);
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIPrincipal> principal;
+ MOZ_TRY_VAR(principal, ScopeToPrincipal(scopeURI, {}));
+
+ auto* addonPolicy = BasePrincipal::Cast(principal)->AddonPolicy();
+ if (NS_WARN_IF(!addonPolicy)) {
+ outer->MaybeRejectWithNotAllowedError(
+ "Not an extension principal or extension disabled");
+ outer.forget(aPromise);
+ return NS_OK;
+ }
+
+ OriginAttributes attrs;
+ ServiceWorkerInfo* info = GetActiveWorkerInfoForScope(
+ attrs, NS_ConvertUTF16toUTF8(aExtensionBaseURL));
+ if (NS_WARN_IF(!info)) {
+ outer->MaybeRejectWithInvalidStateError(
+ "No active worker for the extension background service worker");
+ outer.forget(aPromise);
+ return NS_OK;
+ }
+
+ ServiceWorkerPrivate* workerPrivate = info->WorkerPrivate();
+ auto result =
+ workerPrivate->WakeForExtensionAPIEvent(aAPINamespace, aAPIEventName);
+ if (result.isErr()) {
+ outer->MaybeReject(result.propagateErr());
+ outer.forget(aPromise);
+ return NS_OK;
+ }
+
+ RefPtr<ServiceWorkerPrivate::PromiseExtensionWorkerHasListener> innerPromise =
+ result.unwrap();
+
+ innerPromise->Then(
+ GetMainThreadSerialEventTarget(), __func__,
+ [outer](bool aSubscribedEvent) { outer->MaybeResolve(aSubscribedEvent); },
+ [outer](nsresult aErrorResult) { outer->MaybeReject(aErrorResult); });
+
+ outer.forget(aPromise);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+ServiceWorkerManager::GetRegistrationByPrincipal(
+ nsIPrincipal* aPrincipal, const nsAString& aScope,
+ nsIServiceWorkerRegistrationInfo** aInfo) {
+ MOZ_ASSERT(aPrincipal);
+ MOZ_ASSERT(aInfo);
+
+ nsCOMPtr<nsIURI> scopeURI;
+ nsresult rv = NS_NewURI(getter_AddRefs(scopeURI), aScope);
+ if (NS_FAILED(rv)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ RefPtr<ServiceWorkerRegistrationInfo> info =
+ GetServiceWorkerRegistrationInfo(aPrincipal, scopeURI);
+ if (!info) {
+ return NS_ERROR_FAILURE;
+ }
+ info.forget(aInfo);
+
+ return NS_OK;
+}
+
+already_AddRefed<ServiceWorkerRegistrationInfo>
+ServiceWorkerManager::GetRegistration(const nsACString& aScopeKey,
+ const nsACString& aScope) const {
+ RefPtr<ServiceWorkerRegistrationInfo> reg;
+
+ RegistrationDataPerPrincipal* data;
+ if (!mRegistrationInfos.Get(aScopeKey, &data)) {
+ return reg.forget();
+ }
+
+ data->mInfos.Get(aScope, getter_AddRefs(reg));
+ return reg.forget();
+}
+
+already_AddRefed<ServiceWorkerRegistrationInfo>
+ServiceWorkerManager::CreateNewRegistration(
+ const nsCString& aScope, nsIPrincipal* aPrincipal,
+ ServiceWorkerUpdateViaCache aUpdateViaCache,
+ IPCNavigationPreloadState aNavigationPreloadState) {
+#ifdef DEBUG
+ MOZ_ASSERT(NS_IsMainThread());
+ nsCOMPtr<nsIURI> scopeURI;
+ nsresult rv = NS_NewURI(getter_AddRefs(scopeURI), aScope);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+
+ RefPtr<ServiceWorkerRegistrationInfo> tmp =
+ GetRegistration(aPrincipal, aScope);
+ MOZ_ASSERT(!tmp);
+#endif
+
+ RefPtr<ServiceWorkerRegistrationInfo> registration =
+ new ServiceWorkerRegistrationInfo(aScope, aPrincipal, aUpdateViaCache,
+ std::move(aNavigationPreloadState));
+
+ // From now on ownership of registration is with
+ // mServiceWorkerRegistrationInfos.
+ AddScopeAndRegistration(aScope, registration);
+ return registration.forget();
+}
+
+void ServiceWorkerManager::MaybeRemoveRegistration(
+ ServiceWorkerRegistrationInfo* aRegistration) {
+ MOZ_ASSERT(aRegistration);
+ RefPtr<ServiceWorkerInfo> newest = aRegistration->Newest();
+ if (!newest && HasScope(aRegistration->Principal(), aRegistration->Scope())) {
+ RemoveRegistration(aRegistration);
+ }
+}
+
+void ServiceWorkerManager::RemoveRegistration(
+ ServiceWorkerRegistrationInfo* aRegistration) {
+ // Note, we do not need to call mActor->SendUnregister() here. There are a
+ // few ways we can get here: 1) Through a normal unregister which calls
+ // SendUnregister() in the
+ // unregister job Start() method.
+ // 2) Through origin storage being purged. These result in ForceUnregister()
+ // starting unregister jobs which in turn call SendUnregister().
+ // 3) Through the failure to install a new service worker. Since we don't
+ // store the registration until install succeeds, we do not need to call
+ // SendUnregister here.
+ MOZ_ASSERT(HasScope(aRegistration->Principal(), aRegistration->Scope()));
+
+ RemoveScopeAndRegistration(aRegistration);
+}
+
+NS_IMETHODIMP
+ServiceWorkerManager::GetAllRegistrations(nsIArray** aResult) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ nsCOMPtr<nsIMutableArray> array(do_CreateInstance(NS_ARRAY_CONTRACTID));
+ if (!array) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ for (const auto& info : mRegistrationInfos.Values()) {
+ for (ServiceWorkerRegistrationInfo* reg : info->mInfos.Values()) {
+ MOZ_ASSERT(reg);
+
+ array->AppendElement(reg);
+ }
+ }
+
+ array.forget(aResult);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+ServiceWorkerManager::RemoveRegistrationsByOriginAttributes(
+ const nsAString& aPattern) {
+ MOZ_ASSERT(XRE_IsParentProcess());
+ MOZ_ASSERT(NS_IsMainThread());
+
+ MOZ_ASSERT(!aPattern.IsEmpty());
+
+ OriginAttributesPattern pattern;
+ MOZ_ALWAYS_TRUE(pattern.Init(aPattern));
+
+ for (const auto& data : mRegistrationInfos.Values()) {
+ // We can use iteration because ForceUnregister (and Unregister) are
+ // async. Otherwise doing some R/W operations on an hashtable during
+ // iteration will crash.
+ for (ServiceWorkerRegistrationInfo* reg : data->mInfos.Values()) {
+ MOZ_ASSERT(reg);
+ MOZ_ASSERT(reg->Principal());
+
+ bool matches = pattern.Matches(reg->Principal()->OriginAttributesRef());
+ if (!matches) {
+ continue;
+ }
+
+ ForceUnregister(data.get(), reg);
+ }
+ }
+
+ return NS_OK;
+}
+
+void ServiceWorkerManager::ForceUnregister(
+ RegistrationDataPerPrincipal* aRegistrationData,
+ ServiceWorkerRegistrationInfo* aRegistration) {
+ MOZ_ASSERT(aRegistrationData);
+ MOZ_ASSERT(aRegistration);
+
+ RefPtr<ServiceWorkerJobQueue> queue;
+ aRegistrationData->mJobQueues.Get(aRegistration->Scope(),
+ getter_AddRefs(queue));
+ if (queue) {
+ queue->CancelAll();
+ }
+
+ if (auto entry =
+ aRegistrationData->mUpdateTimers.Lookup(aRegistration->Scope())) {
+ entry.Data()->Cancel();
+ entry.Remove();
+ }
+
+ // Since Unregister is async, it is ok to call it in an enumeration.
+ Unregister(aRegistration->Principal(), nullptr,
+ NS_ConvertUTF8toUTF16(aRegistration->Scope()));
+}
+
+NS_IMETHODIMP
+ServiceWorkerManager::AddListener(nsIServiceWorkerManagerListener* aListener) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (!aListener || mListeners.Contains(aListener)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ mListeners.AppendElement(aListener);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+ServiceWorkerManager::RemoveListener(
+ nsIServiceWorkerManagerListener* aListener) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (!aListener || !mListeners.Contains(aListener)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ mListeners.RemoveElement(aListener);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+ServiceWorkerManager::Observe(nsISupports* aSubject, const char* aTopic,
+ const char16_t* aData) {
+ if (strcmp(aTopic, kFinishShutdownTopic) == 0) {
+ MaybeFinishShutdown();
+ return NS_OK;
+ }
+
+ MOZ_CRASH("Received message we aren't supposed to be registered for!");
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+ServiceWorkerManager::PropagateUnregister(
+ nsIPrincipal* aPrincipal, nsIServiceWorkerUnregisterCallback* aCallback,
+ const nsAString& aScope) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aPrincipal);
+
+ // Return earlier with an explicit failure if this xpcom method is called
+ // when the ServiceWorkerManager is not initialized yet or it is already
+ // shutting down.
+ if (NS_WARN_IF(!mActor)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ PrincipalInfo principalInfo;
+ if (NS_WARN_IF(
+ NS_FAILED(PrincipalToPrincipalInfo(aPrincipal, &principalInfo)))) {
+ return NS_ERROR_FAILURE;
+ }
+
+ mActor->SendPropagateUnregister(principalInfo, aScope);
+
+ nsresult rv = Unregister(aPrincipal, aCallback, aScope);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ return NS_OK;
+}
+
+void ServiceWorkerManager::NotifyListenersOnRegister(
+ nsIServiceWorkerRegistrationInfo* aInfo) {
+ nsTArray<nsCOMPtr<nsIServiceWorkerManagerListener>> listeners(
+ mListeners.Clone());
+ for (size_t index = 0; index < listeners.Length(); ++index) {
+ listeners[index]->OnRegister(aInfo);
+ }
+}
+
+void ServiceWorkerManager::NotifyListenersOnUnregister(
+ nsIServiceWorkerRegistrationInfo* aInfo) {
+ nsTArray<nsCOMPtr<nsIServiceWorkerManagerListener>> listeners(
+ mListeners.Clone());
+ for (size_t index = 0; index < listeners.Length(); ++index) {
+ listeners[index]->OnUnregister(aInfo);
+ }
+}
+
+void ServiceWorkerManager::NotifyListenersOnQuotaUsageCheckFinish(
+ nsIServiceWorkerRegistrationInfo* aRegistration) {
+ nsTArray<nsCOMPtr<nsIServiceWorkerManagerListener>> listeners(
+ mListeners.Clone());
+ for (size_t index = 0; index < listeners.Length(); ++index) {
+ listeners[index]->OnQuotaUsageCheckFinish(aRegistration);
+ }
+}
+
+class UpdateTimerCallback final : public nsITimerCallback, public nsINamed {
+ nsCOMPtr<nsIPrincipal> mPrincipal;
+ const nsCString mScope;
+
+ ~UpdateTimerCallback() = default;
+
+ public:
+ UpdateTimerCallback(nsIPrincipal* aPrincipal, const nsACString& aScope)
+ : mPrincipal(aPrincipal), mScope(aScope) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(mPrincipal);
+ MOZ_ASSERT(!mScope.IsEmpty());
+ }
+
+ NS_IMETHOD
+ Notify(nsITimer* aTimer) override {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance();
+ if (!swm) {
+ // shutting down, do nothing
+ return NS_OK;
+ }
+
+ swm->UpdateTimerFired(mPrincipal, mScope);
+ return NS_OK;
+ }
+
+ NS_IMETHOD
+ GetName(nsACString& aName) override {
+ aName.AssignLiteral("UpdateTimerCallback");
+ return NS_OK;
+ }
+
+ NS_DECL_ISUPPORTS
+};
+
+NS_IMPL_ISUPPORTS(UpdateTimerCallback, nsITimerCallback, nsINamed)
+
+void ServiceWorkerManager::ScheduleUpdateTimer(nsIPrincipal* aPrincipal,
+ const nsACString& aScope) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aPrincipal);
+ MOZ_ASSERT(!aScope.IsEmpty());
+
+ if (mShuttingDown) {
+ return;
+ }
+
+ nsAutoCString scopeKey;
+ nsresult rv = PrincipalToScopeKey(aPrincipal, scopeKey);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+
+ RegistrationDataPerPrincipal* data;
+ if (!mRegistrationInfos.Get(scopeKey, &data)) {
+ return;
+ }
+
+ data->mUpdateTimers.WithEntryHandle(
+ aScope, [&aPrincipal, &aScope](auto&& entry) {
+ if (entry) {
+ // In case there is already a timer scheduled, just use the original
+ // schedule time. We don't want to push it out to a later time since
+ // that could allow updates to be starved forever if events are
+ // continuously fired.
+ return;
+ }
+
+ nsCOMPtr<nsITimerCallback> callback =
+ new UpdateTimerCallback(aPrincipal, aScope);
+
+ const uint32_t UPDATE_DELAY_MS = 1000;
+
+ nsCOMPtr<nsITimer> timer;
+
+ const nsresult rv =
+ NS_NewTimerWithCallback(getter_AddRefs(timer), callback,
+ UPDATE_DELAY_MS, nsITimer::TYPE_ONE_SHOT);
+
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+
+ entry.Insert(std::move(timer));
+ });
+}
+
+void ServiceWorkerManager::UpdateTimerFired(nsIPrincipal* aPrincipal,
+ const nsACString& aScope) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aPrincipal);
+ MOZ_ASSERT(!aScope.IsEmpty());
+
+ if (mShuttingDown) {
+ return;
+ }
+
+ // First cleanup the timer.
+ nsAutoCString scopeKey;
+ nsresult rv = PrincipalToScopeKey(aPrincipal, scopeKey);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+
+ RegistrationDataPerPrincipal* data;
+ if (!mRegistrationInfos.Get(scopeKey, &data)) {
+ return;
+ }
+
+ if (auto entry = data->mUpdateTimers.Lookup(aScope)) {
+ entry.Data()->Cancel();
+ entry.Remove();
+ }
+
+ RefPtr<ServiceWorkerRegistrationInfo> registration;
+ data->mInfos.Get(aScope, getter_AddRefs(registration));
+ if (!registration) {
+ return;
+ }
+
+ if (!registration->CheckAndClearIfUpdateNeeded()) {
+ return;
+ }
+
+ OriginAttributes attrs = aPrincipal->OriginAttributesRef();
+
+ SoftUpdate(attrs, aScope);
+}
+
+void ServiceWorkerManager::MaybeSendUnregister(nsIPrincipal* aPrincipal,
+ const nsACString& aScope) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aPrincipal);
+ MOZ_ASSERT(!aScope.IsEmpty());
+
+ if (!mActor) {
+ return;
+ }
+
+ PrincipalInfo principalInfo;
+ nsresult rv = PrincipalToPrincipalInfo(aPrincipal, &principalInfo);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+
+ Unused << mActor->SendUnregister(principalInfo,
+ NS_ConvertUTF8toUTF16(aScope));
+}
+
+void ServiceWorkerManager::AddOrphanedRegistration(
+ ServiceWorkerRegistrationInfo* aRegistration) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aRegistration);
+ MOZ_ASSERT(aRegistration->IsUnregistered());
+ MOZ_ASSERT(!aRegistration->IsControllingClients());
+ MOZ_ASSERT(!aRegistration->IsIdle());
+ MOZ_ASSERT(!mOrphanedRegistrations.has(aRegistration));
+
+ MOZ_ALWAYS_TRUE(mOrphanedRegistrations.putNew(aRegistration));
+}
+
+void ServiceWorkerManager::RemoveOrphanedRegistration(
+ ServiceWorkerRegistrationInfo* aRegistration) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aRegistration);
+ MOZ_ASSERT(aRegistration->IsUnregistered());
+ MOZ_ASSERT(!aRegistration->IsControllingClients());
+ MOZ_ASSERT(aRegistration->IsIdle());
+ MOZ_ASSERT(mOrphanedRegistrations.has(aRegistration));
+
+ mOrphanedRegistrations.remove(aRegistration);
+}
+
+uint32_t ServiceWorkerManager::MaybeInitServiceWorkerShutdownProgress() const {
+ if (!mShutdownBlocker) {
+ return ServiceWorkerShutdownBlocker::kInvalidShutdownStateId;
+ }
+
+ return mShutdownBlocker->CreateShutdownState();
+}
+
+void ServiceWorkerManager::ReportServiceWorkerShutdownProgress(
+ uint32_t aShutdownStateId,
+ ServiceWorkerShutdownState::Progress aProgress) const {
+ MOZ_ASSERT(mShutdownBlocker);
+ mShutdownBlocker->ReportShutdownProgress(aShutdownStateId, aProgress);
+}
+
+} // namespace mozilla::dom
diff --git a/dom/serviceworkers/ServiceWorkerManager.h b/dom/serviceworkers/ServiceWorkerManager.h
new file mode 100644
index 0000000000..9a3b9ee39f
--- /dev/null
+++ b/dom/serviceworkers/ServiceWorkerManager.h
@@ -0,0 +1,439 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_workers_serviceworkermanager_h
+#define mozilla_dom_workers_serviceworkermanager_h
+
+#include <cstdint>
+#include "ErrorList.h"
+#include "ServiceWorkerShutdownState.h"
+#include "js/ErrorReport.h"
+#include "mozilla/AlreadyAddRefed.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/HashTable.h"
+#include "mozilla/MozPromise.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/dom/ClientHandle.h"
+#include "mozilla/dom/ClientOpPromise.h"
+#include "mozilla/dom/ServiceWorkerRegistrationBinding.h"
+#include "mozilla/dom/ServiceWorkerRegistrationInfo.h"
+#include "mozilla/dom/ServiceWorkerUtils.h"
+#include "mozilla/mozalloc.h"
+#include "nsClassHashtable.h"
+#include "nsContentUtils.h"
+#include "nsHashKeys.h"
+#include "nsIObserver.h"
+#include "nsIServiceWorkerManager.h"
+#include "nsISupports.h"
+#include "nsStringFwd.h"
+#include "nsTArray.h"
+
+class nsIConsoleReportCollector;
+
+namespace mozilla {
+
+class OriginAttributes;
+
+namespace ipc {
+class PrincipalInfo;
+} // namespace ipc
+
+namespace dom {
+
+extern uint32_t gServiceWorkersRegistered;
+extern uint32_t gServiceWorkersRegisteredFetch;
+
+class ContentParent;
+class ServiceWorkerInfo;
+class ServiceWorkerJobQueue;
+class ServiceWorkerManagerChild;
+class ServiceWorkerPrivate;
+class ServiceWorkerRegistrar;
+class ServiceWorkerShutdownBlocker;
+
+class ServiceWorkerUpdateFinishCallback {
+ protected:
+ virtual ~ServiceWorkerUpdateFinishCallback() = default;
+
+ public:
+ NS_INLINE_DECL_REFCOUNTING(ServiceWorkerUpdateFinishCallback)
+
+ virtual void UpdateSucceeded(ServiceWorkerRegistrationInfo* aInfo) = 0;
+
+ virtual void UpdateFailed(ErrorResult& aStatus) = 0;
+};
+
+#define NS_SERVICEWORKERMANAGER_IMPL_IID \
+ { /* f4f8755a-69ca-46e8-a65d-775745535990 */ \
+ 0xf4f8755a, 0x69ca, 0x46e8, { \
+ 0xa6, 0x5d, 0x77, 0x57, 0x45, 0x53, 0x59, 0x90 \
+ } \
+ }
+
+/*
+ * The ServiceWorkerManager is a per-process global that deals with the
+ * installation, querying and event dispatch of ServiceWorkers for all the
+ * origins in the process.
+ *
+ * NOTE: the following documentation is a WIP:
+ *
+ * The ServiceWorkerManager (SWM) is a main-thread, parent-process singleton
+ * that encapsulates the browser-global state of service workers. This state
+ * includes, but is not limited to, all service worker registrations and all
+ * controlled service worker clients. The SWM also provides methods to read and
+ * mutate this state and to dispatch operations (e.g. DOM events such as a
+ * FetchEvent) to service workers.
+ *
+ * Example usage:
+ *
+ * MOZ_ASSERT(NS_IsMainThread(), "SWM is main-thread only");
+ *
+ * RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance();
+ *
+ * // Nullness must be checked by code that possibly executes during browser
+ * // shutdown, which is when the SWM is destroyed.
+ * if (swm) {
+ * // Do something with the SWM.
+ * }
+ */
+class ServiceWorkerManager final : public nsIServiceWorkerManager,
+ public nsIObserver {
+ friend class GetRegistrationsRunnable;
+ friend class GetRegistrationRunnable;
+ friend class ServiceWorkerJob;
+ friend class ServiceWorkerRegistrationInfo;
+ friend class ServiceWorkerShutdownBlocker;
+ friend class ServiceWorkerUnregisterJob;
+ friend class ServiceWorkerUpdateJob;
+ friend class UpdateTimerCallback;
+
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSISERVICEWORKERMANAGER
+ NS_DECL_NSIOBSERVER
+
+ // Return true if the given principal and URI matches a registered service
+ // worker which handles fetch event.
+ // If there is a matched service worker but doesn't handle fetch events, this
+ // method will try to set the matched service worker as the controller of the
+ // passed in channel. Then also schedule a soft-update job for the service
+ // worker.
+ bool IsAvailable(nsIPrincipal* aPrincipal, nsIURI* aURI,
+ nsIChannel* aChannel);
+
+ void DispatchFetchEvent(nsIInterceptedChannel* aChannel, ErrorResult& aRv);
+
+ void Update(nsIPrincipal* aPrincipal, const nsACString& aScope,
+ nsCString aNewestWorkerScriptUrl,
+ ServiceWorkerUpdateFinishCallback* aCallback);
+
+ void UpdateInternal(nsIPrincipal* aPrincipal, const nsACString& aScope,
+ nsCString&& aNewestWorkerScriptUrl,
+ ServiceWorkerUpdateFinishCallback* aCallback);
+
+ void SoftUpdate(const OriginAttributes& aOriginAttributes,
+ const nsACString& aScope);
+
+ void SoftUpdateInternal(const OriginAttributes& aOriginAttributes,
+ const nsACString& aScope,
+ ServiceWorkerUpdateFinishCallback* aCallback);
+
+ RefPtr<ServiceWorkerRegistrationPromise> Register(
+ const ClientInfo& aClientInfo, const nsACString& aScopeURL,
+ const nsACString& aScriptURL,
+ ServiceWorkerUpdateViaCache aUpdateViaCache);
+
+ RefPtr<ServiceWorkerRegistrationPromise> GetRegistration(
+ const ClientInfo& aClientInfo, const nsACString& aURL) const;
+
+ RefPtr<ServiceWorkerRegistrationListPromise> GetRegistrations(
+ const ClientInfo& aClientInfo) const;
+
+ already_AddRefed<ServiceWorkerRegistrationInfo> GetRegistration(
+ nsIPrincipal* aPrincipal, const nsACString& aScope) const;
+
+ already_AddRefed<ServiceWorkerRegistrationInfo> GetRegistration(
+ const mozilla::ipc::PrincipalInfo& aPrincipal,
+ const nsACString& aScope) const;
+
+ already_AddRefed<ServiceWorkerRegistrationInfo> CreateNewRegistration(
+ const nsCString& aScope, nsIPrincipal* aPrincipal,
+ ServiceWorkerUpdateViaCache aUpdateViaCache,
+ IPCNavigationPreloadState aNavigationPreloadState =
+ IPCNavigationPreloadState(false, "true"_ns));
+
+ void RemoveRegistration(ServiceWorkerRegistrationInfo* aRegistration);
+
+ void StoreRegistration(nsIPrincipal* aPrincipal,
+ ServiceWorkerRegistrationInfo* aRegistration);
+
+ /**
+ * Report an error for the given scope to any window we think might be
+ * interested, failing over to the Browser Console if we couldn't find any.
+ *
+ * Error messages should be localized, so you probably want to call
+ * LocalizeAndReportToAllClients instead, which in turn calls us after
+ * localizing the error.
+ */
+ void ReportToAllClients(const nsCString& aScope, const nsString& aMessage,
+ const nsString& aFilename, const nsString& aLine,
+ uint32_t aLineNumber, uint32_t aColumnNumber,
+ uint32_t aFlags);
+
+ /**
+ * Report a localized error for the given scope to any window we think might
+ * be interested.
+ *
+ * Note that this method takes an nsTArray<nsString> for the parameters, not
+ * bare chart16_t*[]. You can use a std::initializer_list constructor inline
+ * so that argument might look like: nsTArray<nsString> { some_nsString,
+ * PromiseFlatString(some_nsSubString_aka_nsAString),
+ * NS_ConvertUTF8toUTF16(some_nsCString_or_nsCSubString),
+ * u"some literal"_ns }. If you have anything else, like a
+ * number, you can use an nsAutoString with AppendInt/friends.
+ *
+ * @param [aFlags]
+ * The nsIScriptError flag, one of errorFlag (0x0), warningFlag (0x1),
+ * infoFlag (0x8). We default to error if omitted because usually we're
+ * logging exceptional and/or obvious breakage.
+ */
+ static void LocalizeAndReportToAllClients(
+ const nsCString& aScope, const char* aStringKey,
+ const nsTArray<nsString>& aParamArray, uint32_t aFlags = 0x0,
+ const nsString& aFilename = u""_ns, const nsString& aLine = u""_ns,
+ uint32_t aLineNumber = 0, uint32_t aColumnNumber = 0);
+
+ // Always consumes the error by reporting to consoles of all controlled
+ // documents.
+ void HandleError(JSContext* aCx, nsIPrincipal* aPrincipal,
+ const nsCString& aScope, const nsString& aWorkerURL,
+ const nsString& aMessage, const nsString& aFilename,
+ const nsString& aLine, uint32_t aLineNumber,
+ uint32_t aColumnNumber, uint32_t aFlags, JSExnType aExnType);
+
+ [[nodiscard]] RefPtr<GenericErrorResultPromise> MaybeClaimClient(
+ const ClientInfo& aClientInfo,
+ ServiceWorkerRegistrationInfo* aWorkerRegistration);
+
+ [[nodiscard]] RefPtr<GenericErrorResultPromise> MaybeClaimClient(
+ const ClientInfo& aClientInfo,
+ const ServiceWorkerDescriptor& aServiceWorker);
+
+ static already_AddRefed<ServiceWorkerManager> GetInstance();
+
+ void LoadRegistration(const ServiceWorkerRegistrationData& aRegistration);
+
+ void LoadRegistrations(
+ const nsTArray<ServiceWorkerRegistrationData>& aRegistrations);
+
+ void MaybeCheckNavigationUpdate(const ClientInfo& aClientInfo);
+
+ nsresult SendPushEvent(const nsACString& aOriginAttributes,
+ const nsACString& aScope, const nsAString& aMessageId,
+ const Maybe<nsTArray<uint8_t>>& aData);
+
+ void WorkerIsIdle(ServiceWorkerInfo* aWorker);
+
+ RefPtr<ServiceWorkerRegistrationPromise> WhenReady(
+ const ClientInfo& aClientInfo);
+
+ void CheckPendingReadyPromises();
+
+ void RemovePendingReadyPromise(const ClientInfo& aClientInfo);
+
+ void NoteInheritedController(const ClientInfo& aClientInfo,
+ const ServiceWorkerDescriptor& aController);
+
+ void BlockShutdownOn(GenericNonExclusivePromise* aPromise,
+ uint32_t aShutdownStateId);
+
+ nsresult GetClientRegistration(
+ const ClientInfo& aClientInfo,
+ ServiceWorkerRegistrationInfo** aRegistrationInfo);
+
+ int32_t GetPrincipalQuotaUsageCheckCount(nsIPrincipal* aPrincipal);
+
+ void CheckPrincipalQuotaUsage(nsIPrincipal* aPrincipal,
+ const nsACString& aScope);
+
+ // Returns the shutdown state ID (may be an invalid ID if an
+ // nsIAsyncShutdownBlocker is not used).
+ uint32_t MaybeInitServiceWorkerShutdownProgress() const;
+
+ void ReportServiceWorkerShutdownProgress(
+ uint32_t aShutdownStateId,
+ ServiceWorkerShutdownState::Progress aProgress) const;
+
+ // Record periodic telemetry on number of running ServiceWorkers. When
+ // the number of running ServiceWorkers changes (or on shutdown),
+ // ServiceWorkerPrivateImpl will call RecordTelemetry with the number of
+ // running serviceworkers and those supporting Fetch. We use
+ // mTelemetryLastChange to determine how many datapoints to inject into
+ // Telemetry, and dispatch a background runnable to call
+ // RecordTelemetryGap() and Accumulate them.
+ void RecordTelemetry(uint32_t aNumber, uint32_t aFetch);
+
+ void EvictFromBFCache(ServiceWorkerRegistrationInfo* aRegistration);
+
+ private:
+ struct RegistrationDataPerPrincipal;
+
+ static bool FindScopeForPath(const nsACString& aScopeKey,
+ const nsACString& aPath,
+ RegistrationDataPerPrincipal** aData,
+ nsACString& aMatch);
+
+ ServiceWorkerManager();
+ ~ServiceWorkerManager();
+
+ void Init(ServiceWorkerRegistrar* aRegistrar);
+
+ RefPtr<GenericErrorResultPromise> StartControllingClient(
+ const ClientInfo& aClientInfo,
+ ServiceWorkerRegistrationInfo* aRegistrationInfo,
+ bool aControlClientHandle = true);
+
+ void StopControllingClient(const ClientInfo& aClientInfo);
+
+ void MaybeStartShutdown();
+
+ void MaybeFinishShutdown();
+
+ already_AddRefed<ServiceWorkerJobQueue> GetOrCreateJobQueue(
+ const nsACString& aOriginSuffix, const nsACString& aScope);
+
+ void MaybeRemoveRegistrationInfo(const nsACString& aScopeKey);
+
+ already_AddRefed<ServiceWorkerRegistrationInfo> GetRegistration(
+ const nsACString& aScopeKey, const nsACString& aScope) const;
+
+ void AbortCurrentUpdate(ServiceWorkerRegistrationInfo* aRegistration);
+
+ nsresult Update(ServiceWorkerRegistrationInfo* aRegistration);
+
+ ServiceWorkerInfo* GetActiveWorkerInfoForScope(
+ const OriginAttributes& aOriginAttributes, const nsACString& aScope);
+
+ void StopControllingRegistration(
+ ServiceWorkerRegistrationInfo* aRegistration);
+
+ already_AddRefed<ServiceWorkerRegistrationInfo>
+ GetServiceWorkerRegistrationInfo(const ClientInfo& aClientInfo) const;
+
+ already_AddRefed<ServiceWorkerRegistrationInfo>
+ GetServiceWorkerRegistrationInfo(nsIPrincipal* aPrincipal,
+ nsIURI* aURI) const;
+
+ already_AddRefed<ServiceWorkerRegistrationInfo>
+ GetServiceWorkerRegistrationInfo(const nsACString& aScopeKey,
+ nsIURI* aURI) const;
+
+ // This method generates a key using isInElementBrowser from the principal. We
+ // don't use the origin because it can change during the loading.
+ static nsresult PrincipalToScopeKey(nsIPrincipal* aPrincipal,
+ nsACString& aKey);
+
+ static nsresult PrincipalInfoToScopeKey(
+ const mozilla::ipc::PrincipalInfo& aPrincipalInfo, nsACString& aKey);
+
+ static void AddScopeAndRegistration(
+ const nsACString& aScope, ServiceWorkerRegistrationInfo* aRegistation);
+
+ static bool HasScope(nsIPrincipal* aPrincipal, const nsACString& aScope);
+
+ static void RemoveScopeAndRegistration(
+ ServiceWorkerRegistrationInfo* aRegistration);
+
+ void QueueFireEventOnServiceWorkerRegistrations(
+ ServiceWorkerRegistrationInfo* aRegistration, const nsAString& aName);
+
+ void UpdateClientControllers(ServiceWorkerRegistrationInfo* aRegistration);
+
+ void MaybeRemoveRegistration(ServiceWorkerRegistrationInfo* aRegistration);
+
+ RefPtr<ServiceWorkerManagerChild> mActor;
+
+ bool mShuttingDown;
+
+ nsTArray<nsCOMPtr<nsIServiceWorkerManagerListener>> mListeners;
+
+ void NotifyListenersOnRegister(
+ nsIServiceWorkerRegistrationInfo* aRegistration);
+
+ void NotifyListenersOnUnregister(
+ nsIServiceWorkerRegistrationInfo* aRegistration);
+
+ void NotifyListenersOnQuotaUsageCheckFinish(
+ nsIServiceWorkerRegistrationInfo* aRegistration);
+
+ void ScheduleUpdateTimer(nsIPrincipal* aPrincipal, const nsACString& aScope);
+
+ void UpdateTimerFired(nsIPrincipal* aPrincipal, const nsACString& aScope);
+
+ void MaybeSendUnregister(nsIPrincipal* aPrincipal, const nsACString& aScope);
+
+ nsresult SendNotificationEvent(const nsAString& aEventName,
+ const nsACString& aOriginSuffix,
+ const nsACString& aScope, const nsAString& aID,
+ const nsAString& aTitle, const nsAString& aDir,
+ const nsAString& aLang, const nsAString& aBody,
+ const nsAString& aTag, const nsAString& aIcon,
+ const nsAString& aData,
+ const nsAString& aBehavior);
+
+ // Used by remove() and removeAll() when clearing history.
+ // MUST ONLY BE CALLED FROM UnregisterIfMatchesHost!
+ void ForceUnregister(RegistrationDataPerPrincipal* aRegistrationData,
+ ServiceWorkerRegistrationInfo* aRegistration);
+
+ // An "orphaned" registration is one that is unregistered and not controlling
+ // clients. The ServiceWorkerManager must know about all orphaned
+ // registrations to forcefully shutdown all Service Workers during browser
+ // shutdown.
+ void AddOrphanedRegistration(ServiceWorkerRegistrationInfo* aRegistration);
+
+ void RemoveOrphanedRegistration(ServiceWorkerRegistrationInfo* aRegistration);
+
+ HashSet<RefPtr<ServiceWorkerRegistrationInfo>,
+ PointerHasher<ServiceWorkerRegistrationInfo*>>
+ mOrphanedRegistrations;
+
+ RefPtr<ServiceWorkerShutdownBlocker> mShutdownBlocker;
+
+ nsClassHashtable<nsCStringHashKey, RegistrationDataPerPrincipal>
+ mRegistrationInfos;
+
+ struct ControlledClientData {
+ RefPtr<ClientHandle> mClientHandle;
+ RefPtr<ServiceWorkerRegistrationInfo> mRegistrationInfo;
+
+ ControlledClientData(ClientHandle* aClientHandle,
+ ServiceWorkerRegistrationInfo* aRegistrationInfo)
+ : mClientHandle(aClientHandle), mRegistrationInfo(aRegistrationInfo) {}
+ };
+
+ nsClassHashtable<nsIDHashKey, ControlledClientData> mControlledClients;
+
+ struct PendingReadyData {
+ RefPtr<ClientHandle> mClientHandle;
+ RefPtr<ServiceWorkerRegistrationPromise::Private> mPromise;
+
+ explicit PendingReadyData(ClientHandle* aClientHandle)
+ : mClientHandle(aClientHandle),
+ mPromise(new ServiceWorkerRegistrationPromise::Private(__func__)) {}
+ };
+
+ nsTArray<UniquePtr<PendingReadyData>> mPendingReadyList;
+
+ const uint32_t mTelemetryPeriodMs = 5 * 1000;
+ TimeStamp mTelemetryLastChange;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_workers_serviceworkermanager_h
diff --git a/dom/serviceworkers/ServiceWorkerManagerChild.h b/dom/serviceworkers/ServiceWorkerManagerChild.h
new file mode 100644
index 0000000000..54a374b14b
--- /dev/null
+++ b/dom/serviceworkers/ServiceWorkerManagerChild.h
@@ -0,0 +1,42 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_ServiceWorkerManagerChild_h
+#define mozilla_dom_ServiceWorkerManagerChild_h
+
+#include "mozilla/dom/PServiceWorkerManagerChild.h"
+#include "mozilla/ipc/BackgroundUtils.h"
+
+namespace mozilla {
+
+class OriginAttributes;
+
+namespace ipc {
+class BackgroundChildImpl;
+} // namespace ipc
+
+namespace dom {
+
+class ServiceWorkerManagerChild final : public PServiceWorkerManagerChild {
+ friend class mozilla::ipc::BackgroundChildImpl;
+
+ public:
+ NS_INLINE_DECL_REFCOUNTING(ServiceWorkerManagerChild)
+
+ void ManagerShuttingDown() { mShuttingDown = true; }
+
+ private:
+ ServiceWorkerManagerChild() : mShuttingDown(false) {}
+
+ ~ServiceWorkerManagerChild() = default;
+
+ bool mShuttingDown;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_ServiceWorkerManagerChild_h
diff --git a/dom/serviceworkers/ServiceWorkerManagerParent.cpp b/dom/serviceworkers/ServiceWorkerManagerParent.cpp
new file mode 100644
index 0000000000..5ed0f4faa8
--- /dev/null
+++ b/dom/serviceworkers/ServiceWorkerManagerParent.cpp
@@ -0,0 +1,106 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "ServiceWorkerManagerParent.h"
+#include "ServiceWorkerUtils.h"
+#include "mozilla/dom/ContentParent.h"
+#include "mozilla/dom/ServiceWorkerRegistrar.h"
+#include "mozilla/ipc/BackgroundParent.h"
+#include "mozilla/ipc/BackgroundUtils.h"
+#include "mozilla/Unused.h"
+#include "nsThreadUtils.h"
+
+namespace mozilla {
+
+using namespace ipc;
+
+namespace dom {
+
+ServiceWorkerManagerParent::ServiceWorkerManagerParent() {
+ AssertIsOnBackgroundThread();
+}
+
+ServiceWorkerManagerParent::~ServiceWorkerManagerParent() {
+ AssertIsOnBackgroundThread();
+}
+
+mozilla::ipc::IPCResult ServiceWorkerManagerParent::RecvRegister(
+ const ServiceWorkerRegistrationData& aData) {
+ AssertIsInMainProcess();
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(!BackgroundParent::IsOtherProcessActor(Manager()));
+
+ // Basic validation.
+ if (aData.scope().IsEmpty() ||
+ aData.principal().type() == PrincipalInfo::TNullPrincipalInfo ||
+ aData.principal().type() == PrincipalInfo::TSystemPrincipalInfo) {
+ return IPC_FAIL_NO_REASON(this);
+ }
+
+ // If false then we have shutdown during the process of trying to update the
+ // registrar. We can give up on this modification.
+ if (const RefPtr<dom::ServiceWorkerRegistrar> service =
+ dom::ServiceWorkerRegistrar::Get()) {
+ service->RegisterServiceWorker(aData);
+ }
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult ServiceWorkerManagerParent::RecvUnregister(
+ const PrincipalInfo& aPrincipalInfo, const nsString& aScope) {
+ AssertIsInMainProcess();
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(!BackgroundParent::IsOtherProcessActor(Manager()));
+
+ // Basic validation.
+ if (aScope.IsEmpty() ||
+ aPrincipalInfo.type() == PrincipalInfo::TNullPrincipalInfo ||
+ aPrincipalInfo.type() == PrincipalInfo::TSystemPrincipalInfo) {
+ return IPC_FAIL_NO_REASON(this);
+ }
+
+ // If false then we have shutdown during the process of trying to update the
+ // registrar. We can give up on this modification.
+ if (const RefPtr<dom::ServiceWorkerRegistrar> service =
+ dom::ServiceWorkerRegistrar::Get()) {
+ service->UnregisterServiceWorker(aPrincipalInfo,
+ NS_ConvertUTF16toUTF8(aScope));
+ }
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult ServiceWorkerManagerParent::RecvPropagateUnregister(
+ const PrincipalInfo& aPrincipalInfo, const nsString& aScope) {
+ AssertIsOnBackgroundThread();
+
+ RefPtr<dom::ServiceWorkerRegistrar> service =
+ dom::ServiceWorkerRegistrar::Get();
+ MOZ_ASSERT(service);
+
+ // It's possible that we don't have any ServiceWorkerManager managing this
+ // scope but we still need to unregister it from the ServiceWorkerRegistrar.
+ service->UnregisterServiceWorker(aPrincipalInfo,
+ NS_ConvertUTF16toUTF8(aScope));
+
+ // There is no longer any point to propagating because the only sender is the
+ // one and only ServiceWorkerManager, but it is necessary for us to have run
+ // the unregister call above because until Bug 1183245 is fixed,
+ // nsIServiceWorkerManager.propagateUnregister() is a de facto API for
+ // clearing ServiceWorker registrations by Sanitizer.jsm via
+ // ServiceWorkerCleanUp.jsm, as well as devtools "unregister" affordance and
+ // the no-longer-relevant about:serviceworkers UI.
+
+ return IPC_OK();
+}
+
+void ServiceWorkerManagerParent::ActorDestroy(ActorDestroyReason aWhy) {
+ AssertIsOnBackgroundThread();
+}
+
+} // namespace dom
+} // namespace mozilla
diff --git a/dom/serviceworkers/ServiceWorkerManagerParent.h b/dom/serviceworkers/ServiceWorkerManagerParent.h
new file mode 100644
index 0000000000..741f2250b3
--- /dev/null
+++ b/dom/serviceworkers/ServiceWorkerManagerParent.h
@@ -0,0 +1,48 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_ServiceWorkerManagerParent_h
+#define mozilla_dom_ServiceWorkerManagerParent_h
+
+#include "mozilla/dom/PServiceWorkerManagerParent.h"
+
+namespace mozilla {
+
+namespace ipc {
+class BackgroundParentImpl;
+} // namespace ipc
+
+namespace dom {
+
+class ServiceWorkerManagerService;
+
+class ServiceWorkerManagerParent final : public PServiceWorkerManagerParent {
+ friend class mozilla::ipc::BackgroundParentImpl;
+ friend class PServiceWorkerManagerParent;
+
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(ServiceWorkerManagerParent)
+
+ private:
+ ServiceWorkerManagerParent();
+ ~ServiceWorkerManagerParent();
+
+ mozilla::ipc::IPCResult RecvRegister(
+ const ServiceWorkerRegistrationData& aData);
+
+ mozilla::ipc::IPCResult RecvUnregister(const PrincipalInfo& aPrincipalInfo,
+ const nsString& aScope);
+
+ mozilla::ipc::IPCResult RecvPropagateUnregister(
+ const PrincipalInfo& aPrincipalInfo, const nsString& aScope);
+
+ virtual void ActorDestroy(ActorDestroyReason aWhy) override;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_ServiceWorkerManagerParent_h
diff --git a/dom/serviceworkers/ServiceWorkerOp.cpp b/dom/serviceworkers/ServiceWorkerOp.cpp
new file mode 100644
index 0000000000..8cd9218852
--- /dev/null
+++ b/dom/serviceworkers/ServiceWorkerOp.cpp
@@ -0,0 +1,1913 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "ServiceWorkerOp.h"
+
+#include <utility>
+
+#include "ServiceWorkerOpPromise.h"
+#include "js/Exception.h" // JS::ExceptionStack, JS::StealPendingExceptionStack
+#include "jsapi.h"
+
+#include "nsCOMPtr.h"
+#include "nsContentUtils.h"
+#include "nsDebug.h"
+#include "nsError.h"
+#include "nsINamed.h"
+#include "nsIPushErrorReporter.h"
+#include "nsISupportsImpl.h"
+#include "nsITimer.h"
+#include "nsProxyRelease.h"
+#include "nsServiceManagerUtils.h"
+#include "nsTArray.h"
+#include "nsThreadUtils.h"
+
+#include "ServiceWorkerCloneData.h"
+#include "ServiceWorkerShutdownState.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/CycleCollectedJSContext.h"
+#include "mozilla/DebugOnly.h"
+#include "mozilla/ErrorResult.h"
+#include "mozilla/OwningNonNull.h"
+#include "mozilla/SchedulerGroup.h"
+#include "mozilla/ScopeExit.h"
+#include "mozilla/Telemetry.h"
+#include "mozilla/Unused.h"
+#include "mozilla/dom/BindingDeclarations.h"
+#include "mozilla/dom/Client.h"
+#include "mozilla/dom/ExtendableMessageEventBinding.h"
+#include "mozilla/dom/FetchEventBinding.h"
+#include "mozilla/dom/FetchEventOpProxyChild.h"
+#include "mozilla/dom/InternalHeaders.h"
+#include "mozilla/dom/InternalRequest.h"
+#include "mozilla/dom/InternalResponse.h"
+#include "mozilla/dom/Notification.h"
+#include "mozilla/dom/NotificationEvent.h"
+#include "mozilla/dom/NotificationEventBinding.h"
+#include "mozilla/dom/PerformanceTiming.h"
+#include "mozilla/dom/PerformanceStorage.h"
+#include "mozilla/dom/PushEventBinding.h"
+#include "mozilla/dom/RemoteWorkerChild.h"
+#include "mozilla/dom/RemoteWorkerService.h"
+#include "mozilla/dom/Request.h"
+#include "mozilla/dom/Response.h"
+#include "mozilla/dom/RootedDictionary.h"
+#include "mozilla/dom/SafeRefPtr.h"
+#include "mozilla/dom/ServiceWorkerBinding.h"
+#include "mozilla/dom/ServiceWorkerGlobalScopeBinding.h"
+#include "mozilla/dom/WorkerCommon.h"
+#include "mozilla/dom/WorkerRef.h"
+#include "mozilla/dom/WorkerScope.h"
+#include "mozilla/extensions/ExtensionBrowser.h"
+#include "mozilla/ipc/IPCStreamUtils.h"
+#include "mozilla/net/MozURL.h"
+
+namespace mozilla::dom {
+
+namespace {
+
+class ExtendableEventKeepAliveHandler final
+ : public ExtendableEvent::ExtensionsHandler,
+ public PromiseNativeHandler {
+ public:
+ NS_DECL_ISUPPORTS
+
+ static RefPtr<ExtendableEventKeepAliveHandler> Create(
+ RefPtr<ExtendableEventCallback> aCallback) {
+ MOZ_ASSERT(IsCurrentThreadRunningWorker());
+
+ RefPtr<ExtendableEventKeepAliveHandler> self =
+ new ExtendableEventKeepAliveHandler(std::move(aCallback));
+
+ self->mWorkerRef = StrongWorkerRef::Create(
+ GetCurrentThreadWorkerPrivate(), "ExtendableEventKeepAliveHandler",
+ [self]() { self->Cleanup(); });
+
+ if (NS_WARN_IF(!self->mWorkerRef)) {
+ return nullptr;
+ }
+
+ return self;
+ }
+
+ /**
+ * ExtendableEvent::ExtensionsHandler interface
+ */
+ bool WaitOnPromise(Promise& aPromise) override {
+ if (!mAcceptingPromises) {
+ MOZ_ASSERT(!GetDispatchFlag());
+ MOZ_ASSERT(!mSelfRef, "We shouldn't be holding a self reference!");
+ return false;
+ }
+
+ if (!mSelfRef) {
+ MOZ_ASSERT(!mPendingPromisesCount);
+ mSelfRef = this;
+ }
+
+ ++mPendingPromisesCount;
+ aPromise.AppendNativeHandler(this);
+
+ return true;
+ }
+
+ /**
+ * PromiseNativeHandler interface
+ */
+ void ResolvedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue,
+ ErrorResult& aRv) override {
+ RemovePromise(Resolved);
+ }
+
+ void RejectedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue,
+ ErrorResult& aRv) override {
+ RemovePromise(Rejected);
+ }
+
+ void MaybeDone() {
+ MOZ_ASSERT(IsCurrentThreadRunningWorker());
+ MOZ_ASSERT(!GetDispatchFlag());
+
+ if (mPendingPromisesCount) {
+ return;
+ }
+
+ if (mCallback) {
+ mCallback->FinishedWithResult(mRejected ? Rejected : Resolved);
+ mCallback = nullptr;
+ }
+
+ Cleanup();
+ }
+
+ private:
+ /**
+ * This class is useful for the case where pending microtasks will continue
+ * extending the event, which means that the event is not "done." For example:
+ *
+ * // `e` is an ExtendableEvent, `p` is a Promise
+ * e.waitUntil(p);
+ * p.then(() => e.waitUntil(otherPromise));
+ */
+ class MaybeDoneRunner : public MicroTaskRunnable {
+ public:
+ explicit MaybeDoneRunner(RefPtr<ExtendableEventKeepAliveHandler> aHandler)
+ : mHandler(std::move(aHandler)) {}
+
+ void Run(AutoSlowOperation& /* unused */) override {
+ mHandler->MaybeDone();
+ }
+
+ private:
+ RefPtr<ExtendableEventKeepAliveHandler> mHandler;
+ };
+
+ explicit ExtendableEventKeepAliveHandler(
+ RefPtr<ExtendableEventCallback> aCallback)
+ : mCallback(std::move(aCallback)) {}
+
+ ~ExtendableEventKeepAliveHandler() { Cleanup(); }
+
+ void Cleanup() {
+ MOZ_ASSERT(IsCurrentThreadRunningWorker());
+
+ if (mCallback) {
+ mCallback->FinishedWithResult(Rejected);
+ }
+
+ mSelfRef = nullptr;
+ mWorkerRef = nullptr;
+ mCallback = nullptr;
+ mAcceptingPromises = false;
+ }
+
+ void RemovePromise(ExtendableEventResult aResult) {
+ MOZ_ASSERT(IsCurrentThreadRunningWorker());
+ MOZ_DIAGNOSTIC_ASSERT(mPendingPromisesCount > 0);
+
+ // NOTE: mSelfRef can be nullptr here if MaybeCleanup() was just called
+ // before a promise settled. This can happen, for example, if the worker
+ // thread is being terminated for running too long, browser shutdown, etc.
+
+ mRejected |= (aResult == Rejected);
+
+ --mPendingPromisesCount;
+ if (mPendingPromisesCount || GetDispatchFlag()) {
+ return;
+ }
+
+ CycleCollectedJSContext* cx = CycleCollectedJSContext::Get();
+ MOZ_ASSERT(cx);
+
+ RefPtr<MaybeDoneRunner> r = new MaybeDoneRunner(this);
+ cx->DispatchToMicroTask(r.forget());
+ }
+
+ /**
+ * We start holding a self reference when the first extension promise is
+ * added, and this reference is released when the last promise settles or
+ * when the worker is shutting down.
+ *
+ * This is needed in the case that we're waiting indefinitely on a to-be-GC'ed
+ * promise that's no longer reachable and will never be settled.
+ */
+ RefPtr<ExtendableEventKeepAliveHandler> mSelfRef;
+
+ RefPtr<StrongWorkerRef> mWorkerRef;
+
+ RefPtr<ExtendableEventCallback> mCallback;
+
+ uint32_t mPendingPromisesCount = 0;
+
+ bool mRejected = false;
+ bool mAcceptingPromises = true;
+};
+
+NS_IMPL_ISUPPORTS0(ExtendableEventKeepAliveHandler)
+
+nsresult DispatchExtendableEventOnWorkerScope(
+ JSContext* aCx, WorkerGlobalScope* aWorkerScope, ExtendableEvent* aEvent,
+ RefPtr<ExtendableEventCallback> aCallback) {
+ MOZ_ASSERT(aCx);
+ MOZ_ASSERT(aWorkerScope);
+ MOZ_ASSERT(aEvent);
+
+ nsCOMPtr<nsIGlobalObject> globalObject = aWorkerScope;
+ WidgetEvent* internalEvent = aEvent->WidgetEventPtr();
+
+ RefPtr<ExtendableEventKeepAliveHandler> keepAliveHandler =
+ ExtendableEventKeepAliveHandler::Create(std::move(aCallback));
+ if (NS_WARN_IF(!keepAliveHandler)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // This must be always set *before* dispatching the event, otherwise
+ // waitUntil() calls will fail.
+ aEvent->SetKeepAliveHandler(keepAliveHandler);
+
+ ErrorResult result;
+ aWorkerScope->DispatchEvent(*aEvent, result);
+ if (NS_WARN_IF(result.Failed())) {
+ result.SuppressException();
+ return NS_ERROR_FAILURE;
+ }
+
+ keepAliveHandler->MaybeDone();
+
+ // We don't block the event when getting an exception but still report the
+ // error message. NOTE: this will not stop the event.
+ if (internalEvent->mFlags.mExceptionWasRaised) {
+ return NS_ERROR_XPC_JS_THREW_EXCEPTION;
+ }
+
+ return NS_OK;
+}
+
+bool DispatchFailed(nsresult aStatus) {
+ return NS_FAILED(aStatus) && aStatus != NS_ERROR_XPC_JS_THREW_EXCEPTION;
+}
+
+} // anonymous namespace
+
+class ServiceWorkerOp::ServiceWorkerOpRunnable : public WorkerDebuggeeRunnable {
+ public:
+ NS_DECL_ISUPPORTS_INHERITED
+
+ ServiceWorkerOpRunnable(RefPtr<ServiceWorkerOp> aOwner,
+ WorkerPrivate* aWorkerPrivate)
+ : WorkerDebuggeeRunnable(aWorkerPrivate, WorkerThreadModifyBusyCount),
+ mOwner(std::move(aOwner)) {
+ AssertIsOnMainThread();
+ MOZ_ASSERT(mOwner);
+ MOZ_ASSERT(aWorkerPrivate);
+ }
+
+ private:
+ ~ServiceWorkerOpRunnable() = default;
+
+ bool WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override {
+ MOZ_ASSERT(aWorkerPrivate);
+ aWorkerPrivate->AssertIsOnWorkerThread();
+ MOZ_ASSERT(aWorkerPrivate->IsServiceWorker());
+ MOZ_ASSERT(mOwner);
+
+ bool rv = mOwner->Exec(aCx, aWorkerPrivate);
+ Unused << NS_WARN_IF(!rv);
+ mOwner = nullptr;
+
+ return rv;
+ }
+
+ nsresult Cancel() override {
+ // We need to check first if cancel is permitted
+ nsresult rv = WorkerRunnable::Cancel();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ MOZ_ASSERT(mOwner);
+
+ mOwner->RejectAll(NS_ERROR_DOM_ABORT_ERR);
+ mOwner = nullptr;
+
+ return NS_OK;
+ }
+
+ RefPtr<ServiceWorkerOp> mOwner;
+};
+
+NS_IMPL_ISUPPORTS_INHERITED0(ServiceWorkerOp::ServiceWorkerOpRunnable,
+ WorkerRunnable)
+
+bool ServiceWorkerOp::MaybeStart(RemoteWorkerChild* aOwner,
+ RemoteWorkerChild::State& aState) {
+ MOZ_ASSERT(!mStarted);
+ MOZ_ASSERT(aOwner);
+ MOZ_ASSERT(aOwner->GetOwningEventTarget()->IsOnCurrentThread());
+
+ auto launcherData = aOwner->mLauncherData.Access();
+
+ if (NS_WARN_IF(!launcherData->mIPCActive)) {
+ RejectAll(NS_ERROR_DOM_ABORT_ERR);
+ mStarted = true;
+ return true;
+ }
+
+ // Allow termination to happen while the Service Worker is initializing.
+ if (aState.is<Pending>() && !IsTerminationOp()) {
+ return false;
+ }
+
+ if (NS_WARN_IF(aState.is<RemoteWorkerChild::PendingTerminated>()) ||
+ NS_WARN_IF(aState.is<RemoteWorkerChild::Terminated>())) {
+ RejectAll(NS_ERROR_DOM_INVALID_STATE_ERR);
+ mStarted = true;
+ return true;
+ }
+
+ MOZ_ASSERT(aState.is<RemoteWorkerChild::Running>() || IsTerminationOp());
+
+ RefPtr<ServiceWorkerOp> self = this;
+
+ if (IsTerminationOp()) {
+ aOwner->GetTerminationPromise()->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [self](
+ const GenericNonExclusivePromise::ResolveOrRejectValue& aResult) {
+ MaybeReportServiceWorkerShutdownProgress(self->mArgs, true);
+
+ MOZ_ASSERT(!self->mPromiseHolder.IsEmpty());
+
+ if (NS_WARN_IF(aResult.IsReject())) {
+ self->mPromiseHolder.Reject(aResult.RejectValue(), __func__);
+ return;
+ }
+
+ self->mPromiseHolder.Resolve(NS_OK, __func__);
+ });
+ }
+
+ RefPtr<RemoteWorkerChild> owner = aOwner;
+
+ nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction(
+ __func__, [self = std::move(self), owner = std::move(owner)]() mutable {
+ MaybeReportServiceWorkerShutdownProgress(self->mArgs);
+
+ auto lock = owner->mState.Lock();
+ auto& state = lock.ref();
+
+ if (NS_WARN_IF(!state.is<Running>() && !self->IsTerminationOp())) {
+ self->RejectAll(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return;
+ }
+
+ if (self->IsTerminationOp()) {
+ owner->CloseWorkerOnMainThread(state);
+ } else {
+ MOZ_ASSERT(state.is<Running>());
+
+ RefPtr<WorkerRunnable> workerRunnable =
+ self->GetRunnable(state.as<Running>().mWorkerPrivate);
+
+ if (NS_WARN_IF(!workerRunnable->Dispatch())) {
+ self->RejectAll(NS_ERROR_FAILURE);
+ }
+ }
+
+ nsCOMPtr<nsIEventTarget> target = owner->GetOwningEventTarget();
+ NS_ProxyRelease(__func__, target, owner.forget());
+ });
+
+ mStarted = true;
+
+ MOZ_ALWAYS_SUCCEEDS(
+ SchedulerGroup::Dispatch(TaskCategory::Other, r.forget()));
+
+ return true;
+}
+
+void ServiceWorkerOp::Cancel() { RejectAll(NS_ERROR_DOM_ABORT_ERR); }
+
+ServiceWorkerOp::ServiceWorkerOp(
+ ServiceWorkerOpArgs&& aArgs,
+ std::function<void(const ServiceWorkerOpResult&)>&& aCallback)
+ : mArgs(std::move(aArgs)) {
+ MOZ_ASSERT(RemoteWorkerService::Thread()->IsOnCurrentThread());
+
+ RefPtr<ServiceWorkerOpPromise> promise = mPromiseHolder.Ensure(__func__);
+
+ promise->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [callback = std::move(aCallback)](
+ ServiceWorkerOpPromise::ResolveOrRejectValue&& aResult) mutable {
+ if (NS_WARN_IF(aResult.IsReject())) {
+ MOZ_ASSERT(NS_FAILED(aResult.RejectValue()));
+ callback(aResult.RejectValue());
+ return;
+ }
+
+ callback(aResult.ResolveValue());
+ });
+}
+
+ServiceWorkerOp::~ServiceWorkerOp() {
+ Unused << NS_WARN_IF(!mPromiseHolder.IsEmpty());
+ mPromiseHolder.RejectIfExists(NS_ERROR_DOM_ABORT_ERR, __func__);
+}
+
+bool ServiceWorkerOp::Started() const {
+ MOZ_ASSERT(RemoteWorkerService::Thread()->IsOnCurrentThread());
+
+ return mStarted;
+}
+
+bool ServiceWorkerOp::IsTerminationOp() const {
+ return mArgs.type() ==
+ ServiceWorkerOpArgs::TServiceWorkerTerminateWorkerOpArgs;
+}
+
+RefPtr<WorkerRunnable> ServiceWorkerOp::GetRunnable(
+ WorkerPrivate* aWorkerPrivate) {
+ AssertIsOnMainThread();
+ MOZ_ASSERT(aWorkerPrivate);
+
+ return new ServiceWorkerOpRunnable(this, aWorkerPrivate);
+}
+
+void ServiceWorkerOp::RejectAll(nsresult aStatus) {
+ MOZ_ASSERT(!mPromiseHolder.IsEmpty());
+ mPromiseHolder.Reject(aStatus, __func__);
+}
+
+class CheckScriptEvaluationOp final : public ServiceWorkerOp {
+ using ServiceWorkerOp::ServiceWorkerOp;
+
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(CheckScriptEvaluationOp, override)
+
+ private:
+ ~CheckScriptEvaluationOp() = default;
+
+ bool Exec(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override {
+ MOZ_ASSERT(aWorkerPrivate);
+ aWorkerPrivate->AssertIsOnWorkerThread();
+ MOZ_ASSERT(aWorkerPrivate->IsServiceWorker());
+ MOZ_ASSERT(!mPromiseHolder.IsEmpty());
+
+ ServiceWorkerCheckScriptEvaluationOpResult result;
+ result.workerScriptExecutedSuccessfully() =
+ aWorkerPrivate->WorkerScriptExecutedSuccessfully();
+ result.fetchHandlerWasAdded() = aWorkerPrivate->FetchHandlerWasAdded();
+
+ mPromiseHolder.Resolve(result, __func__);
+
+ return true;
+ }
+};
+
+class TerminateServiceWorkerOp final : public ServiceWorkerOp {
+ using ServiceWorkerOp::ServiceWorkerOp;
+
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(TerminateServiceWorkerOp, override)
+
+ private:
+ ~TerminateServiceWorkerOp() = default;
+
+ bool Exec(JSContext*, WorkerPrivate*) override {
+ MOZ_ASSERT_UNREACHABLE(
+ "Worker termination should be handled in "
+ "`ServiceWorkerOp::MaybeStart()`");
+
+ return false;
+ }
+};
+
+class UpdateServiceWorkerStateOp final : public ServiceWorkerOp {
+ using ServiceWorkerOp::ServiceWorkerOp;
+
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(UpdateServiceWorkerStateOp, override);
+
+ private:
+ class UpdateStateOpRunnable final : public MainThreadWorkerControlRunnable {
+ public:
+ NS_DECL_ISUPPORTS_INHERITED
+
+ UpdateStateOpRunnable(RefPtr<UpdateServiceWorkerStateOp> aOwner,
+ WorkerPrivate* aWorkerPrivate)
+ : MainThreadWorkerControlRunnable(aWorkerPrivate),
+ mOwner(std::move(aOwner)) {
+ AssertIsOnMainThread();
+ MOZ_ASSERT(mOwner);
+ MOZ_ASSERT(aWorkerPrivate);
+ }
+
+ private:
+ ~UpdateStateOpRunnable() = default;
+
+ bool WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override {
+ MOZ_ASSERT(aWorkerPrivate);
+ aWorkerPrivate->AssertIsOnWorkerThread();
+ MOZ_ASSERT(aWorkerPrivate->IsServiceWorker());
+
+ if (mOwner) {
+ Unused << mOwner->Exec(aCx, aWorkerPrivate);
+ mOwner = nullptr;
+ }
+
+ return true;
+ }
+
+ nsresult Cancel() override {
+ MOZ_ASSERT(mOwner);
+
+ mOwner->RejectAll(NS_ERROR_DOM_ABORT_ERR);
+ mOwner = nullptr;
+
+ return MainThreadWorkerControlRunnable::Cancel();
+ }
+
+ RefPtr<UpdateServiceWorkerStateOp> mOwner;
+ };
+
+ ~UpdateServiceWorkerStateOp() = default;
+
+ RefPtr<WorkerRunnable> GetRunnable(WorkerPrivate* aWorkerPrivate) override {
+ AssertIsOnMainThread();
+ MOZ_ASSERT(aWorkerPrivate);
+ MOZ_ASSERT(mArgs.type() ==
+ ServiceWorkerOpArgs::TServiceWorkerUpdateStateOpArgs);
+
+ return new UpdateStateOpRunnable(this, aWorkerPrivate);
+ }
+
+ bool Exec(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override {
+ MOZ_ASSERT(aWorkerPrivate);
+ aWorkerPrivate->AssertIsOnWorkerThread();
+ MOZ_ASSERT(aWorkerPrivate->IsServiceWorker());
+ MOZ_ASSERT(!mPromiseHolder.IsEmpty());
+
+ ServiceWorkerState state =
+ mArgs.get_ServiceWorkerUpdateStateOpArgs().state();
+ aWorkerPrivate->UpdateServiceWorkerState(state);
+
+ mPromiseHolder.Resolve(NS_OK, __func__);
+
+ return true;
+ }
+};
+
+NS_IMPL_ISUPPORTS_INHERITED0(UpdateServiceWorkerStateOp::UpdateStateOpRunnable,
+ MainThreadWorkerControlRunnable)
+
+void ExtendableEventOp::FinishedWithResult(ExtendableEventResult aResult) {
+ MOZ_ASSERT(IsCurrentThreadRunningWorker());
+ MOZ_ASSERT(!mPromiseHolder.IsEmpty());
+
+ mPromiseHolder.Resolve(aResult == Resolved ? NS_OK : NS_ERROR_FAILURE,
+ __func__);
+}
+
+class LifeCycleEventOp final : public ExtendableEventOp {
+ using ExtendableEventOp::ExtendableEventOp;
+
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(LifeCycleEventOp, override)
+
+ private:
+ ~LifeCycleEventOp() = default;
+
+ bool Exec(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override {
+ MOZ_ASSERT(aWorkerPrivate);
+ aWorkerPrivate->AssertIsOnWorkerThread();
+ MOZ_ASSERT(aWorkerPrivate->IsServiceWorker());
+ MOZ_ASSERT(!mPromiseHolder.IsEmpty());
+
+ RefPtr<ExtendableEvent> event;
+ RefPtr<EventTarget> target = aWorkerPrivate->GlobalScope();
+
+ const nsString& eventName =
+ mArgs.get_ServiceWorkerLifeCycleEventOpArgs().eventName();
+
+ if (eventName.EqualsASCII("install") || eventName.EqualsASCII("activate")) {
+ ExtendableEventInit init;
+ init.mBubbles = false;
+ init.mCancelable = false;
+ event = ExtendableEvent::Constructor(target, eventName, init);
+ } else {
+ MOZ_CRASH("Unexpected lifecycle event");
+ }
+
+ event->SetTrusted(true);
+
+ nsresult rv = DispatchExtendableEventOnWorkerScope(
+ aCx, aWorkerPrivate->GlobalScope(), event, this);
+
+ if (NS_WARN_IF(DispatchFailed(rv))) {
+ RejectAll(rv);
+ }
+
+ return !DispatchFailed(rv);
+ }
+};
+
+/**
+ * PushEventOp
+ */
+class PushEventOp final : public ExtendableEventOp {
+ using ExtendableEventOp::ExtendableEventOp;
+
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(PushEventOp, override)
+
+ private:
+ ~PushEventOp() = default;
+
+ bool Exec(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override {
+ MOZ_ASSERT(aWorkerPrivate);
+ aWorkerPrivate->AssertIsOnWorkerThread();
+ MOZ_ASSERT(aWorkerPrivate->IsServiceWorker());
+ MOZ_ASSERT(!mPromiseHolder.IsEmpty());
+
+ ErrorResult result;
+
+ auto scopeExit = MakeScopeExit([&] {
+ MOZ_ASSERT(result.Failed());
+
+ RejectAll(result.StealNSResult());
+ ReportError(aWorkerPrivate);
+ });
+
+ const ServiceWorkerPushEventOpArgs& args =
+ mArgs.get_ServiceWorkerPushEventOpArgs();
+
+ RootedDictionary<PushEventInit> pushEventInit(aCx);
+
+ if (args.data().type() != OptionalPushData::Tvoid_t) {
+ auto& bytes = args.data().get_ArrayOfuint8_t();
+ JSObject* data =
+ Uint8Array::Create(aCx, bytes.Length(), bytes.Elements());
+
+ if (!data) {
+ result = ErrorResult(NS_ERROR_FAILURE);
+ return false;
+ }
+
+ DebugOnly<bool> inited =
+ pushEventInit.mData.Construct().SetAsArrayBufferView().Init(data);
+ MOZ_ASSERT(inited);
+ }
+
+ pushEventInit.mBubbles = false;
+ pushEventInit.mCancelable = false;
+
+ GlobalObject globalObj(aCx, aWorkerPrivate->GlobalScope()->GetWrapper());
+ RefPtr<PushEvent> pushEvent =
+ PushEvent::Constructor(globalObj, u"push"_ns, pushEventInit, result);
+
+ if (NS_WARN_IF(result.Failed())) {
+ return false;
+ }
+
+ pushEvent->SetTrusted(true);
+
+ scopeExit.release();
+
+ nsresult rv = DispatchExtendableEventOnWorkerScope(
+ aCx, aWorkerPrivate->GlobalScope(), pushEvent, this);
+
+ if (NS_FAILED(rv)) {
+ if (NS_WARN_IF(DispatchFailed(rv))) {
+ RejectAll(rv);
+ }
+
+ // We don't cancel WorkerPrivate when catching an exception.
+ ReportError(aWorkerPrivate,
+ nsIPushErrorReporter::DELIVERY_UNCAUGHT_EXCEPTION);
+ }
+
+ return !DispatchFailed(rv);
+ }
+
+ void FinishedWithResult(ExtendableEventResult aResult) override {
+ MOZ_ASSERT(IsCurrentThreadRunningWorker());
+
+ WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate();
+
+ if (aResult == Rejected) {
+ ReportError(workerPrivate,
+ nsIPushErrorReporter::DELIVERY_UNHANDLED_REJECTION);
+ }
+
+ ExtendableEventOp::FinishedWithResult(aResult);
+ }
+
+ void ReportError(
+ WorkerPrivate* aWorkerPrivate,
+ uint16_t aError = nsIPushErrorReporter::DELIVERY_INTERNAL_ERROR) {
+ MOZ_ASSERT(aWorkerPrivate);
+ aWorkerPrivate->AssertIsOnWorkerThread();
+ MOZ_ASSERT(aWorkerPrivate->IsServiceWorker());
+
+ if (NS_WARN_IF(aError > nsIPushErrorReporter::DELIVERY_INTERNAL_ERROR) ||
+ mArgs.get_ServiceWorkerPushEventOpArgs().messageId().IsEmpty()) {
+ return;
+ }
+
+ nsString messageId = mArgs.get_ServiceWorkerPushEventOpArgs().messageId();
+ nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction(
+ __func__, [messageId = std::move(messageId), error = aError] {
+ nsCOMPtr<nsIPushErrorReporter> reporter =
+ do_GetService("@mozilla.org/push/Service;1");
+
+ if (reporter) {
+ nsresult rv = reporter->ReportDeliveryError(messageId, error);
+ Unused << NS_WARN_IF(NS_FAILED(rv));
+ }
+ });
+
+ MOZ_ALWAYS_SUCCEEDS(aWorkerPrivate->DispatchToMainThread(r.forget()));
+ }
+};
+
+class PushSubscriptionChangeEventOp final : public ExtendableEventOp {
+ using ExtendableEventOp::ExtendableEventOp;
+
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(PushSubscriptionChangeEventOp, override)
+
+ private:
+ ~PushSubscriptionChangeEventOp() = default;
+
+ bool Exec(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override {
+ MOZ_ASSERT(aWorkerPrivate);
+ aWorkerPrivate->AssertIsOnWorkerThread();
+ MOZ_ASSERT(aWorkerPrivate->IsServiceWorker());
+ MOZ_ASSERT(!mPromiseHolder.IsEmpty());
+
+ RefPtr<EventTarget> target = aWorkerPrivate->GlobalScope();
+
+ ExtendableEventInit init;
+ init.mBubbles = false;
+ init.mCancelable = false;
+
+ RefPtr<ExtendableEvent> event = ExtendableEvent::Constructor(
+ target, u"pushsubscriptionchange"_ns, init);
+ event->SetTrusted(true);
+
+ nsresult rv = DispatchExtendableEventOnWorkerScope(
+ aCx, aWorkerPrivate->GlobalScope(), event, this);
+
+ if (NS_WARN_IF(DispatchFailed(rv))) {
+ RejectAll(rv);
+ }
+
+ return !DispatchFailed(rv);
+ }
+};
+
+class NotificationEventOp : public ExtendableEventOp,
+ public nsITimerCallback,
+ public nsINamed {
+ using ExtendableEventOp::ExtendableEventOp;
+
+ public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+
+ private:
+ ~NotificationEventOp() {
+ MOZ_DIAGNOSTIC_ASSERT(!mTimer);
+ MOZ_DIAGNOSTIC_ASSERT(!mWorkerRef);
+ }
+
+ void ClearWindowAllowed(WorkerPrivate* aWorkerPrivate) {
+ MOZ_ASSERT(aWorkerPrivate);
+ aWorkerPrivate->AssertIsOnWorkerThread();
+ MOZ_ASSERT(aWorkerPrivate->IsServiceWorker());
+
+ if (!mTimer) {
+ return;
+ }
+
+ // This might be executed after the global was unrooted, in which case
+ // GlobalScope() will return null. Making the check here just to be safe.
+ WorkerGlobalScope* globalScope = aWorkerPrivate->GlobalScope();
+ if (!globalScope) {
+ return;
+ }
+
+ globalScope->ConsumeWindowInteraction();
+ mTimer->Cancel();
+ mTimer = nullptr;
+
+ mWorkerRef = nullptr;
+ }
+
+ void StartClearWindowTimer(WorkerPrivate* aWorkerPrivate) {
+ MOZ_ASSERT(aWorkerPrivate);
+ aWorkerPrivate->AssertIsOnWorkerThread();
+ MOZ_ASSERT(!mTimer);
+
+ nsresult rv;
+ nsCOMPtr<nsITimer> timer =
+ NS_NewTimer(aWorkerPrivate->ControlEventTarget());
+ if (NS_WARN_IF(!timer)) {
+ return;
+ }
+
+ MOZ_ASSERT(!mWorkerRef);
+ RefPtr<NotificationEventOp> self = this;
+ mWorkerRef = StrongWorkerRef::Create(
+ aWorkerPrivate, "NotificationEventOp", [self = std::move(self)] {
+ // We could try to hold the worker alive until the timer fires, but
+ // other APIs are not likely to work in this partially shutdown state.
+ // We might as well let the worker thread exit.
+ self->ClearWindowAllowed(self->mWorkerRef->Private());
+ });
+
+ if (!mWorkerRef) {
+ return;
+ }
+
+ aWorkerPrivate->GlobalScope()->AllowWindowInteraction();
+ timer.swap(mTimer);
+
+ // We swap first and then initialize the timer so that even if initializing
+ // fails, we still clean the busy count and interaction count correctly.
+ // The timer can't be initialized before modyfing the busy count since the
+ // timer thread could run and call the timeout but the worker may
+ // already be terminating and modifying the busy count could fail.
+ uint32_t delay = mArgs.get_ServiceWorkerNotificationEventOpArgs()
+ .disableOpenClickDelay();
+ rv = mTimer->InitWithCallback(this, delay, nsITimer::TYPE_ONE_SHOT);
+
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ ClearWindowAllowed(aWorkerPrivate);
+ return;
+ }
+ }
+
+ // ExtendableEventOp interface
+ bool Exec(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override {
+ MOZ_ASSERT(aWorkerPrivate);
+ aWorkerPrivate->AssertIsOnWorkerThread();
+ MOZ_ASSERT(aWorkerPrivate->IsServiceWorker());
+ MOZ_ASSERT(!mPromiseHolder.IsEmpty());
+
+ RefPtr<EventTarget> target = aWorkerPrivate->GlobalScope();
+
+ ServiceWorkerNotificationEventOpArgs& args =
+ mArgs.get_ServiceWorkerNotificationEventOpArgs();
+
+ ErrorResult result;
+ RefPtr<Notification> notification = Notification::ConstructFromFields(
+ aWorkerPrivate->GlobalScope(), args.id(), args.title(), args.dir(),
+ args.lang(), args.body(), args.tag(), args.icon(), args.data(),
+ args.scope(), result);
+
+ if (NS_WARN_IF(result.Failed())) {
+ return false;
+ }
+
+ NotificationEventInit init;
+ init.mNotification = notification;
+ init.mBubbles = false;
+ init.mCancelable = false;
+
+ RefPtr<NotificationEvent> notificationEvent =
+ NotificationEvent::Constructor(target, args.eventName(), init);
+
+ notificationEvent->SetTrusted(true);
+
+ if (args.eventName().EqualsLiteral("notificationclick")) {
+ StartClearWindowTimer(aWorkerPrivate);
+ }
+
+ nsresult rv = DispatchExtendableEventOnWorkerScope(
+ aCx, aWorkerPrivate->GlobalScope(), notificationEvent, this);
+
+ if (NS_WARN_IF(DispatchFailed(rv))) {
+ // This will reject mPromiseHolder.
+ FinishedWithResult(Rejected);
+ }
+
+ return !DispatchFailed(rv);
+ }
+
+ void FinishedWithResult(ExtendableEventResult aResult) override {
+ MOZ_ASSERT(IsCurrentThreadRunningWorker());
+
+ WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate();
+ MOZ_ASSERT(workerPrivate);
+
+ ClearWindowAllowed(workerPrivate);
+
+ ExtendableEventOp::FinishedWithResult(aResult);
+ }
+
+ // nsITimerCallback interface
+ NS_IMETHOD Notify(nsITimer* aTimer) override {
+ MOZ_DIAGNOSTIC_ASSERT(mTimer == aTimer);
+ WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate();
+ ClearWindowAllowed(workerPrivate);
+ return NS_OK;
+ }
+
+ // nsINamed interface
+ NS_IMETHOD GetName(nsACString& aName) override {
+ aName.AssignLiteral("NotificationEventOp");
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsITimer> mTimer;
+ RefPtr<StrongWorkerRef> mWorkerRef;
+};
+
+NS_IMPL_ISUPPORTS(NotificationEventOp, nsITimerCallback, nsINamed)
+
+class MessageEventOp final : public ExtendableEventOp {
+ using ExtendableEventOp::ExtendableEventOp;
+
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(MessageEventOp, override)
+
+ MessageEventOp(ServiceWorkerOpArgs&& aArgs,
+ std::function<void(const ServiceWorkerOpResult&)>&& aCallback)
+ : ExtendableEventOp(std::move(aArgs), std::move(aCallback)),
+ mData(new ServiceWorkerCloneData()) {
+ mData->CopyFromClonedMessageData(
+ mArgs.get_ServiceWorkerMessageEventOpArgs().clonedData());
+ }
+
+ private:
+ ~MessageEventOp() = default;
+
+ bool Exec(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override {
+ MOZ_ASSERT(aWorkerPrivate);
+ aWorkerPrivate->AssertIsOnWorkerThread();
+ MOZ_ASSERT(aWorkerPrivate->IsServiceWorker());
+ MOZ_ASSERT(!mPromiseHolder.IsEmpty());
+
+ JS::Rooted<JS::Value> messageData(aCx);
+ nsCOMPtr<nsIGlobalObject> sgo = aWorkerPrivate->GlobalScope();
+ ErrorResult rv;
+ if (!mData->IsErrorMessageData()) {
+ mData->Read(aCx, &messageData, rv);
+ }
+
+ // If mData is an error message data, then it means that it failed to
+ // serialize on the caller side because it contains a shared memory object.
+ // If deserialization fails, we will fire a messageerror event.
+ const bool deserializationFailed =
+ rv.Failed() || mData->IsErrorMessageData();
+
+ Sequence<OwningNonNull<MessagePort>> ports;
+ if (!mData->TakeTransferredPortsAsSequence(ports)) {
+ RejectAll(NS_ERROR_FAILURE);
+ rv.SuppressException();
+ return false;
+ }
+
+ RootedDictionary<ExtendableMessageEventInit> init(aCx);
+
+ init.mBubbles = false;
+ init.mCancelable = false;
+
+ // On a messageerror event, we disregard ports:
+ // https://w3c.github.io/ServiceWorker/#service-worker-postmessage
+ if (!deserializationFailed) {
+ init.mData = messageData;
+ init.mPorts = std::move(ports);
+ }
+
+ RefPtr<net::MozURL> mozUrl;
+ nsresult result = net::MozURL::Init(
+ getter_AddRefs(mozUrl), mArgs.get_ServiceWorkerMessageEventOpArgs()
+ .clientInfoAndState()
+ .info()
+ .url());
+ if (NS_WARN_IF(NS_FAILED(result))) {
+ RejectAll(result);
+ rv.SuppressException();
+ return false;
+ }
+
+ nsCString origin;
+ mozUrl->Origin(origin);
+
+ CopyUTF8toUTF16(origin, init.mOrigin);
+
+ init.mSource.SetValue().SetAsClient() = new Client(
+ sgo, mArgs.get_ServiceWorkerMessageEventOpArgs().clientInfoAndState());
+
+ rv.SuppressException();
+ RefPtr<EventTarget> target = aWorkerPrivate->GlobalScope();
+ RefPtr<ExtendableMessageEvent> extendableEvent =
+ ExtendableMessageEvent::Constructor(
+ target, deserializationFailed ? u"messageerror"_ns : u"message"_ns,
+ init);
+
+ extendableEvent->SetTrusted(true);
+
+ nsresult rv2 = DispatchExtendableEventOnWorkerScope(
+ aCx, aWorkerPrivate->GlobalScope(), extendableEvent, this);
+
+ if (NS_WARN_IF(DispatchFailed(rv2))) {
+ RejectAll(rv2);
+ }
+
+ return !DispatchFailed(rv2);
+ }
+
+ RefPtr<ServiceWorkerCloneData> mData;
+};
+
+/**
+ * Used for ScopeExit-style network request cancelation in
+ * `ResolvedCallback()` (e.g. if `FetchEvent::RespondWith()` is resolved with
+ * a non-JS object).
+ */
+class MOZ_STACK_CLASS FetchEventOp::AutoCancel {
+ public:
+ explicit AutoCancel(FetchEventOp* aOwner)
+ : mOwner(aOwner),
+ mLine(0),
+ mColumn(0),
+ mMessageName("InterceptionFailedWithURL"_ns) {
+ MOZ_ASSERT(IsCurrentThreadRunningWorker());
+ MOZ_ASSERT(mOwner);
+
+ nsAutoString requestURL;
+ mOwner->GetRequestURL(requestURL);
+ mParams.AppendElement(requestURL);
+ }
+
+ ~AutoCancel() {
+ if (mOwner) {
+ if (mSourceSpec.IsEmpty()) {
+ mOwner->AsyncLog(mMessageName, std::move(mParams));
+ } else {
+ mOwner->AsyncLog(mSourceSpec, mLine, mColumn, mMessageName,
+ std::move(mParams));
+ }
+
+ MOZ_ASSERT(!mOwner->mRespondWithPromiseHolder.IsEmpty());
+ mOwner->mHandled->MaybeRejectWithNetworkError("AutoCancel"_ns);
+ mOwner->mRespondWithPromiseHolder.Reject(
+ CancelInterceptionArgs(
+ NS_ERROR_INTERCEPTION_FAILED,
+ FetchEventTimeStamps(mOwner->mFetchHandlerStart,
+ mOwner->mFetchHandlerFinish)),
+ __func__);
+ }
+ }
+
+ // This function steals the error message from a ErrorResult.
+ void SetCancelErrorResult(JSContext* aCx, ErrorResult& aRv) {
+ MOZ_DIAGNOSTIC_ASSERT(aRv.Failed());
+ MOZ_DIAGNOSTIC_ASSERT(!JS_IsExceptionPending(aCx));
+
+ // Storing the error as exception in the JSContext.
+ if (!aRv.MaybeSetPendingException(aCx)) {
+ return;
+ }
+
+ MOZ_ASSERT(!aRv.Failed());
+
+ // Let's take the pending exception.
+ JS::ExceptionStack exnStack(aCx);
+ if (!JS::StealPendingExceptionStack(aCx, &exnStack)) {
+ return;
+ }
+
+ // Converting the exception in a JS::ErrorReportBuilder.
+ JS::ErrorReportBuilder report(aCx);
+ if (!report.init(aCx, exnStack, JS::ErrorReportBuilder::WithSideEffects)) {
+ JS_ClearPendingException(aCx);
+ return;
+ }
+
+ MOZ_ASSERT(mOwner);
+ MOZ_ASSERT(mMessageName.EqualsLiteral("InterceptionFailedWithURL"));
+ MOZ_ASSERT(mParams.Length() == 1);
+
+ // Let's store the error message here.
+ mMessageName.Assign(report.toStringResult().c_str());
+ mParams.Clear();
+ }
+
+ template <typename... Params>
+ void SetCancelMessage(const nsACString& aMessageName, Params&&... aParams) {
+ MOZ_ASSERT(mOwner);
+ MOZ_ASSERT(mMessageName.EqualsLiteral("InterceptionFailedWithURL"));
+ MOZ_ASSERT(mParams.Length() == 1);
+ mMessageName = aMessageName;
+ mParams.Clear();
+ StringArrayAppender::Append(mParams, sizeof...(Params),
+ std::forward<Params>(aParams)...);
+ }
+
+ template <typename... Params>
+ void SetCancelMessageAndLocation(const nsACString& aSourceSpec,
+ uint32_t aLine, uint32_t aColumn,
+ const nsACString& aMessageName,
+ Params&&... aParams) {
+ MOZ_ASSERT(mOwner);
+ MOZ_ASSERT(mMessageName.EqualsLiteral("InterceptionFailedWithURL"));
+ MOZ_ASSERT(mParams.Length() == 1);
+
+ mSourceSpec = aSourceSpec;
+ mLine = aLine;
+ mColumn = aColumn;
+
+ mMessageName = aMessageName;
+ mParams.Clear();
+ StringArrayAppender::Append(mParams, sizeof...(Params),
+ std::forward<Params>(aParams)...);
+ }
+
+ void Reset() { mOwner = nullptr; }
+
+ private:
+ FetchEventOp* MOZ_NON_OWNING_REF mOwner;
+ nsCString mSourceSpec;
+ uint32_t mLine;
+ uint32_t mColumn;
+ nsCString mMessageName;
+ nsTArray<nsString> mParams;
+};
+
+NS_IMPL_ISUPPORTS0(FetchEventOp)
+
+void FetchEventOp::SetActor(RefPtr<FetchEventOpProxyChild> aActor) {
+ MOZ_ASSERT(RemoteWorkerService::Thread()->IsOnCurrentThread());
+ MOZ_ASSERT(!Started());
+ MOZ_ASSERT(!mActor);
+
+ mActor = std::move(aActor);
+}
+
+void FetchEventOp::RevokeActor(FetchEventOpProxyChild* aActor) {
+ MOZ_ASSERT(aActor);
+ MOZ_ASSERT_IF(mActor, mActor == aActor);
+
+ mActor = nullptr;
+}
+
+RefPtr<FetchEventRespondWithPromise> FetchEventOp::GetRespondWithPromise() {
+ MOZ_ASSERT(RemoteWorkerService::Thread()->IsOnCurrentThread());
+ MOZ_ASSERT(!Started());
+ MOZ_ASSERT(mRespondWithPromiseHolder.IsEmpty());
+
+ return mRespondWithPromiseHolder.Ensure(__func__);
+}
+
+void FetchEventOp::RespondWithCalledAt(const nsCString& aRespondWithScriptSpec,
+ uint32_t aRespondWithLineNumber,
+ uint32_t aRespondWithColumnNumber) {
+ MOZ_ASSERT(IsCurrentThreadRunningWorker());
+ MOZ_ASSERT(!mRespondWithClosure);
+
+ mRespondWithClosure.emplace(aRespondWithScriptSpec, aRespondWithLineNumber,
+ aRespondWithColumnNumber);
+}
+
+void FetchEventOp::ReportCanceled(const nsCString& aPreventDefaultScriptSpec,
+ uint32_t aPreventDefaultLineNumber,
+ uint32_t aPreventDefaultColumnNumber) {
+ MOZ_ASSERT(IsCurrentThreadRunningWorker());
+ MOZ_ASSERT(mActor);
+ MOZ_ASSERT(!mPromiseHolder.IsEmpty());
+
+ nsString requestURL;
+ GetRequestURL(requestURL);
+
+ AsyncLog(aPreventDefaultScriptSpec, aPreventDefaultLineNumber,
+ aPreventDefaultColumnNumber, "InterceptionCanceledWithURL"_ns,
+ {std::move(requestURL)});
+}
+
+FetchEventOp::~FetchEventOp() {
+ mRespondWithPromiseHolder.RejectIfExists(
+ CancelInterceptionArgs(
+ NS_ERROR_DOM_ABORT_ERR,
+ FetchEventTimeStamps(mFetchHandlerStart, mFetchHandlerFinish)),
+ __func__);
+
+ if (mActor) {
+ NS_ProxyRelease("FetchEventOp::mActor", RemoteWorkerService::Thread(),
+ mActor.forget());
+ }
+}
+
+void FetchEventOp::RejectAll(nsresult aStatus) {
+ MOZ_ASSERT(!mRespondWithPromiseHolder.IsEmpty());
+ MOZ_ASSERT(!mPromiseHolder.IsEmpty());
+
+ if (mFetchHandlerStart.IsNull()) {
+ mFetchHandlerStart = TimeStamp::Now();
+ }
+ if (mFetchHandlerFinish.IsNull()) {
+ mFetchHandlerFinish = TimeStamp::Now();
+ }
+
+ mRespondWithPromiseHolder.Reject(
+ CancelInterceptionArgs(
+ aStatus,
+ FetchEventTimeStamps(mFetchHandlerStart, mFetchHandlerFinish)),
+ __func__);
+ mPromiseHolder.Reject(aStatus, __func__);
+}
+
+void FetchEventOp::FinishedWithResult(ExtendableEventResult aResult) {
+ MOZ_ASSERT(IsCurrentThreadRunningWorker());
+ MOZ_ASSERT(!mPromiseHolder.IsEmpty());
+ MOZ_ASSERT(!mResult);
+
+ mResult.emplace(aResult);
+
+ /**
+ * This should only return early if neither waitUntil() nor respondWith()
+ * are called. The early return is so that mRespondWithPromiseHolder has a
+ * chance to settle before mPromiseHolder does.
+ */
+ if (!mPostDispatchChecksDone) {
+ return;
+ }
+
+ MaybeFinished();
+}
+
+void FetchEventOp::MaybeFinished() {
+ MOZ_ASSERT(IsCurrentThreadRunningWorker());
+ MOZ_ASSERT(!mPromiseHolder.IsEmpty());
+
+ if (mResult) {
+ // It's possible that mRespondWithPromiseHolder wasn't settled. That happens
+ // if the worker was terminated before the respondWith promise settled.
+
+ mHandled = nullptr;
+ mPreloadResponse = nullptr;
+ mPreloadResponseAvailablePromiseRequestHolder.DisconnectIfExists();
+ mPreloadResponseEndPromiseRequestHolder.DisconnectIfExists();
+
+ ServiceWorkerFetchEventOpResult result(
+ mResult.value() == Resolved ? NS_OK : NS_ERROR_FAILURE);
+
+ mPromiseHolder.Resolve(result, __func__);
+ }
+}
+
+bool FetchEventOp::Exec(JSContext* aCx, WorkerPrivate* aWorkerPrivate) {
+ aWorkerPrivate->AssertIsOnWorkerThread();
+ MOZ_ASSERT(aWorkerPrivate->IsServiceWorker());
+ MOZ_ASSERT(!mRespondWithPromiseHolder.IsEmpty());
+ MOZ_ASSERT(!mPromiseHolder.IsEmpty());
+
+ nsresult rv = DispatchFetchEvent(aCx, aWorkerPrivate);
+
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ RejectAll(rv);
+ }
+
+ return NS_SUCCEEDED(rv);
+}
+
+void FetchEventOp::AsyncLog(const nsCString& aMessageName,
+ nsTArray<nsString> aParams) {
+ MOZ_ASSERT(mActor);
+ MOZ_ASSERT(!mPromiseHolder.IsEmpty());
+ MOZ_ASSERT(mRespondWithClosure);
+
+ const FetchEventRespondWithClosure& closure = mRespondWithClosure.ref();
+
+ AsyncLog(closure.respondWithScriptSpec(), closure.respondWithLineNumber(),
+ closure.respondWithColumnNumber(), aMessageName, std::move(aParams));
+}
+
+void FetchEventOp::AsyncLog(const nsCString& aScriptSpec, uint32_t aLineNumber,
+ uint32_t aColumnNumber,
+ const nsCString& aMessageName,
+ nsTArray<nsString> aParams) {
+ MOZ_ASSERT(mActor);
+ MOZ_ASSERT(!mPromiseHolder.IsEmpty());
+
+ // Capture `this` because FetchEventOpProxyChild (mActor) is not thread
+ // safe, so an AddRef from RefPtr<FetchEventOpProxyChild>'s constructor will
+ // assert.
+ RefPtr<FetchEventOp> self = this;
+
+ nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction(
+ __func__, [self = std::move(self), spec = aScriptSpec, line = aLineNumber,
+ column = aColumnNumber, messageName = aMessageName,
+ params = std::move(aParams)] {
+ if (NS_WARN_IF(!self->mActor)) {
+ return;
+ }
+
+ Unused << self->mActor->SendAsyncLog(spec, line, column, messageName,
+ params);
+ });
+
+ MOZ_ALWAYS_SUCCEEDS(
+ RemoteWorkerService::Thread()->Dispatch(r.forget(), NS_DISPATCH_NORMAL));
+}
+
+void FetchEventOp::GetRequestURL(nsAString& aOutRequestURL) {
+ nsTArray<nsCString>& urls =
+ mArgs.get_ParentToChildServiceWorkerFetchEventOpArgs()
+ .common()
+ .internalRequest()
+ .urlList();
+ MOZ_ASSERT(!urls.IsEmpty());
+
+ CopyUTF8toUTF16(urls.LastElement(), aOutRequestURL);
+}
+
+void FetchEventOp::ResolvedCallback(JSContext* aCx,
+ JS::Handle<JS::Value> aValue,
+ ErrorResult& aRv) {
+ MOZ_ASSERT(IsCurrentThreadRunningWorker());
+ MOZ_ASSERT(mRespondWithClosure);
+ MOZ_ASSERT(!mRespondWithPromiseHolder.IsEmpty());
+ MOZ_ASSERT(!mPromiseHolder.IsEmpty());
+
+ mFetchHandlerFinish = TimeStamp::Now();
+
+ nsAutoString requestURL;
+ GetRequestURL(requestURL);
+
+ AutoCancel autoCancel(this);
+
+ if (!aValue.isObject()) {
+ NS_WARNING(
+ "FetchEvent::RespondWith was passed a promise resolved to a "
+ "non-Object "
+ "value");
+
+ nsCString sourceSpec;
+ uint32_t line = 0;
+ uint32_t column = 0;
+ nsString valueString;
+ nsContentUtils::ExtractErrorValues(aCx, aValue, sourceSpec, &line, &column,
+ valueString);
+
+ autoCancel.SetCancelMessageAndLocation(sourceSpec, line, column,
+ "InterceptedNonResponseWithURL"_ns,
+ requestURL, valueString);
+ return;
+ }
+
+ RefPtr<Response> response;
+ nsresult rv = UNWRAP_OBJECT(Response, &aValue.toObject(), response);
+ if (NS_FAILED(rv)) {
+ nsCString sourceSpec;
+ uint32_t line = 0;
+ uint32_t column = 0;
+ nsString valueString;
+ nsContentUtils::ExtractErrorValues(aCx, aValue, sourceSpec, &line, &column,
+ valueString);
+
+ autoCancel.SetCancelMessageAndLocation(sourceSpec, line, column,
+ "InterceptedNonResponseWithURL"_ns,
+ requestURL, valueString);
+ return;
+ }
+
+ WorkerPrivate* worker = GetCurrentThreadWorkerPrivate();
+ MOZ_ASSERT(worker);
+ worker->AssertIsOnWorkerThread();
+
+ // Section "HTTP Fetch", step 3.3:
+ // If one of the following conditions is true, return a network error:
+ // * response's type is "error".
+ // * request's mode is not "no-cors" and response's type is "opaque".
+ // * request's redirect mode is not "manual" and response's type is
+ // "opaqueredirect".
+ // * request's redirect mode is not "follow" and response's url list
+ // has more than one item.
+
+ if (response->Type() == ResponseType::Error) {
+ autoCancel.SetCancelMessage("InterceptedErrorResponseWithURL"_ns,
+ requestURL);
+ return;
+ }
+
+ const ParentToChildServiceWorkerFetchEventOpArgs& args =
+ mArgs.get_ParentToChildServiceWorkerFetchEventOpArgs();
+ const RequestMode requestMode = args.common().internalRequest().requestMode();
+
+ if (response->Type() == ResponseType::Opaque &&
+ requestMode != RequestMode::No_cors) {
+ NS_ConvertASCIItoUTF16 modeString(
+ RequestModeValues::GetString(requestMode));
+
+ nsAutoString requestURL;
+ GetRequestURL(requestURL);
+
+ autoCancel.SetCancelMessage("BadOpaqueInterceptionRequestModeWithURL"_ns,
+ requestURL, modeString);
+ return;
+ }
+
+ const RequestRedirect requestRedirectMode =
+ args.common().internalRequest().requestRedirect();
+
+ if (requestRedirectMode != RequestRedirect::Manual &&
+ response->Type() == ResponseType::Opaqueredirect) {
+ autoCancel.SetCancelMessage("BadOpaqueRedirectInterceptionWithURL"_ns,
+ requestURL);
+ return;
+ }
+
+ if (requestRedirectMode != RequestRedirect::Follow &&
+ response->Redirected()) {
+ autoCancel.SetCancelMessage("BadRedirectModeInterceptionWithURL"_ns,
+ requestURL);
+ return;
+ }
+
+ {
+ ErrorResult error;
+ bool bodyUsed = response->GetBodyUsed(error);
+ error.WouldReportJSException();
+ if (NS_WARN_IF(error.Failed())) {
+ autoCancel.SetCancelErrorResult(aCx, error);
+ return;
+ }
+ if (NS_WARN_IF(bodyUsed)) {
+ autoCancel.SetCancelMessage("InterceptedUsedResponseWithURL"_ns,
+ requestURL);
+ return;
+ }
+ }
+
+ SafeRefPtr<InternalResponse> ir = response->GetInternalResponse();
+ if (NS_WARN_IF(!ir)) {
+ return;
+ }
+
+ // An extra safety check to make sure our invariant that opaque and cors
+ // responses always have a URL does not break.
+ if (NS_WARN_IF((response->Type() == ResponseType::Opaque ||
+ response->Type() == ResponseType::Cors) &&
+ ir->GetUnfilteredURL().IsEmpty())) {
+ MOZ_DIAGNOSTIC_ASSERT(false, "Cors or opaque Response without a URL");
+ return;
+ }
+
+ if (requestMode == RequestMode::Same_origin &&
+ response->Type() == ResponseType::Cors) {
+ Telemetry::ScalarAdd(Telemetry::ScalarID::SW_CORS_RES_FOR_SO_REQ_COUNT, 1);
+
+ // XXXtt: Will have a pref to enable the quirk response in bug 1419684.
+ // The variadic template provided by StringArrayAppender requires exactly
+ // an nsString.
+ NS_ConvertUTF8toUTF16 responseURL(ir->GetUnfilteredURL());
+ autoCancel.SetCancelMessage("CorsResponseForSameOriginRequest"_ns,
+ requestURL, responseURL);
+ return;
+ }
+
+ nsCOMPtr<nsIInputStream> body;
+ ir->GetUnfilteredBody(getter_AddRefs(body));
+ // Errors and redirects may not have a body.
+ if (body) {
+ ErrorResult error;
+ response->SetBodyUsed(aCx, error);
+ error.WouldReportJSException();
+ if (NS_WARN_IF(error.Failed())) {
+ autoCancel.SetCancelErrorResult(aCx, error);
+ return;
+ }
+ }
+
+ if (!ir->GetChannelInfo().IsInitialized()) {
+ // This is a synthetic response (I think and hope so).
+ ir->InitChannelInfo(worker->GetChannelInfo());
+ }
+
+ autoCancel.Reset();
+
+ // https://w3c.github.io/ServiceWorker/#on-fetch-request-algorithm Step 26: If
+ // eventHandled is not null, then resolve eventHandled.
+ //
+ // mRespondWithPromiseHolder will resolve a MozPromise that will resolve on
+ // the worker owner's thread, so it's fine to resolve the mHandled promise now
+ // because content will not interfere with respondWith getting the Response to
+ // where it's going.
+ mHandled->MaybeResolveWithUndefined();
+ mRespondWithPromiseHolder.Resolve(
+ FetchEventRespondWithResult(MakeTuple(
+ std::move(ir), mRespondWithClosure.ref(),
+ FetchEventTimeStamps(mFetchHandlerStart, mFetchHandlerFinish))),
+ __func__);
+}
+
+void FetchEventOp::RejectedCallback(JSContext* aCx,
+ JS::Handle<JS::Value> aValue,
+ ErrorResult& aRv) {
+ MOZ_ASSERT(IsCurrentThreadRunningWorker());
+ MOZ_ASSERT(mRespondWithClosure);
+ MOZ_ASSERT(!mRespondWithPromiseHolder.IsEmpty());
+ MOZ_ASSERT(!mPromiseHolder.IsEmpty());
+
+ mFetchHandlerFinish = TimeStamp::Now();
+
+ FetchEventRespondWithClosure& closure = mRespondWithClosure.ref();
+
+ nsCString sourceSpec = closure.respondWithScriptSpec();
+ uint32_t line = closure.respondWithLineNumber();
+ uint32_t column = closure.respondWithColumnNumber();
+ nsString valueString;
+
+ nsContentUtils::ExtractErrorValues(aCx, aValue, sourceSpec, &line, &column,
+ valueString);
+
+ nsString requestURL;
+ GetRequestURL(requestURL);
+
+ AsyncLog(sourceSpec, line, column, "InterceptionRejectedResponseWithURL"_ns,
+ {std::move(requestURL), valueString});
+
+ // https://w3c.github.io/ServiceWorker/#on-fetch-request-algorithm Step 25.1:
+ // If eventHandled is not null, then reject eventHandled with a "NetworkError"
+ // DOMException in workerRealm.
+ mHandled->MaybeRejectWithNetworkError(
+ "FetchEvent.respondWith() Promise rejected"_ns);
+ mRespondWithPromiseHolder.Resolve(
+ FetchEventRespondWithResult(CancelInterceptionArgs(
+ NS_ERROR_INTERCEPTION_FAILED,
+ FetchEventTimeStamps(mFetchHandlerStart, mFetchHandlerFinish))),
+ __func__);
+}
+
+nsresult FetchEventOp::DispatchFetchEvent(JSContext* aCx,
+ WorkerPrivate* aWorkerPrivate) {
+ MOZ_ASSERT(aCx);
+ MOZ_ASSERT(aWorkerPrivate);
+ aWorkerPrivate->AssertIsOnWorkerThread();
+ MOZ_ASSERT(aWorkerPrivate->IsServiceWorker());
+
+ ParentToChildServiceWorkerFetchEventOpArgs& args =
+ mArgs.get_ParentToChildServiceWorkerFetchEventOpArgs();
+
+ /**
+ * Testing: Failure injection.
+ *
+ * There are a number of different ways that this fetch event could have
+ * failed that would result in cancellation. This injection point helps
+ * simulate them without worrying about shifting implementation details with
+ * full fidelity reproductions of current scenarios.
+ *
+ * Broadly speaking, we expect fetch event scenarios to fail because of:
+ * - Script load failure, which results in the CompileScriptRunnable closing
+ * the worker and thereby cancelling all pending operations, including this
+ * fetch. The `ServiceWorkerOp::Cancel` impl just calls
+ * RejectAll(NS_ERROR_DOM_ABORT_ERR) which we are able to approximate by
+ * returning the same nsresult here, as our caller also calls RejectAll.
+ * (And timing-wise, this rejection will happen in the correct sequence.)
+ * - An exception gets thrown in the processing of the promise that was passed
+ * to respondWith and it ends up rejecting. The rejection will be converted
+ * by `FetchEventOp::RejectedCallback` into a cancellation with
+ * NS_ERROR_INTERCEPTION_FAILED, and by returning that here we approximate
+ * that failure mode.
+ */
+ if (NS_FAILED(args.common().testingInjectCancellation())) {
+ return args.common().testingInjectCancellation();
+ }
+
+ /**
+ * Step 1: get the InternalRequest. The InternalRequest can't be constructed
+ * here from mArgs because the IPCStream has to be deserialized on the
+ * thread receiving the ServiceWorkerFetchEventOpArgs.
+ * FetchEventOpProxyChild will have already deserialized the stream on the
+ * correct thread before creating this op, so we can take its saved
+ * InternalRequest.
+ */
+ SafeRefPtr<InternalRequest> internalRequest =
+ mActor->ExtractInternalRequest();
+
+ /**
+ * Step 2: get the worker's global object
+ */
+ GlobalObject globalObject(aCx, aWorkerPrivate->GlobalScope()->GetWrapper());
+ nsCOMPtr<nsIGlobalObject> globalObjectAsSupports =
+ do_QueryInterface(globalObject.GetAsSupports());
+ if (NS_WARN_IF(!globalObjectAsSupports)) {
+ return NS_ERROR_DOM_INVALID_STATE_ERR;
+ }
+
+ /**
+ * Step 3: create the public DOM Request object
+ * TODO: this Request object should be created with an AbortSignal object
+ * which should be aborted if the loading is aborted. See but 1394102.
+ */
+ RefPtr<Request> request =
+ new Request(globalObjectAsSupports, internalRequest.clonePtr(), nullptr);
+ MOZ_ASSERT_IF(internalRequest->IsNavigationRequest(),
+ request->Redirect() == RequestRedirect::Manual);
+
+ /**
+ * Step 4a: create the FetchEventInit
+ */
+ RootedDictionary<FetchEventInit> fetchEventInit(aCx);
+ fetchEventInit.mRequest = request;
+ fetchEventInit.mBubbles = false;
+ fetchEventInit.mCancelable = true;
+
+ /**
+ * TODO: only expose the FetchEvent.clientId on subresource requests for
+ * now. Once we implement .targetClientId we can then start exposing
+ * .clientId on non-subresource requests as well. See bug 1487534.
+ */
+ if (!args.common().clientId().IsEmpty() &&
+ !internalRequest->IsNavigationRequest()) {
+ fetchEventInit.mClientId = args.common().clientId();
+ }
+
+ /*
+ * https://w3c.github.io/ServiceWorker/#on-fetch-request-algorithm
+ *
+ * "If request is a non-subresource request and request’s
+ * destination is not "report", initialize e’s resultingClientId attribute
+ * to reservedClient’s [resultingClient's] id, and to the empty string
+ * otherwise." (Step 18.8)
+ */
+ if (!args.common().resultingClientId().IsEmpty() &&
+ args.common().isNonSubresourceRequest() &&
+ internalRequest->Destination() != RequestDestination::Report) {
+ fetchEventInit.mResultingClientId = args.common().resultingClientId();
+ }
+
+ /**
+ * Step 4b: create the FetchEvent
+ */
+ RefPtr<FetchEvent> fetchEvent =
+ FetchEvent::Constructor(globalObject, u"fetch"_ns, fetchEventInit);
+ fetchEvent->SetTrusted(true);
+ fetchEvent->PostInit(args.common().workerScriptSpec(), this);
+ mHandled = fetchEvent->Handled();
+ mPreloadResponse = fetchEvent->PreloadResponse();
+
+ if (args.common().preloadNavigation()) {
+ RefPtr<FetchEventPreloadResponseAvailablePromise> preloadResponsePromise =
+ mActor->GetPreloadResponseAvailablePromise();
+ MOZ_ASSERT(preloadResponsePromise);
+
+ // If preloadResponsePromise has already settled then this callback will get
+ // run synchronously here.
+ RefPtr<FetchEventOp> self = this;
+ preloadResponsePromise
+ ->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [self, globalObjectAsSupports](
+ SafeRefPtr<InternalResponse>&& aPreloadResponse) {
+ self->mPreloadResponse->MaybeResolve(
+ MakeRefPtr<Response>(globalObjectAsSupports,
+ std::move(aPreloadResponse), nullptr));
+ self->mPreloadResponseAvailablePromiseRequestHolder.Complete();
+ },
+ [self](int) {
+ self->mPreloadResponseAvailablePromiseRequestHolder.Complete();
+ })
+ ->Track(mPreloadResponseAvailablePromiseRequestHolder);
+
+ RefPtr<PerformanceStorage> performanceStorage =
+ aWorkerPrivate->GetPerformanceStorage();
+
+ RefPtr<FetchEventPreloadResponseEndPromise> preloadResponseEndPromise =
+ mActor->GetPreloadResponseEndPromise();
+ MOZ_ASSERT(preloadResponseEndPromise);
+ preloadResponseEndPromise
+ ->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [self, performanceStorage,
+ globalObjectAsSupports](ResponseEndArgs&& aArgs) {
+ if (aArgs.timing().isSome() && performanceStorage) {
+ performanceStorage->AddEntry(
+ aArgs.timing().ref().entryName(),
+ aArgs.timing().ref().initiatorType(),
+ MakeUnique<PerformanceTimingData>(
+ aArgs.timing().ref().timingData()));
+ }
+ if (aArgs.endReason() == FetchDriverObserver::eAborted) {
+ self->mPreloadResponse->MaybeReject(NS_ERROR_DOM_ABORT_ERR);
+ }
+ self->mPreloadResponseEndPromiseRequestHolder.Complete();
+ },
+ [self](int) {
+ self->mPreloadResponseEndPromiseRequestHolder.Complete();
+ })
+ ->Track(mPreloadResponseEndPromiseRequestHolder);
+ } else {
+ // preload navigation is disabled, resolved preload response promise with
+ // undefined as default behavior.
+ mPreloadResponse->MaybeResolveWithUndefined();
+ }
+
+ mFetchHandlerStart = TimeStamp::Now();
+
+ /**
+ * Step 5: Dispatch the FetchEvent to the worker's global object
+ */
+ nsresult rv = DispatchExtendableEventOnWorkerScope(
+ aCx, aWorkerPrivate->GlobalScope(), fetchEvent, this);
+ bool dispatchFailed = NS_FAILED(rv) && rv != NS_ERROR_XPC_JS_THREW_EXCEPTION;
+
+ if (NS_WARN_IF(dispatchFailed)) {
+ mHandled = nullptr;
+ mPreloadResponse = nullptr;
+ return rv;
+ }
+
+ /**
+ * At this point, there are 4 (legal) scenarios:
+ *
+ * 1) If neither waitUntil() nor respondWith() are called,
+ * DispatchExtendableEventOnWorkerScope() will have already called
+ * FinishedWithResult(), but this call will have recorded the result
+ * (mResult) and returned early so that mRespondWithPromiseHolder can be
+ * settled first. mRespondWithPromiseHolder will be settled below, followed
+ * by a call to MaybeFinished() which settles mPromiseHolder.
+ *
+ * 2) If waitUntil() is called at least once, and respondWith() is not
+ * called, DispatchExtendableEventOnWorkerScope() will NOT have called
+ * FinishedWithResult(). We'll settle mRespondWithPromiseHolder first, and
+ * at some point in the future when the last waitUntil() promise settles,
+ * FinishedWithResult() will be called, settling mPromiseHolder.
+ *
+ * 3) If waitUntil() is not called, and respondWith() is called,
+ * DispatchExtendableEventOnWorkerScope() will NOT have called
+ * FinishedWithResult(). We can also guarantee that
+ * mRespondWithPromiseHolder will be settled before mPromiseHolder, due to
+ * the Promise::AppendNativeHandler() call ordering in
+ * FetchEvent::RespondWith().
+ *
+ * 4) If waitUntil() is called at least once, and respondWith() is also
+ * called, the effect is similar to scenario 3), with the most imporant
+ * property being mRespondWithPromiseHolder settling before mPromiseHolder.
+ *
+ * Note that if mPromiseHolder is settled before mRespondWithPromiseHolder,
+ * FetchEventOpChild will cancel the interception.
+ */
+ if (!fetchEvent->WaitToRespond()) {
+ MOZ_ASSERT(!mRespondWithPromiseHolder.IsEmpty());
+ MOZ_ASSERT(!aWorkerPrivate->UsesSystemPrincipal(),
+ "We don't support system-principal serviceworkers");
+
+ mFetchHandlerFinish = TimeStamp::Now();
+
+ if (fetchEvent->DefaultPrevented(CallerType::NonSystem)) {
+ // https://w3c.github.io/ServiceWorker/#on-fetch-request-algorithm
+ // Step 24.1.1: If eventHandled is not null, then reject eventHandled with
+ // a "NetworkError" DOMException in workerRealm.
+ mHandled->MaybeRejectWithNetworkError(
+ "FetchEvent.preventDefault() called"_ns);
+ mRespondWithPromiseHolder.Resolve(
+ FetchEventRespondWithResult(CancelInterceptionArgs(
+ NS_ERROR_INTERCEPTION_FAILED,
+ FetchEventTimeStamps(mFetchHandlerStart, mFetchHandlerFinish))),
+ __func__);
+ } else {
+ // https://w3c.github.io/ServiceWorker/#on-fetch-request-algorithm
+ // Step 24.2: If eventHandled is not null, then resolve eventHandled.
+ mHandled->MaybeResolveWithUndefined();
+ mRespondWithPromiseHolder.Resolve(
+ FetchEventRespondWithResult(ResetInterceptionArgs(
+ FetchEventTimeStamps(mFetchHandlerStart, mFetchHandlerFinish))),
+ __func__);
+ }
+ } else {
+ MOZ_ASSERT(mRespondWithClosure);
+ }
+
+ mPostDispatchChecksDone = true;
+ MaybeFinished();
+
+ return NS_OK;
+}
+
+class ExtensionAPIEventOp final : public ServiceWorkerOp {
+ using ServiceWorkerOp::ServiceWorkerOp;
+
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(ExtensionAPIEventOp, override)
+
+ private:
+ ~ExtensionAPIEventOp() = default;
+
+ bool Exec(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override {
+ MOZ_ASSERT(aWorkerPrivate);
+ aWorkerPrivate->AssertIsOnWorkerThread();
+ MOZ_ASSERT(aWorkerPrivate->IsServiceWorker());
+ MOZ_ASSERT(aWorkerPrivate->ExtensionAPIAllowed());
+ MOZ_ASSERT(!mPromiseHolder.IsEmpty());
+
+ ServiceWorkerExtensionAPIEventOpArgs& args =
+ mArgs.get_ServiceWorkerExtensionAPIEventOpArgs();
+
+ ServiceWorkerExtensionAPIEventOpResult result;
+ result.extensionAPIEventListenerWasAdded() = false;
+
+ if (aWorkerPrivate->WorkerScriptExecutedSuccessfully()) {
+ GlobalObject globalObj(aCx, aWorkerPrivate->GlobalScope()->GetWrapper());
+ RefPtr<ServiceWorkerGlobalScope> scope;
+ UNWRAP_OBJECT(ServiceWorkerGlobalScope, globalObj.Get(), scope);
+ SafeRefPtr<extensions::ExtensionBrowser> extensionAPI =
+ scope->AcquireExtensionBrowser();
+ if (!extensionAPI) {
+ // If the worker script did never access the WebExtension APIs
+ // then we can return earlier, no event listener could have been added.
+ mPromiseHolder.Resolve(result, __func__);
+ return true;
+ }
+ // Check if a listener has been subscribed on the expected WebExtensions
+ // API event.
+ bool hasWakeupListener = extensionAPI->HasWakeupEventListener(
+ args.apiNamespace(), args.apiEventName());
+ result.extensionAPIEventListenerWasAdded() = hasWakeupListener;
+ mPromiseHolder.Resolve(result, __func__);
+ } else {
+ mPromiseHolder.Resolve(result, __func__);
+ }
+
+ return true;
+ }
+};
+
+/* static */ already_AddRefed<ServiceWorkerOp> ServiceWorkerOp::Create(
+ ServiceWorkerOpArgs&& aArgs,
+ std::function<void(const ServiceWorkerOpResult&)>&& aCallback) {
+ MOZ_ASSERT(RemoteWorkerService::Thread()->IsOnCurrentThread());
+
+ RefPtr<ServiceWorkerOp> op;
+
+ switch (aArgs.type()) {
+ case ServiceWorkerOpArgs::TServiceWorkerCheckScriptEvaluationOpArgs:
+ op = MakeRefPtr<CheckScriptEvaluationOp>(std::move(aArgs),
+ std::move(aCallback));
+ break;
+ case ServiceWorkerOpArgs::TServiceWorkerUpdateStateOpArgs:
+ op = MakeRefPtr<UpdateServiceWorkerStateOp>(std::move(aArgs),
+ std::move(aCallback));
+ break;
+ case ServiceWorkerOpArgs::TServiceWorkerTerminateWorkerOpArgs:
+ op = MakeRefPtr<TerminateServiceWorkerOp>(std::move(aArgs),
+ std::move(aCallback));
+ break;
+ case ServiceWorkerOpArgs::TServiceWorkerLifeCycleEventOpArgs:
+ op = MakeRefPtr<LifeCycleEventOp>(std::move(aArgs), std::move(aCallback));
+ break;
+ case ServiceWorkerOpArgs::TServiceWorkerPushEventOpArgs:
+ op = MakeRefPtr<PushEventOp>(std::move(aArgs), std::move(aCallback));
+ break;
+ case ServiceWorkerOpArgs::TServiceWorkerPushSubscriptionChangeEventOpArgs:
+ op = MakeRefPtr<PushSubscriptionChangeEventOp>(std::move(aArgs),
+ std::move(aCallback));
+ break;
+ case ServiceWorkerOpArgs::TServiceWorkerNotificationEventOpArgs:
+ op = MakeRefPtr<NotificationEventOp>(std::move(aArgs),
+ std::move(aCallback));
+ break;
+ case ServiceWorkerOpArgs::TServiceWorkerMessageEventOpArgs:
+ op = MakeRefPtr<MessageEventOp>(std::move(aArgs), std::move(aCallback));
+ break;
+ case ServiceWorkerOpArgs::TParentToChildServiceWorkerFetchEventOpArgs:
+ op = MakeRefPtr<FetchEventOp>(std::move(aArgs), std::move(aCallback));
+ break;
+ case ServiceWorkerOpArgs::TServiceWorkerExtensionAPIEventOpArgs:
+ op = MakeRefPtr<ExtensionAPIEventOp>(std::move(aArgs),
+ std::move(aCallback));
+ break;
+ default:
+ MOZ_CRASH("Unknown Service Worker operation!");
+ return nullptr;
+ }
+
+ return op.forget();
+}
+
+} // namespace mozilla::dom
diff --git a/dom/serviceworkers/ServiceWorkerOp.h b/dom/serviceworkers/ServiceWorkerOp.h
new file mode 100644
index 0000000000..b916b6dea4
--- /dev/null
+++ b/dom/serviceworkers/ServiceWorkerOp.h
@@ -0,0 +1,195 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_serviceworkerop_h__
+#define mozilla_dom_serviceworkerop_h__
+
+#include <functional>
+
+#include "mozilla/dom/ServiceWorkerOpPromise.h"
+#include "nsISupportsImpl.h"
+
+#include "ServiceWorkerEvents.h"
+#include "ServiceWorkerOpPromise.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/TimeStamp.h"
+#include "mozilla/dom/PromiseNativeHandler.h"
+#include "mozilla/dom/RemoteWorkerChild.h"
+#include "mozilla/dom/ServiceWorkerOpArgs.h"
+#include "mozilla/dom/WorkerRunnable.h"
+
+namespace mozilla::dom {
+
+class FetchEventOpProxyChild;
+
+class ServiceWorkerOp : public RemoteWorkerChild::Op {
+ public:
+ // `aCallback` will be called when the operation completes or is canceled.
+ static already_AddRefed<ServiceWorkerOp> Create(
+ ServiceWorkerOpArgs&& aArgs,
+ std::function<void(const ServiceWorkerOpResult&)>&& aCallback);
+
+ ServiceWorkerOp(
+ ServiceWorkerOpArgs&& aArgs,
+ std::function<void(const ServiceWorkerOpResult&)>&& aCallback);
+
+ ServiceWorkerOp(const ServiceWorkerOp&) = delete;
+
+ ServiceWorkerOp& operator=(const ServiceWorkerOp&) = delete;
+
+ ServiceWorkerOp(ServiceWorkerOp&&) = default;
+
+ ServiceWorkerOp& operator=(ServiceWorkerOp&&) = default;
+
+ // Returns `true` if the operation has started and `false` otherwise.
+ bool MaybeStart(RemoteWorkerChild* aOwner,
+ RemoteWorkerChild::State& aState) final;
+
+ void Cancel() final;
+
+ protected:
+ ~ServiceWorkerOp();
+
+ bool Started() const;
+
+ bool IsTerminationOp() const;
+
+ // Override to provide a runnable that's not a `ServiceWorkerOpRunnable.`
+ virtual RefPtr<WorkerRunnable> GetRunnable(WorkerPrivate* aWorkerPrivate);
+
+ // Overridden by ServiceWorkerOp subclasses, it should return true when
+ // the ServiceWorkerOp was executed successfully (and false if it did fail).
+ // Content throwing an exception during event dispatch is still considered
+ // success.
+ virtual bool Exec(JSContext* aCx, WorkerPrivate* aWorkerPrivate) = 0;
+
+ // Override to reject any additional MozPromises that subclasses may contain.
+ virtual void RejectAll(nsresult aStatus);
+
+ ServiceWorkerOpArgs mArgs;
+
+ // Subclasses must settle this promise when appropriate.
+ MozPromiseHolder<ServiceWorkerOpPromise> mPromiseHolder;
+
+ private:
+ class ServiceWorkerOpRunnable;
+
+ bool mStarted = false;
+};
+
+class ExtendableEventOp : public ServiceWorkerOp,
+ public ExtendableEventCallback {
+ using ServiceWorkerOp::ServiceWorkerOp;
+
+ protected:
+ ~ExtendableEventOp() = default;
+
+ void FinishedWithResult(ExtendableEventResult aResult) override;
+};
+
+class FetchEventOp final : public ExtendableEventOp,
+ public PromiseNativeHandler {
+ using ExtendableEventOp::ExtendableEventOp;
+
+ public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+
+ /**
+ * This must be called once and only once before the first call to
+ * `MaybeStart()`; `aActor` will be used for `AsyncLog()` and
+ * `ReportCanceled().`
+ */
+ void SetActor(RefPtr<FetchEventOpProxyChild> aActor);
+
+ void RevokeActor(FetchEventOpProxyChild* aActor);
+
+ // This must be called at most once before the first call to `MaybeStart().`
+ RefPtr<FetchEventRespondWithPromise> GetRespondWithPromise();
+
+ // This must be called when `FetchEvent::RespondWith()` is called.
+ void RespondWithCalledAt(const nsCString& aRespondWithScriptSpec,
+ uint32_t aRespondWithLineNumber,
+ uint32_t aRespondWithColumnNumber);
+
+ void ReportCanceled(const nsCString& aPreventDefaultScriptSpec,
+ uint32_t aPreventDefaultLineNumber,
+ uint32_t aPreventDefaultColumnNumber);
+
+ private:
+ class AutoCancel;
+
+ ~FetchEventOp();
+
+ bool Exec(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override;
+
+ void RejectAll(nsresult aStatus) override;
+
+ void FinishedWithResult(ExtendableEventResult aResult) override;
+
+ /**
+ * `{Resolved,Reject}Callback()` are use to handle the
+ * `FetchEvent::RespondWith()` promise.
+ */
+ void ResolvedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue,
+ ErrorResult& aRv) override;
+
+ void RejectedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue,
+ ErrorResult& aRv) override;
+
+ void MaybeFinished();
+
+ // Requires mRespondWithClosure to be non-empty.
+ void AsyncLog(const nsCString& aMessageName, nsTArray<nsString> aParams);
+
+ void AsyncLog(const nsCString& aScriptSpec, uint32_t aLineNumber,
+ uint32_t aColumnNumber, const nsCString& aMessageName,
+ nsTArray<nsString> aParams);
+
+ void GetRequestURL(nsAString& aOutRequestURL);
+
+ // A failure code means that the dispatch failed.
+ nsresult DispatchFetchEvent(JSContext* aCx, WorkerPrivate* aWorkerPrivate);
+
+ // Worker Launcher thread only. Used for `AsyncLog().`
+ RefPtr<FetchEventOpProxyChild> mActor;
+
+ /**
+ * Created on the Worker Launcher thread and settled on the worker thread.
+ * If this isn't settled before `mPromiseHolder` (which it should be),
+ * `FetchEventOpChild` will cancel the intercepted network request.
+ */
+ MozPromiseHolder<FetchEventRespondWithPromise> mRespondWithPromiseHolder;
+
+ // Worker thread only.
+ Maybe<ExtendableEventResult> mResult;
+ bool mPostDispatchChecksDone = false;
+
+ // Worker thread only; set when `FetchEvent::RespondWith()` is called.
+ Maybe<FetchEventRespondWithClosure> mRespondWithClosure;
+
+ // Must be set to `nullptr` on the worker thread because `Promise`'s
+ // destructor must be called on the worker thread.
+ RefPtr<Promise> mHandled;
+
+ // Must be set to `nullptr` on the worker thread because `Promise`'s
+ // destructor must be called on the worker thread.
+ RefPtr<Promise> mPreloadResponse;
+
+ // Holds the callback that resolves mPreloadResponse.
+ MozPromiseRequestHolder<FetchEventPreloadResponseAvailablePromise>
+ mPreloadResponseAvailablePromiseRequestHolder;
+ MozPromiseRequestHolder<FetchEventPreloadResponseEndPromise>
+ mPreloadResponseEndPromiseRequestHolder;
+
+ TimeStamp mFetchHandlerStart;
+ TimeStamp mFetchHandlerFinish;
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_serviceworkerop_h__
diff --git a/dom/serviceworkers/ServiceWorkerOpArgs.ipdlh b/dom/serviceworkers/ServiceWorkerOpArgs.ipdlh
new file mode 100644
index 0000000000..55c0525a39
--- /dev/null
+++ b/dom/serviceworkers/ServiceWorkerOpArgs.ipdlh
@@ -0,0 +1,189 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+include ClientIPCTypes;
+include DOMTypes;
+include FetchTypes;
+
+include "mozilla/dom/ServiceWorkerIPCUtils.h";
+
+using ServiceWorkerState from "mozilla/dom/ServiceWorkerBinding.h";
+using mozilla::TimeStamp from "mozilla/TimeStamp.h";
+
+namespace mozilla {
+namespace dom {
+
+/**
+ * ServiceWorkerOpArgs
+ */
+struct ServiceWorkerCheckScriptEvaluationOpArgs {};
+
+struct ServiceWorkerUpdateStateOpArgs {
+ ServiceWorkerState state;
+};
+
+struct ServiceWorkerTerminateWorkerOpArgs {
+ uint32_t shutdownStateId;
+};
+
+struct ServiceWorkerLifeCycleEventOpArgs {
+ nsString eventName;
+};
+
+// Possibly need to differentiate an empty array from the absence of an array.
+union OptionalPushData {
+ void_t;
+ uint8_t[];
+};
+
+struct ServiceWorkerPushEventOpArgs {
+ nsString messageId;
+ OptionalPushData data;
+};
+
+struct ServiceWorkerPushSubscriptionChangeEventOpArgs {};
+
+struct ServiceWorkerNotificationEventOpArgs {
+ nsString eventName;
+ nsString id;
+ nsString title;
+ nsString dir;
+ nsString lang;
+ nsString body;
+ nsString tag;
+ nsString icon;
+ nsString data;
+ nsString behavior;
+ nsString scope;
+ uint32_t disableOpenClickDelay;
+};
+
+struct ServiceWorkerExtensionAPIEventOpArgs {
+ // WebExtensions API namespace and event names, for a list of the API namespaces
+ // and related API event names refer to the API JSONSchema files in-tree:
+ //
+ // https://searchfox.org/mozilla-central/search?q=&path=extensions%2Fschemas%2F*.json
+ nsString apiNamespace;
+ nsString apiEventName;
+};
+
+struct ServiceWorkerMessageEventOpArgs {
+ ClientInfoAndState clientInfoAndState;
+ ClonedOrErrorMessageData clonedData;
+};
+
+struct ServiceWorkerFetchEventOpArgsCommon {
+ nsCString workerScriptSpec;
+ IPCInternalRequest internalRequest;
+ nsString clientId;
+ nsString resultingClientId;
+ bool isNonSubresourceRequest;
+ // Is navigation preload enabled for this fetch? If true, if some
+ // preloadResponse was not already provided in this structure, then it's
+ // expected that a PreloadResponse message will eventually be sent.
+ bool preloadNavigation;
+ // Failure injection helper; non-NS_OK values indicate that the event, instead
+ // of dispatching should instead return a `CancelInterceptionArgs` response
+ // with this nsresult. This value originates from
+ // `nsIServiceWorkerInfo::testingInjectCancellation`.
+ nsresult testingInjectCancellation;
+};
+
+struct ParentToParentServiceWorkerFetchEventOpArgs {
+ ServiceWorkerFetchEventOpArgsCommon common;
+ ParentToParentInternalResponse? preloadResponse;
+ ResponseEndArgs? preloadResponseEndArgs;
+};
+
+struct ParentToChildServiceWorkerFetchEventOpArgs {
+ ServiceWorkerFetchEventOpArgsCommon common;
+ ParentToChildInternalResponse? preloadResponse;
+ ResponseEndArgs? preloadResponseEndArgs;
+};
+
+union ServiceWorkerOpArgs {
+ ServiceWorkerCheckScriptEvaluationOpArgs;
+ ServiceWorkerUpdateStateOpArgs;
+ ServiceWorkerTerminateWorkerOpArgs;
+ ServiceWorkerLifeCycleEventOpArgs;
+ ServiceWorkerPushEventOpArgs;
+ ServiceWorkerPushSubscriptionChangeEventOpArgs;
+ ServiceWorkerNotificationEventOpArgs;
+ ServiceWorkerMessageEventOpArgs;
+ ServiceWorkerExtensionAPIEventOpArgs;
+ ParentToChildServiceWorkerFetchEventOpArgs;
+};
+
+/**
+ * IPCFetchEventRespondWithResult
+ */
+struct FetchEventRespondWithClosure {
+ nsCString respondWithScriptSpec;
+ uint32_t respondWithLineNumber;
+ uint32_t respondWithColumnNumber;
+};
+
+struct FetchEventTimeStamps {
+ TimeStamp fetchHandlerStart;
+ TimeStamp fetchHandlerFinish;
+};
+
+struct ChildToParentSynthesizeResponseArgs {
+ ChildToParentInternalResponse internalResponse;
+ FetchEventRespondWithClosure closure;
+ FetchEventTimeStamps timeStamps;
+};
+
+struct ParentToParentSynthesizeResponseArgs {
+ ParentToParentInternalResponse internalResponse;
+ FetchEventRespondWithClosure closure;
+ FetchEventTimeStamps timeStamps;
+};
+
+struct ResetInterceptionArgs {
+ FetchEventTimeStamps timeStamps;
+};
+
+struct CancelInterceptionArgs {
+ nsresult status;
+ FetchEventTimeStamps timeStamps;
+};
+
+union ChildToParentFetchEventRespondWithResult {
+ ChildToParentSynthesizeResponseArgs;
+ ResetInterceptionArgs;
+ CancelInterceptionArgs;
+};
+
+union ParentToParentFetchEventRespondWithResult {
+ ParentToParentSynthesizeResponseArgs;
+ ResetInterceptionArgs;
+ CancelInterceptionArgs;
+};
+
+/**
+ * ServiceWorkerOpResult
+ */
+struct ServiceWorkerCheckScriptEvaluationOpResult {
+ bool workerScriptExecutedSuccessfully;
+ bool fetchHandlerWasAdded;
+};
+
+struct ServiceWorkerFetchEventOpResult {
+ nsresult rv;
+};
+
+struct ServiceWorkerExtensionAPIEventOpResult {
+ bool extensionAPIEventListenerWasAdded;
+};
+
+union ServiceWorkerOpResult {
+ nsresult;
+ ServiceWorkerCheckScriptEvaluationOpResult;
+ ServiceWorkerFetchEventOpResult;
+ ServiceWorkerExtensionAPIEventOpResult;
+};
+
+} // namespace dom
+} // namespace mozilla
diff --git a/dom/serviceworkers/ServiceWorkerOpPromise.h b/dom/serviceworkers/ServiceWorkerOpPromise.h
new file mode 100644
index 0000000000..f46b645961
--- /dev/null
+++ b/dom/serviceworkers/ServiceWorkerOpPromise.h
@@ -0,0 +1,48 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_serviceworkeroppromise_h__
+#define mozilla_dom_serviceworkeroppromise_h__
+
+#include <utility>
+
+#include "mozilla/MozPromise.h"
+#include "mozilla/Tuple.h"
+#include "mozilla/dom/SafeRefPtr.h"
+#include "mozilla/dom/ServiceWorkerOpArgs.h"
+
+namespace mozilla::dom {
+
+class InternalResponse;
+
+using SynthesizeResponseArgs =
+ Tuple<SafeRefPtr<InternalResponse>, FetchEventRespondWithClosure,
+ FetchEventTimeStamps>;
+
+using FetchEventRespondWithResult =
+ Variant<SynthesizeResponseArgs, ResetInterceptionArgs,
+ CancelInterceptionArgs>;
+
+using FetchEventRespondWithPromise =
+ MozPromise<FetchEventRespondWithResult, CancelInterceptionArgs, true>;
+
+// The reject type int is arbitrary, since this promise will never get rejected.
+// Unfortunately void is not supported as a reject type.
+using FetchEventPreloadResponseAvailablePromise =
+ MozPromise<SafeRefPtr<InternalResponse>, int, true>;
+
+using FetchEventPreloadResponseEndPromise =
+ MozPromise<ResponseEndArgs, int, true>;
+
+using ServiceWorkerOpPromise =
+ MozPromise<ServiceWorkerOpResult, nsresult, true>;
+
+using ServiceWorkerFetchEventOpPromise =
+ MozPromise<ServiceWorkerFetchEventOpResult, nsresult, true>;
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_serviceworkeroppromise_h__
diff --git a/dom/serviceworkers/ServiceWorkerParent.cpp b/dom/serviceworkers/ServiceWorkerParent.cpp
new file mode 100644
index 0000000000..4e31344e92
--- /dev/null
+++ b/dom/serviceworkers/ServiceWorkerParent.cpp
@@ -0,0 +1,62 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "ServiceWorkerParent.h"
+
+#include "ServiceWorkerCloneData.h"
+#include "ServiceWorkerProxy.h"
+#include "mozilla/dom/ClientInfo.h"
+#include "mozilla/dom/ClientState.h"
+#include "mozilla/dom/ipc/StructuredCloneData.h"
+
+namespace mozilla::dom {
+
+using mozilla::dom::ipc::StructuredCloneData;
+using mozilla::ipc::IPCResult;
+
+void ServiceWorkerParent::ActorDestroy(ActorDestroyReason aReason) {
+ if (mProxy) {
+ mProxy->RevokeActor(this);
+ mProxy = nullptr;
+ }
+}
+
+IPCResult ServiceWorkerParent::RecvTeardown() {
+ MaybeSendDelete();
+ return IPC_OK();
+}
+
+IPCResult ServiceWorkerParent::RecvPostMessage(
+ const ClonedOrErrorMessageData& aClonedData,
+ const ClientInfoAndState& aSource) {
+ RefPtr<ServiceWorkerCloneData> data = new ServiceWorkerCloneData();
+ data->CopyFromClonedMessageData(aClonedData);
+
+ mProxy->PostMessage(std::move(data), ClientInfo(aSource.info()),
+ ClientState::FromIPC(aSource.state()));
+
+ return IPC_OK();
+}
+
+ServiceWorkerParent::ServiceWorkerParent() : mDeleteSent(false) {}
+
+ServiceWorkerParent::~ServiceWorkerParent() { MOZ_DIAGNOSTIC_ASSERT(!mProxy); }
+
+void ServiceWorkerParent::Init(const IPCServiceWorkerDescriptor& aDescriptor) {
+ MOZ_DIAGNOSTIC_ASSERT(!mProxy);
+ mProxy = new ServiceWorkerProxy(ServiceWorkerDescriptor(aDescriptor));
+ mProxy->Init(this);
+}
+
+void ServiceWorkerParent::MaybeSendDelete() {
+ if (mDeleteSent) {
+ return;
+ }
+ mDeleteSent = true;
+ Unused << Send__delete__(this);
+}
+
+} // namespace mozilla::dom
diff --git a/dom/serviceworkers/ServiceWorkerParent.h b/dom/serviceworkers/ServiceWorkerParent.h
new file mode 100644
index 0000000000..7224c632c7
--- /dev/null
+++ b/dom/serviceworkers/ServiceWorkerParent.h
@@ -0,0 +1,44 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_serviceworkerparent_h__
+#define mozilla_dom_serviceworkerparent_h__
+
+#include "mozilla/dom/PServiceWorkerParent.h"
+
+namespace mozilla::dom {
+
+class IPCServiceWorkerDescriptor;
+class ServiceWorkerProxy;
+
+class ServiceWorkerParent final : public PServiceWorkerParent {
+ RefPtr<ServiceWorkerProxy> mProxy;
+ bool mDeleteSent;
+
+ ~ServiceWorkerParent();
+
+ // PServiceWorkerParent
+ void ActorDestroy(ActorDestroyReason aReason) override;
+
+ mozilla::ipc::IPCResult RecvTeardown() override;
+
+ mozilla::ipc::IPCResult RecvPostMessage(
+ const ClonedOrErrorMessageData& aClonedData,
+ const ClientInfoAndState& aSource) override;
+
+ public:
+ NS_INLINE_DECL_REFCOUNTING(ServiceWorkerParent, override);
+
+ ServiceWorkerParent();
+
+ void Init(const IPCServiceWorkerDescriptor& aDescriptor);
+
+ void MaybeSendDelete();
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_serviceworkerparent_h__
diff --git a/dom/serviceworkers/ServiceWorkerPrivate.cpp b/dom/serviceworkers/ServiceWorkerPrivate.cpp
new file mode 100644
index 0000000000..d8fbe91d76
--- /dev/null
+++ b/dom/serviceworkers/ServiceWorkerPrivate.cpp
@@ -0,0 +1,1678 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "ServiceWorkerPrivate.h"
+
+#include <utility>
+
+#include "MainThreadUtils.h"
+#include "ServiceWorkerCloneData.h"
+#include "ServiceWorkerManager.h"
+#include "ServiceWorkerRegistrationInfo.h"
+#include "ServiceWorkerUtils.h"
+#include "js/ErrorReport.h"
+#include "mozIThirdPartyUtil.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/CycleCollectedJSContext.h" // for MicroTaskRunnable
+#include "mozilla/ErrorResult.h"
+#include "mozilla/JSObjectHolder.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/RemoteLazyInputStreamStorage.h"
+#include "mozilla/Result.h"
+#include "mozilla/ResultExtensions.h"
+#include "mozilla/ScopeExit.h"
+#include "mozilla/Services.h"
+#include "mozilla/StaticPrefs_dom.h"
+#include "mozilla/StoragePrincipalHelper.h"
+#include "mozilla/Telemetry.h"
+#include "mozilla/Unused.h"
+#include "mozilla/dom/ClientIPCTypes.h"
+#include "mozilla/dom/DOMTypes.h"
+#include "mozilla/dom/FetchEventOpChild.h"
+#include "mozilla/dom/InternalHeaders.h"
+#include "mozilla/dom/InternalRequest.h"
+#include "mozilla/dom/ReferrerInfo.h"
+#include "mozilla/dom/RemoteType.h"
+#include "mozilla/dom/RemoteWorkerControllerChild.h"
+#include "mozilla/dom/RemoteWorkerManager.h" // RemoteWorkerManager::GetRemoteType
+#include "mozilla/dom/ServiceWorkerBinding.h"
+#include "mozilla/extensions/WebExtensionPolicy.h" // WebExtensionPolicy
+#include "mozilla/ipc/BackgroundChild.h"
+#include "mozilla/ipc/IPCStreamUtils.h"
+#include "mozilla/ipc/PBackgroundChild.h"
+#include "mozilla/ipc/URIUtils.h"
+#include "mozilla/net/CookieJarSettings.h"
+#include "nsContentUtils.h"
+#include "nsDebug.h"
+#include "nsError.h"
+#include "nsICacheInfoChannel.h"
+#include "nsIChannel.h"
+#include "nsIHttpChannel.h"
+#include "nsIHttpChannelInternal.h"
+#include "nsIHttpHeaderVisitor.h"
+#include "nsINetworkInterceptController.h"
+#include "nsINamed.h"
+#include "nsIObserverService.h"
+#include "nsIRedirectHistoryEntry.h"
+#include "nsIScriptError.h"
+#include "nsIScriptSecurityManager.h"
+#include "nsISupportsImpl.h"
+#include "nsIURI.h"
+#include "nsIUploadChannel2.h"
+#include "nsNetUtil.h"
+#include "nsProxyRelease.h"
+#include "nsQueryObject.h"
+#include "nsStreamUtils.h"
+#include "nsStringStream.h"
+#include "nsThreadUtils.h"
+
+#include "mozilla/dom/Client.h"
+#include "mozilla/dom/FetchUtil.h"
+#include "mozilla/dom/IndexedDatabaseManager.h"
+#include "mozilla/dom/NotificationEvent.h"
+#include "mozilla/dom/PromiseNativeHandler.h"
+#include "mozilla/dom/PushEventBinding.h"
+#include "mozilla/dom/RequestBinding.h"
+#include "mozilla/dom/RootedDictionary.h"
+#include "mozilla/dom/WorkerDebugger.h"
+#include "mozilla/dom/WorkerRef.h"
+#include "mozilla/dom/WorkerRunnable.h"
+#include "mozilla/dom/WorkerScope.h"
+#include "mozilla/dom/ipc/StructuredCloneData.h"
+#include "mozilla/ipc/BackgroundUtils.h"
+#include "mozilla/net/NeckoChannelParams.h"
+#include "mozilla/StaticPrefs_privacy.h"
+#include "nsIReferrerInfo.h"
+
+extern mozilla::LazyLogModule sWorkerTelemetryLog;
+
+#ifdef LOG
+# undef LOG
+#endif
+#define LOG(_args) MOZ_LOG(sWorkerTelemetryLog, LogLevel::Debug, _args);
+
+using namespace mozilla;
+using namespace mozilla::dom;
+using namespace mozilla::ipc;
+
+namespace mozilla::dom {
+
+uint32_t ServiceWorkerPrivate::sRunningServiceWorkers = 0;
+uint32_t ServiceWorkerPrivate::sRunningServiceWorkersFetch = 0;
+uint32_t ServiceWorkerPrivate::sRunningServiceWorkersMax = 0;
+uint32_t ServiceWorkerPrivate::sRunningServiceWorkersFetchMax = 0;
+
+// Tracks the "dom.serviceWorkers.disable_open_click_delay" preference. Modified
+// on main thread, read on worker threads.
+// It is updated every time a "notificationclick" event is dispatched. While
+// this is done without synchronization, at the worst, the thread will just get
+// an older value within which a popup is allowed to be displayed, which will
+// still be a valid value since it was set prior to dispatching the runnable.
+Atomic<uint32_t> gDOMDisableOpenClickDelay(0);
+
+/**
+ * KeepAliveToken
+ */
+KeepAliveToken::KeepAliveToken(ServiceWorkerPrivate* aPrivate)
+ : mPrivate(aPrivate) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aPrivate);
+ mPrivate->AddToken();
+}
+
+KeepAliveToken::~KeepAliveToken() {
+ MOZ_ASSERT(NS_IsMainThread());
+ mPrivate->ReleaseToken();
+}
+
+NS_IMPL_ISUPPORTS0(KeepAliveToken)
+
+/**
+ * RAIIActorPtrHolder
+ */
+ServiceWorkerPrivate::RAIIActorPtrHolder::RAIIActorPtrHolder(
+ already_AddRefed<RemoteWorkerControllerChild> aActor)
+ : mActor(aActor) {
+ AssertIsOnMainThread();
+ MOZ_ASSERT(mActor);
+ MOZ_ASSERT(mActor->Manager());
+}
+
+ServiceWorkerPrivate::RAIIActorPtrHolder::~RAIIActorPtrHolder() {
+ AssertIsOnMainThread();
+
+ mDestructorPromiseHolder.ResolveIfExists(true, __func__);
+
+ mActor->MaybeSendDelete();
+}
+
+RemoteWorkerControllerChild*
+ServiceWorkerPrivate::RAIIActorPtrHolder::operator->() const {
+ AssertIsOnMainThread();
+
+ return get();
+}
+
+RemoteWorkerControllerChild* ServiceWorkerPrivate::RAIIActorPtrHolder::get()
+ const {
+ AssertIsOnMainThread();
+
+ return mActor.get();
+}
+
+RefPtr<GenericPromise>
+ServiceWorkerPrivate::RAIIActorPtrHolder::OnDestructor() {
+ AssertIsOnMainThread();
+
+ return mDestructorPromiseHolder.Ensure(__func__);
+}
+
+/**
+ * PendingFunctionEvent
+ */
+ServiceWorkerPrivate::PendingFunctionalEvent::PendingFunctionalEvent(
+ ServiceWorkerPrivate* aOwner,
+ RefPtr<ServiceWorkerRegistrationInfo>&& aRegistration)
+ : mOwner(aOwner), mRegistration(std::move(aRegistration)) {
+ AssertIsOnMainThread();
+ MOZ_ASSERT(mOwner);
+ MOZ_ASSERT(mOwner->mInfo);
+ MOZ_ASSERT(mOwner->mInfo->State() == ServiceWorkerState::Activating);
+ MOZ_ASSERT(mRegistration);
+}
+
+ServiceWorkerPrivate::PendingFunctionalEvent::~PendingFunctionalEvent() {
+ AssertIsOnMainThread();
+}
+
+ServiceWorkerPrivate::PendingPushEvent::PendingPushEvent(
+ ServiceWorkerPrivate* aOwner,
+ RefPtr<ServiceWorkerRegistrationInfo>&& aRegistration,
+ ServiceWorkerPushEventOpArgs&& aArgs)
+ : PendingFunctionalEvent(aOwner, std::move(aRegistration)),
+ mArgs(std::move(aArgs)) {
+ AssertIsOnMainThread();
+}
+
+nsresult ServiceWorkerPrivate::PendingPushEvent::Send() {
+ AssertIsOnMainThread();
+ MOZ_ASSERT(mOwner);
+ MOZ_ASSERT(mOwner->mInfo);
+
+ return mOwner->SendPushEventInternal(std::move(mRegistration),
+ std::move(mArgs));
+}
+
+ServiceWorkerPrivate::PendingFetchEvent::PendingFetchEvent(
+ ServiceWorkerPrivate* aOwner,
+ RefPtr<ServiceWorkerRegistrationInfo>&& aRegistration,
+ ParentToParentServiceWorkerFetchEventOpArgs&& aArgs,
+ nsCOMPtr<nsIInterceptedChannel>&& aChannel,
+ RefPtr<FetchServicePromises>&& aPreloadResponseReadyPromises)
+ : PendingFunctionalEvent(aOwner, std::move(aRegistration)),
+ mArgs(std::move(aArgs)),
+ mChannel(std::move(aChannel)),
+ mPreloadResponseReadyPromises(std::move(aPreloadResponseReadyPromises)) {
+ AssertIsOnMainThread();
+ MOZ_ASSERT(mChannel);
+}
+
+nsresult ServiceWorkerPrivate::PendingFetchEvent::Send() {
+ AssertIsOnMainThread();
+ MOZ_ASSERT(mOwner);
+ MOZ_ASSERT(mOwner->mInfo);
+
+ return mOwner->SendFetchEventInternal(
+ std::move(mRegistration), std::move(mArgs), std::move(mChannel),
+ std::move(mPreloadResponseReadyPromises));
+}
+
+ServiceWorkerPrivate::PendingFetchEvent::~PendingFetchEvent() {
+ AssertIsOnMainThread();
+
+ if (NS_WARN_IF(mChannel)) {
+ mChannel->CancelInterception(NS_ERROR_INTERCEPTION_FAILED);
+ }
+}
+
+namespace {
+
+class HeaderFiller final : public nsIHttpHeaderVisitor {
+ public:
+ NS_DECL_ISUPPORTS
+
+ explicit HeaderFiller(HeadersGuardEnum aGuard)
+ : mInternalHeaders(new InternalHeaders(aGuard)) {
+ MOZ_ASSERT(mInternalHeaders);
+ }
+
+ NS_IMETHOD
+ VisitHeader(const nsACString& aHeader, const nsACString& aValue) override {
+ ErrorResult result;
+ mInternalHeaders->Append(aHeader, aValue, result);
+
+ if (NS_WARN_IF(result.Failed())) {
+ return result.StealNSResult();
+ }
+
+ return NS_OK;
+ }
+
+ RefPtr<InternalHeaders> Extract() {
+ return RefPtr<InternalHeaders>(std::move(mInternalHeaders));
+ }
+
+ private:
+ ~HeaderFiller() = default;
+
+ RefPtr<InternalHeaders> mInternalHeaders;
+};
+
+NS_IMPL_ISUPPORTS(HeaderFiller, nsIHttpHeaderVisitor)
+
+Result<IPCInternalRequest, nsresult> GetIPCInternalRequest(
+ nsIInterceptedChannel* aChannel) {
+ AssertIsOnMainThread();
+
+ nsCOMPtr<nsIURI> uri;
+ MOZ_TRY(aChannel->GetSecureUpgradedChannelURI(getter_AddRefs(uri)));
+
+ nsCOMPtr<nsIURI> uriNoFragment;
+ MOZ_TRY(NS_GetURIWithoutRef(uri, getter_AddRefs(uriNoFragment)));
+
+ nsCOMPtr<nsIChannel> underlyingChannel;
+ MOZ_TRY(aChannel->GetChannel(getter_AddRefs(underlyingChannel)));
+
+ nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(underlyingChannel);
+ MOZ_ASSERT(httpChannel, "How come we don't have an HTTP channel?");
+
+ nsCOMPtr<nsIHttpChannelInternal> internalChannel =
+ do_QueryInterface(httpChannel);
+ NS_ENSURE_TRUE(internalChannel, Err(NS_ERROR_NOT_AVAILABLE));
+
+ nsCOMPtr<nsICacheInfoChannel> cacheInfoChannel =
+ do_QueryInterface(underlyingChannel);
+
+ nsAutoCString spec;
+ MOZ_TRY(uriNoFragment->GetSpec(spec));
+
+ nsAutoCString fragment;
+ MOZ_TRY(uri->GetRef(fragment));
+
+ nsAutoCString method;
+ MOZ_TRY(httpChannel->GetRequestMethod(method));
+
+ // This is safe due to static_asserts in ServiceWorkerManager.cpp
+ uint32_t cacheModeInt;
+ MOZ_ALWAYS_SUCCEEDS(internalChannel->GetFetchCacheMode(&cacheModeInt));
+ RequestCache cacheMode = static_cast<RequestCache>(cacheModeInt);
+
+ RequestMode requestMode =
+ InternalRequest::MapChannelToRequestMode(underlyingChannel);
+
+ // This is safe due to static_asserts in ServiceWorkerManager.cpp
+ uint32_t redirectMode;
+ MOZ_ALWAYS_SUCCEEDS(internalChannel->GetRedirectMode(&redirectMode));
+ RequestRedirect requestRedirect = static_cast<RequestRedirect>(redirectMode);
+
+ RequestCredentials requestCredentials =
+ InternalRequest::MapChannelToRequestCredentials(underlyingChannel);
+
+ nsAutoString referrer;
+ ReferrerPolicy referrerPolicy = ReferrerPolicy::_empty;
+
+ nsCOMPtr<nsIReferrerInfo> referrerInfo = httpChannel->GetReferrerInfo();
+ if (referrerInfo) {
+ referrerPolicy = referrerInfo->ReferrerPolicy();
+ Unused << referrerInfo->GetComputedReferrerSpec(referrer);
+ }
+
+ uint32_t loadFlags;
+ MOZ_TRY(underlyingChannel->GetLoadFlags(&loadFlags));
+
+ nsCOMPtr<nsILoadInfo> loadInfo = underlyingChannel->LoadInfo();
+ nsContentPolicyType contentPolicyType = loadInfo->InternalContentPolicyType();
+
+ nsAutoString integrity;
+ MOZ_TRY(internalChannel->GetIntegrityMetadata(integrity));
+
+ RefPtr<HeaderFiller> headerFiller =
+ MakeRefPtr<HeaderFiller>(HeadersGuardEnum::Request);
+ MOZ_TRY(httpChannel->VisitNonDefaultRequestHeaders(headerFiller));
+
+ RefPtr<InternalHeaders> internalHeaders = headerFiller->Extract();
+
+ ErrorResult result;
+ internalHeaders->SetGuard(HeadersGuardEnum::Immutable, result);
+ if (NS_WARN_IF(result.Failed())) {
+ return Err(result.StealNSResult());
+ }
+
+ nsTArray<HeadersEntry> ipcHeaders;
+ HeadersGuardEnum ipcHeadersGuard;
+ internalHeaders->ToIPC(ipcHeaders, ipcHeadersGuard);
+
+ nsAutoCString alternativeDataType;
+ if (cacheInfoChannel &&
+ !cacheInfoChannel->PreferredAlternativeDataTypes().IsEmpty()) {
+ // TODO: the internal request probably needs all the preferred types.
+ alternativeDataType.Assign(
+ cacheInfoChannel->PreferredAlternativeDataTypes()[0].type());
+ }
+
+ Maybe<PrincipalInfo> principalInfo;
+ Maybe<PrincipalInfo> interceptionPrincipalInfo;
+ if (loadInfo->TriggeringPrincipal()) {
+ principalInfo.emplace();
+ interceptionPrincipalInfo.emplace();
+ MOZ_ALWAYS_SUCCEEDS(PrincipalToPrincipalInfo(
+ loadInfo->TriggeringPrincipal(), principalInfo.ptr()));
+ MOZ_ALWAYS_SUCCEEDS(PrincipalToPrincipalInfo(
+ loadInfo->TriggeringPrincipal(), interceptionPrincipalInfo.ptr()));
+ }
+
+ nsTArray<RedirectHistoryEntryInfo> redirectChain;
+ for (const nsCOMPtr<nsIRedirectHistoryEntry>& redirectEntry :
+ loadInfo->RedirectChain()) {
+ RedirectHistoryEntryInfo* entry = redirectChain.AppendElement();
+ MOZ_ALWAYS_SUCCEEDS(RHEntryToRHEntryInfo(redirectEntry, entry));
+ }
+
+ bool isThirdPartyChannel;
+ // ThirdPartyUtil* thirdPartyUtil = ThirdPartyUtil::GetInstance();
+ nsCOMPtr<mozIThirdPartyUtil> thirdPartyUtil =
+ do_GetService(THIRDPARTYUTIL_CONTRACTID);
+ if (thirdPartyUtil) {
+ nsCOMPtr<nsIURI> uri;
+ MOZ_TRY(underlyingChannel->GetURI(getter_AddRefs(uri)));
+ MOZ_TRY(thirdPartyUtil->IsThirdPartyChannel(underlyingChannel, uri,
+ &isThirdPartyChannel));
+ }
+
+ // Note: all the arguments are copied rather than moved, which would be more
+ // efficient, because there's no move-friendly constructor generated.
+ return IPCInternalRequest(
+ method, {spec}, ipcHeadersGuard, ipcHeaders, Nothing(), -1,
+ alternativeDataType, contentPolicyType, referrer, referrerPolicy,
+ requestMode, requestCredentials, cacheMode, requestRedirect, integrity,
+ fragment, principalInfo, interceptionPrincipalInfo, contentPolicyType,
+ redirectChain, isThirdPartyChannel);
+}
+
+nsresult MaybeStoreStreamForBackgroundThread(nsIInterceptedChannel* aChannel,
+ IPCInternalRequest& aIPCRequest) {
+ nsCOMPtr<nsIChannel> channel;
+ MOZ_ALWAYS_SUCCEEDS(aChannel->GetChannel(getter_AddRefs(channel)));
+
+ Maybe<BodyStreamVariant> body;
+ nsCOMPtr<nsIUploadChannel2> uploadChannel = do_QueryInterface(channel);
+
+ if (uploadChannel) {
+ nsCOMPtr<nsIInputStream> uploadStream;
+ MOZ_TRY(uploadChannel->CloneUploadStream(&aIPCRequest.bodySize(),
+ getter_AddRefs(uploadStream)));
+
+ if (uploadStream) {
+ Maybe<BodyStreamVariant>& body = aIPCRequest.body();
+ body.emplace(ParentToParentStream());
+
+ MOZ_TRY(
+ nsID::GenerateUUIDInPlace(body->get_ParentToParentStream().uuid()));
+
+ auto storageOrErr = RemoteLazyInputStreamStorage::Get();
+ if (NS_WARN_IF(storageOrErr.isErr())) {
+ return storageOrErr.unwrapErr();
+ }
+
+ auto storage = storageOrErr.unwrap();
+ storage->AddStream(uploadStream, body->get_ParentToParentStream().uuid());
+ }
+ }
+
+ return NS_OK;
+}
+
+} // anonymous namespace
+
+/**
+ * ServiceWorkerPrivate
+ */
+ServiceWorkerPrivate::ServiceWorkerPrivate(ServiceWorkerInfo* aInfo)
+ : mInfo(aInfo), mDebuggerCount(0), mTokenCount(0) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aInfo);
+ MOZ_ASSERT(!mControllerChild);
+
+ mIdleWorkerTimer = NS_NewTimer();
+ MOZ_ASSERT(mIdleWorkerTimer);
+
+ // Assert in all debug builds as well as non-debug Nightly and Dev Edition.
+#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED
+ MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(Initialize()));
+#else
+ MOZ_ALWAYS_SUCCEEDS(Initialize());
+#endif
+}
+
+ServiceWorkerPrivate::~ServiceWorkerPrivate() {
+ MOZ_ASSERT(!mTokenCount);
+ MOZ_ASSERT(!mInfo);
+ MOZ_ASSERT(!mControllerChild);
+ MOZ_ASSERT(mIdlePromiseHolder.IsEmpty());
+
+ mIdleWorkerTimer->Cancel();
+}
+
+nsresult ServiceWorkerPrivate::Initialize() {
+ AssertIsOnMainThread();
+ MOZ_ASSERT(mInfo);
+
+ nsCOMPtr<nsIPrincipal> principal = mInfo->Principal();
+
+ nsCOMPtr<nsIURI> uri;
+ auto* basePrin = BasePrincipal::Cast(principal);
+ nsresult rv = basePrin->GetURI(getter_AddRefs(uri));
+
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ if (NS_WARN_IF(!uri)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ URIParams baseScriptURL;
+ SerializeURI(uri, baseScriptURL);
+
+ nsString id;
+ rv = mInfo->GetId(id);
+
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ PrincipalInfo principalInfo;
+ rv = PrincipalToPrincipalInfo(principal, &principalInfo);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance();
+
+ if (NS_WARN_IF(!swm)) {
+ return NS_ERROR_DOM_ABORT_ERR;
+ }
+
+ RefPtr<ServiceWorkerRegistrationInfo> regInfo =
+ swm->GetRegistration(principal, mInfo->Scope());
+
+ if (NS_WARN_IF(!regInfo)) {
+ return NS_ERROR_DOM_INVALID_STATE_ERR;
+ }
+
+ nsCOMPtr<nsICookieJarSettings> cookieJarSettings =
+ net::CookieJarSettings::Create(principal);
+ MOZ_ASSERT(cookieJarSettings);
+
+ // We can populate the partitionKey from the originAttribute of the principal
+ // if it has partitionKey set. It's because ServiceWorker is using the foreign
+ // partitioned principal and it implies that it's a third-party service
+ // worker. So, the cookieJarSettings can directly use the partitionKey from
+ // it. For first-party case, we can populate the partitionKey from the
+ // principal URI.
+ if (!principal->OriginAttributesRef().mPartitionKey.IsEmpty()) {
+ net::CookieJarSettings::Cast(cookieJarSettings)
+ ->SetPartitionKey(principal->OriginAttributesRef().mPartitionKey);
+ } else {
+ net::CookieJarSettings::Cast(cookieJarSettings)->SetPartitionKey(uri);
+ }
+
+ net::CookieJarSettingsArgs cjsData;
+ net::CookieJarSettings::Cast(cookieJarSettings)->Serialize(cjsData);
+
+ nsCOMPtr<nsIPrincipal> partitionedPrincipal;
+ rv = StoragePrincipalHelper::CreatePartitionedPrincipalForServiceWorker(
+ principal, cookieJarSettings, getter_AddRefs(partitionedPrincipal));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ PrincipalInfo partitionedPrincipalInfo;
+ rv =
+ PrincipalToPrincipalInfo(partitionedPrincipal, &partitionedPrincipalInfo);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ StorageAccess storageAccess =
+ StorageAllowedForServiceWorker(principal, cookieJarSettings);
+
+ ServiceWorkerData serviceWorkerData;
+ serviceWorkerData.cacheName() = mInfo->CacheName();
+ serviceWorkerData.loadFlags() = static_cast<uint32_t>(
+ mInfo->GetImportsLoadFlags() | nsIChannel::LOAD_BYPASS_SERVICE_WORKER);
+ serviceWorkerData.id() = std::move(id);
+
+ nsAutoCString domain;
+ rv = uri->GetHost(domain);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ auto remoteType = RemoteWorkerManager::GetRemoteType(
+ principal, WorkerKind::WorkerKindService);
+ if (NS_WARN_IF(remoteType.isErr())) {
+ return remoteType.unwrapErr();
+ }
+
+ // Determine if the service worker is registered under a third-party context
+ // by checking if it's running under a partitioned principal.
+ bool isThirdPartyContextToTopWindow =
+ !principal->OriginAttributesRef().mPartitionKey.IsEmpty();
+
+ mRemoteWorkerData = RemoteWorkerData(
+ NS_ConvertUTF8toUTF16(mInfo->ScriptSpec()), baseScriptURL, baseScriptURL,
+ /* name */ VoidString(),
+ /* loading principal */ principalInfo, principalInfo,
+ partitionedPrincipalInfo,
+ /* useRegularPrincipal */ true,
+
+ // ServiceWorkers run as first-party, no storage-access permission needed.
+ /* hasStorageAccessPermissionGranted */ false,
+
+ cjsData, domain,
+ /* isSecureContext */ true,
+ /* clientInfo*/ Nothing(),
+
+ // The RemoteWorkerData CTOR doesn't allow to set the referrerInfo via
+ // already_AddRefed<>. Let's set it to null.
+ /* referrerInfo */ nullptr,
+
+ storageAccess, isThirdPartyContextToTopWindow,
+ nsContentUtils::ShouldResistFingerprinting_dangerous(
+ principal,
+ "Service Workers exist outside a Document or Channel; as a property "
+ "of the domain (and origin attributes). We don't have a "
+ "CookieJarSettings to perform the nested check, but we can rely on"
+ "the FPI/dFPI partition key check."),
+ // Origin trials are associated to a window, so it doesn't make sense on
+ // service workers.
+ OriginTrials(), std::move(serviceWorkerData), regInfo->AgentClusterId(),
+ remoteType.unwrap());
+
+ mRemoteWorkerData.referrerInfo() = MakeAndAddRef<ReferrerInfo>();
+
+ // This fills in the rest of mRemoteWorkerData.serviceWorkerData().
+ RefreshRemoteWorkerData(regInfo);
+
+ return NS_OK;
+}
+
+nsresult ServiceWorkerPrivate::CheckScriptEvaluation(
+ RefPtr<LifeCycleEventCallback> aCallback) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aCallback);
+
+ RefPtr<ServiceWorkerPrivate> self = this;
+
+ /**
+ * We need to capture the actor associated with the current Service Worker so
+ * we can terminate it if script evaluation failed.
+ */
+ nsresult rv = SpawnWorkerIfNeeded();
+
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ aCallback->SetResult(false);
+ aCallback->Run();
+
+ return rv;
+ }
+
+ MOZ_ASSERT(mControllerChild);
+
+ RefPtr<RAIIActorPtrHolder> holder = mControllerChild;
+
+ return ExecServiceWorkerOp(
+ ServiceWorkerCheckScriptEvaluationOpArgs(),
+ [self = std::move(self), holder = std::move(holder),
+ callback = aCallback](ServiceWorkerOpResult&& aResult) mutable {
+ if (aResult.type() == ServiceWorkerOpResult::
+ TServiceWorkerCheckScriptEvaluationOpResult) {
+ auto& result =
+ aResult.get_ServiceWorkerCheckScriptEvaluationOpResult();
+
+ if (result.workerScriptExecutedSuccessfully()) {
+ self->SetHandlesFetch(result.fetchHandlerWasAdded());
+ if (self->mHandlesFetch == Unknown) {
+ self->mHandlesFetch =
+ result.fetchHandlerWasAdded() ? Enabled : Disabled;
+ // Update telemetry for # of running SW - the already-running SW
+ // handles fetch
+ if (self->mHandlesFetch == Enabled) {
+ self->UpdateRunning(0, 1);
+ }
+ }
+
+ callback->SetResult(result.workerScriptExecutedSuccessfully());
+ callback->Run();
+ return;
+ }
+ }
+
+ /**
+ * If script evaluation failed, first terminate the Service Worker
+ * before invoking the callback.
+ */
+ MOZ_ASSERT_IF(aResult.type() == ServiceWorkerOpResult::Tnsresult,
+ NS_FAILED(aResult.get_nsresult()));
+
+ // If a termination operation was already issued using `holder`...
+ if (self->mControllerChild != holder) {
+ holder->OnDestructor()->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [callback = std::move(callback)](
+ const GenericPromise::ResolveOrRejectValue&) {
+ callback->SetResult(false);
+ callback->Run();
+ });
+
+ return;
+ }
+
+ RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance();
+ MOZ_ASSERT(swm);
+
+ auto shutdownStateId = swm->MaybeInitServiceWorkerShutdownProgress();
+
+ RefPtr<GenericNonExclusivePromise> promise =
+ self->ShutdownInternal(shutdownStateId);
+
+ swm->BlockShutdownOn(promise, shutdownStateId);
+
+ promise->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [callback = std::move(callback)](
+ const GenericNonExclusivePromise::ResolveOrRejectValue&) {
+ callback->SetResult(false);
+ callback->Run();
+ });
+ },
+ [callback = aCallback] {
+ callback->SetResult(false);
+ callback->Run();
+ });
+}
+
+nsresult ServiceWorkerPrivate::SendMessageEvent(
+ RefPtr<ServiceWorkerCloneData>&& aData,
+ const ClientInfoAndState& aClientInfoAndState) {
+ AssertIsOnMainThread();
+ MOZ_ASSERT(aData);
+
+ auto scopeExit = MakeScopeExit([&] { Shutdown(); });
+
+ PBackgroundChild* bgChild = BackgroundChild::GetForCurrentThread();
+
+ if (NS_WARN_IF(!bgChild)) {
+ return NS_ERROR_DOM_INVALID_STATE_ERR;
+ }
+
+ ServiceWorkerMessageEventOpArgs args;
+ args.clientInfoAndState() = aClientInfoAndState;
+ if (!aData->BuildClonedMessageData(args.clonedData())) {
+ return NS_ERROR_DOM_DATA_CLONE_ERR;
+ }
+
+ scopeExit.release();
+
+ return ExecServiceWorkerOp(
+ std::move(args), [](ServiceWorkerOpResult&& aResult) {
+ MOZ_ASSERT(aResult.type() == ServiceWorkerOpResult::Tnsresult);
+ });
+}
+
+nsresult ServiceWorkerPrivate::SendLifeCycleEvent(
+ const nsAString& aEventType, RefPtr<LifeCycleEventCallback> aCallback) {
+ AssertIsOnMainThread();
+ MOZ_ASSERT(aCallback);
+
+ return ExecServiceWorkerOp(
+ ServiceWorkerLifeCycleEventOpArgs(nsString(aEventType)),
+ [callback = aCallback](ServiceWorkerOpResult&& aResult) {
+ MOZ_ASSERT(aResult.type() == ServiceWorkerOpResult::Tnsresult);
+
+ callback->SetResult(NS_SUCCEEDED(aResult.get_nsresult()));
+ callback->Run();
+ },
+ [callback = aCallback] {
+ callback->SetResult(false);
+ callback->Run();
+ });
+}
+
+nsresult ServiceWorkerPrivate::SendPushEvent(
+ const nsAString& aMessageId, const Maybe<nsTArray<uint8_t>>& aData,
+ RefPtr<ServiceWorkerRegistrationInfo> aRegistration) {
+ AssertIsOnMainThread();
+ MOZ_ASSERT(mInfo);
+ MOZ_ASSERT(aRegistration);
+
+ ServiceWorkerPushEventOpArgs args;
+ args.messageId() = nsString(aMessageId);
+
+ if (aData) {
+ args.data() = aData.ref();
+ } else {
+ args.data() = void_t();
+ }
+
+ if (mInfo->State() == ServiceWorkerState::Activating) {
+ UniquePtr<PendingFunctionalEvent> pendingEvent =
+ MakeUnique<PendingPushEvent>(this, std::move(aRegistration),
+ std::move(args));
+
+ mPendingFunctionalEvents.AppendElement(std::move(pendingEvent));
+
+ return NS_OK;
+ }
+
+ MOZ_ASSERT(mInfo->State() == ServiceWorkerState::Activated);
+
+ return SendPushEventInternal(std::move(aRegistration), std::move(args));
+}
+
+nsresult ServiceWorkerPrivate::SendPushEventInternal(
+ RefPtr<ServiceWorkerRegistrationInfo>&& aRegistration,
+ ServiceWorkerPushEventOpArgs&& aArgs) {
+ AssertIsOnMainThread();
+ MOZ_ASSERT(aRegistration);
+
+ return ExecServiceWorkerOp(
+ std::move(aArgs),
+ [registration = aRegistration](ServiceWorkerOpResult&& aResult) {
+ MOZ_ASSERT(aResult.type() == ServiceWorkerOpResult::Tnsresult);
+
+ registration->MaybeScheduleTimeCheckAndUpdate();
+ },
+ [registration = aRegistration]() {
+ registration->MaybeScheduleTimeCheckAndUpdate();
+ });
+}
+
+nsresult ServiceWorkerPrivate::SendPushSubscriptionChangeEvent() {
+ AssertIsOnMainThread();
+
+ return ExecServiceWorkerOp(
+ ServiceWorkerPushSubscriptionChangeEventOpArgs(),
+ [](ServiceWorkerOpResult&& aResult) {
+ MOZ_ASSERT(aResult.type() == ServiceWorkerOpResult::Tnsresult);
+ });
+}
+
+nsresult ServiceWorkerPrivate::SendNotificationEvent(
+ const nsAString& aEventName, const nsAString& aID, const nsAString& aTitle,
+ const nsAString& aDir, const nsAString& aLang, const nsAString& aBody,
+ const nsAString& aTag, const nsAString& aIcon, const nsAString& aData,
+ const nsAString& aBehavior, const nsAString& aScope) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (aEventName.EqualsLiteral(NOTIFICATION_CLICK_EVENT_NAME)) {
+ gDOMDisableOpenClickDelay =
+ Preferences::GetInt("dom.serviceWorkers.disable_open_click_delay");
+ } else if (!aEventName.EqualsLiteral(NOTIFICATION_CLOSE_EVENT_NAME)) {
+ MOZ_ASSERT_UNREACHABLE("Invalid notification event name");
+ return NS_ERROR_FAILURE;
+ }
+
+ ServiceWorkerNotificationEventOpArgs args;
+ args.eventName() = nsString(aEventName);
+ args.id() = nsString(aID);
+ args.title() = nsString(aTitle);
+ args.dir() = nsString(aDir);
+ args.lang() = nsString(aLang);
+ args.body() = nsString(aBody);
+ args.tag() = nsString(aTag);
+ args.icon() = nsString(aIcon);
+ args.data() = nsString(aData);
+ args.behavior() = nsString(aBehavior);
+ args.scope() = nsString(aScope);
+ args.disableOpenClickDelay() = gDOMDisableOpenClickDelay;
+
+ return ExecServiceWorkerOp(
+ std::move(args), [](ServiceWorkerOpResult&& aResult) {
+ MOZ_ASSERT(aResult.type() == ServiceWorkerOpResult::Tnsresult);
+ });
+}
+
+nsresult ServiceWorkerPrivate::SendFetchEvent(
+ nsCOMPtr<nsIInterceptedChannel> aChannel, nsILoadGroup* aLoadGroup,
+ const nsAString& aClientId, const nsAString& aResultingClientId) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aChannel);
+
+ RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance();
+ if (NS_WARN_IF(!mInfo || !swm)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsCOMPtr<nsIChannel> channel;
+ nsresult rv = aChannel->GetChannel(getter_AddRefs(channel));
+ NS_ENSURE_SUCCESS(rv, rv);
+ bool isNonSubresourceRequest =
+ nsContentUtils::IsNonSubresourceRequest(channel);
+
+ RefPtr<ServiceWorkerRegistrationInfo> registration;
+ if (isNonSubresourceRequest) {
+ registration = swm->GetRegistration(mInfo->Principal(), mInfo->Scope());
+ } else {
+ nsCOMPtr<nsILoadInfo> loadInfo = channel->LoadInfo();
+
+ // We'll check for a null registration below rather than an error code here.
+ Unused << swm->GetClientRegistration(loadInfo->GetClientInfo().ref(),
+ getter_AddRefs(registration));
+ }
+
+ // Its possible the registration is removed between starting the interception
+ // and actually dispatching the fetch event. In these cases we simply
+ // want to restart the original network request. Since this is a normal
+ // condition we handle the reset here instead of returning an error which
+ // would in turn trigger a console report.
+ if (!registration) {
+ nsresult rv = aChannel->ResetInterception(false);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("Failed to resume intercepted network request");
+ aChannel->CancelInterception(rv);
+ }
+ return NS_OK;
+ }
+
+ // Handle Fetch algorithm - step 16. If the service worker didn't register
+ // any fetch event handlers, then abort the interception and maybe trigger
+ // the soft update algorithm.
+ if (!mInfo->HandlesFetch()) {
+ nsresult rv = aChannel->ResetInterception(false);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("Failed to resume intercepted network request");
+ aChannel->CancelInterception(rv);
+ }
+
+ // Trigger soft updates if necessary.
+ registration->MaybeScheduleTimeCheckAndUpdate();
+
+ return NS_OK;
+ }
+
+ auto scopeExit = MakeScopeExit([&] {
+ aChannel->CancelInterception(NS_ERROR_INTERCEPTION_FAILED);
+ Shutdown();
+ });
+
+ IPCInternalRequest request;
+ MOZ_TRY_VAR(request, GetIPCInternalRequest(aChannel));
+
+ scopeExit.release();
+
+ bool preloadNavigation = isNonSubresourceRequest &&
+ request.method().LowerCaseEqualsASCII("get") &&
+ registration->GetNavigationPreloadState().enabled();
+
+ RefPtr<FetchServicePromises> preloadResponsePromises;
+ if (preloadNavigation) {
+ preloadResponsePromises = SetupNavigationPreload(aChannel, registration);
+ }
+
+ ParentToParentServiceWorkerFetchEventOpArgs args(
+ ServiceWorkerFetchEventOpArgsCommon(
+ mInfo->ScriptSpec(), request, nsString(aClientId),
+ nsString(aResultingClientId), isNonSubresourceRequest,
+ preloadNavigation, mInfo->TestingInjectCancellation()),
+ Nothing(), Nothing());
+
+ if (mInfo->State() == ServiceWorkerState::Activating) {
+ UniquePtr<PendingFunctionalEvent> pendingEvent =
+ MakeUnique<PendingFetchEvent>(this, std::move(registration),
+ std::move(args), std::move(aChannel),
+ std::move(preloadResponsePromises));
+
+ mPendingFunctionalEvents.AppendElement(std::move(pendingEvent));
+
+ return NS_OK;
+ }
+
+ MOZ_ASSERT(mInfo->State() == ServiceWorkerState::Activated);
+
+ return SendFetchEventInternal(std::move(registration), std::move(args),
+ std::move(aChannel),
+ std::move(preloadResponsePromises));
+}
+
+nsresult ServiceWorkerPrivate::SendFetchEventInternal(
+ RefPtr<ServiceWorkerRegistrationInfo>&& aRegistration,
+ ParentToParentServiceWorkerFetchEventOpArgs&& aArgs,
+ nsCOMPtr<nsIInterceptedChannel>&& aChannel,
+ RefPtr<FetchServicePromises>&& aPreloadResponseReadyPromises) {
+ AssertIsOnMainThread();
+
+ auto scopeExit = MakeScopeExit([&] { Shutdown(); });
+
+ if (NS_WARN_IF(!mInfo)) {
+ return NS_ERROR_DOM_INVALID_STATE_ERR;
+ }
+
+ MOZ_TRY(SpawnWorkerIfNeeded());
+ MOZ_TRY(MaybeStoreStreamForBackgroundThread(
+ aChannel, aArgs.common().internalRequest()));
+
+ scopeExit.release();
+
+ MOZ_ASSERT(mControllerChild);
+
+ RefPtr<RAIIActorPtrHolder> holder = mControllerChild;
+
+ FetchEventOpChild::SendFetchEvent(
+ mControllerChild->get(), std::move(aArgs), std::move(aChannel),
+ std::move(aRegistration), std::move(aPreloadResponseReadyPromises),
+ CreateEventKeepAliveToken())
+ ->Then(GetCurrentSerialEventTarget(), __func__,
+ [holder = std::move(holder)](
+ const GenericPromise::ResolveOrRejectValue& aResult) {
+ Unused << NS_WARN_IF(aResult.IsReject());
+ });
+
+ return NS_OK;
+}
+
+Result<RefPtr<ServiceWorkerPrivate::PromiseExtensionWorkerHasListener>,
+ nsresult>
+ServiceWorkerPrivate::WakeForExtensionAPIEvent(
+ const nsAString& aExtensionAPINamespace,
+ const nsAString& aExtensionAPIEventName) {
+ AssertIsOnMainThread();
+
+ ServiceWorkerExtensionAPIEventOpArgs args;
+ args.apiNamespace() = nsString(aExtensionAPINamespace);
+ args.apiEventName() = nsString(aExtensionAPIEventName);
+
+ auto promise =
+ MakeRefPtr<PromiseExtensionWorkerHasListener::Private>(__func__);
+
+ nsresult rv = ExecServiceWorkerOp(
+ std::move(args),
+ [promise](ServiceWorkerOpResult&& aResult) {
+ MOZ_ASSERT(
+ aResult.type() ==
+ ServiceWorkerOpResult::TServiceWorkerExtensionAPIEventOpResult);
+ auto& result = aResult.get_ServiceWorkerExtensionAPIEventOpResult();
+ promise->Resolve(result.extensionAPIEventListenerWasAdded(), __func__);
+ },
+ [promise]() { promise->Reject(NS_ERROR_FAILURE, __func__); });
+
+ if (NS_FAILED(rv)) {
+ promise->Reject(rv, __func__);
+ }
+
+ RefPtr<PromiseExtensionWorkerHasListener> outPromise(promise);
+ return outPromise;
+}
+
+nsresult ServiceWorkerPrivate::SpawnWorkerIfNeeded() {
+ AssertIsOnMainThread();
+
+ if (mControllerChild) {
+ RenewKeepAliveToken();
+ return NS_OK;
+ }
+
+ if (!mInfo) {
+ return NS_ERROR_DOM_INVALID_STATE_ERR;
+ }
+
+ mServiceWorkerLaunchTimeStart = TimeStamp::Now();
+
+ PBackgroundChild* bgChild = BackgroundChild::GetForCurrentThread();
+
+ if (NS_WARN_IF(!bgChild)) {
+ return NS_ERROR_DOM_INVALID_STATE_ERR;
+ }
+
+ // If the worker principal is an extension principal, then we should not spawn
+ // a worker if there is no WebExtensionPolicy associated to that principal
+ // or if the WebExtensionPolicy is not active.
+ auto* principal = mInfo->Principal();
+ if (principal->SchemeIs("moz-extension")) {
+ auto* addonPolicy = BasePrincipal::Cast(principal)->AddonPolicy();
+ if (!addonPolicy || !addonPolicy->Active()) {
+ NS_WARNING(
+ "Trying to wake up a service worker for a disabled webextension.");
+ return NS_ERROR_DOM_INVALID_STATE_ERR;
+ }
+ }
+
+ RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance();
+
+ if (NS_WARN_IF(!swm)) {
+ return NS_ERROR_DOM_ABORT_ERR;
+ }
+
+ RefPtr<ServiceWorkerRegistrationInfo> regInfo =
+ swm->GetRegistration(principal, mInfo->Scope());
+
+ if (NS_WARN_IF(!regInfo)) {
+ return NS_ERROR_DOM_INVALID_STATE_ERR;
+ }
+
+ RefreshRemoteWorkerData(regInfo);
+
+ RefPtr<RemoteWorkerControllerChild> controllerChild =
+ new RemoteWorkerControllerChild(this);
+
+ if (NS_WARN_IF(!bgChild->SendPRemoteWorkerControllerConstructor(
+ controllerChild, mRemoteWorkerData))) {
+ return NS_ERROR_DOM_INVALID_STATE_ERR;
+ }
+
+ /**
+ * Manually `AddRef()` because `DeallocPRemoteWorkerControllerChild()`
+ * calls `Release()` and the `AllocPRemoteWorkerControllerChild()` function
+ * is not called.
+ */
+ // NOLINTNEXTLINE(readability-redundant-smartptr-get)
+ controllerChild.get()->AddRef();
+
+ mControllerChild = new RAIIActorPtrHolder(controllerChild.forget());
+
+ // Update Running count here because we may Terminate before we get
+ // CreationSucceeded(). We'll update if it handles Fetch if that changes
+ // (
+ UpdateRunning(1, mHandlesFetch == Enabled ? 1 : 0);
+
+ return NS_OK;
+}
+
+void ServiceWorkerPrivate::TerminateWorker() {
+ MOZ_ASSERT(NS_IsMainThread());
+ mIdleWorkerTimer->Cancel();
+ mIdleKeepAliveToken = nullptr;
+ Shutdown();
+}
+
+void ServiceWorkerPrivate::NoteDeadServiceWorkerInfo() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ TerminateWorker();
+ mInfo = nullptr;
+}
+
+void ServiceWorkerPrivate::UpdateState(ServiceWorkerState aState) {
+ AssertIsOnMainThread();
+
+ if (!mControllerChild) {
+ return;
+ }
+
+ nsresult rv = ExecServiceWorkerOp(
+ ServiceWorkerUpdateStateOpArgs(aState),
+ [](ServiceWorkerOpResult&& aResult) {
+ MOZ_ASSERT(aResult.type() == ServiceWorkerOpResult::Tnsresult);
+ });
+
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ Shutdown();
+ return;
+ }
+
+ if (aState != ServiceWorkerState::Activated) {
+ return;
+ }
+
+ for (auto& event : mPendingFunctionalEvents) {
+ Unused << NS_WARN_IF(NS_FAILED(event->Send()));
+ }
+
+ mPendingFunctionalEvents.Clear();
+}
+
+nsresult ServiceWorkerPrivate::GetDebugger(nsIWorkerDebugger** aResult) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aResult);
+
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+nsresult ServiceWorkerPrivate::AttachDebugger() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ // When the first debugger attaches to a worker, we spawn a worker if needed,
+ // and cancel the idle timeout. The idle timeout should not be reset until
+ // the last debugger detached from the worker.
+ if (!mDebuggerCount) {
+ nsresult rv = SpawnWorkerIfNeeded();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ /**
+ * Renewing the idle KeepAliveToken for spawning workers happens
+ * asynchronously, rather than synchronously.
+ * The asynchronous renewal is because the actual spawning of workers occurs
+ * in a content process, so we will only renew once notified that the worker
+ * has been successfully created
+ *
+ * This means that the DevTools way of starting up a worker by calling
+ * `AttachDebugger` immediately followed by `DetachDebugger` will spawn and
+ * immediately terminate a worker (because `mTokenCount` is possibly 0
+ * due to the idle KeepAliveToken being created asynchronously). So, just
+ * renew the KeepAliveToken right now.
+ */
+ RenewKeepAliveToken();
+ mIdleWorkerTimer->Cancel();
+ }
+
+ ++mDebuggerCount;
+
+ return NS_OK;
+}
+
+nsresult ServiceWorkerPrivate::DetachDebugger() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (!mDebuggerCount) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ --mDebuggerCount;
+
+ // When the last debugger detaches from a worker, we either reset the idle
+ // timeout, or terminate the worker if there are no more active tokens.
+ if (!mDebuggerCount) {
+ if (mTokenCount) {
+ ResetIdleTimeout();
+ } else {
+ TerminateWorker();
+ }
+ }
+
+ return NS_OK;
+}
+
+bool ServiceWorkerPrivate::IsIdle() const {
+ MOZ_ASSERT(NS_IsMainThread());
+ return mTokenCount == 0 || (mTokenCount == 1 && mIdleKeepAliveToken);
+}
+
+RefPtr<GenericPromise> ServiceWorkerPrivate::GetIdlePromise() {
+#ifdef DEBUG
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(!IsIdle());
+ MOZ_ASSERT(!mIdlePromiseObtained, "Idle promise may only be obtained once!");
+ mIdlePromiseObtained = true;
+#endif
+
+ return mIdlePromiseHolder.Ensure(__func__);
+}
+
+namespace {
+
+class ServiceWorkerPrivateTimerCallback final : public nsITimerCallback,
+ public nsINamed {
+ public:
+ using Method = void (ServiceWorkerPrivate::*)(nsITimer*);
+
+ ServiceWorkerPrivateTimerCallback(ServiceWorkerPrivate* aServiceWorkerPrivate,
+ Method aMethod)
+ : mServiceWorkerPrivate(aServiceWorkerPrivate), mMethod(aMethod) {}
+
+ NS_IMETHOD
+ Notify(nsITimer* aTimer) override {
+ (mServiceWorkerPrivate->*mMethod)(aTimer);
+ mServiceWorkerPrivate = nullptr;
+ return NS_OK;
+ }
+
+ NS_IMETHOD
+ GetName(nsACString& aName) override {
+ aName.AssignLiteral("ServiceWorkerPrivateTimerCallback");
+ return NS_OK;
+ }
+
+ private:
+ ~ServiceWorkerPrivateTimerCallback() = default;
+
+ RefPtr<ServiceWorkerPrivate> mServiceWorkerPrivate;
+ Method mMethod;
+
+ NS_DECL_THREADSAFE_ISUPPORTS
+};
+
+NS_IMPL_ISUPPORTS(ServiceWorkerPrivateTimerCallback, nsITimerCallback,
+ nsINamed);
+
+} // anonymous namespace
+
+void ServiceWorkerPrivate::NoteIdleWorkerCallback(nsITimer* aTimer) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ MOZ_ASSERT(aTimer == mIdleWorkerTimer, "Invalid timer!");
+
+ // Release ServiceWorkerPrivate's token, since the grace period has ended.
+ mIdleKeepAliveToken = nullptr;
+
+ if (mControllerChild) {
+ // If we still have a living worker at this point it means that either there
+ // are pending waitUntil promises or the worker is doing some long-running
+ // computation. Wait a bit more until we forcibly terminate the worker.
+ uint32_t timeout =
+ Preferences::GetInt("dom.serviceWorkers.idle_extended_timeout");
+ nsCOMPtr<nsITimerCallback> cb = new ServiceWorkerPrivateTimerCallback(
+ this, &ServiceWorkerPrivate::TerminateWorkerCallback);
+ DebugOnly<nsresult> rv = mIdleWorkerTimer->InitWithCallback(
+ cb, timeout, nsITimer::TYPE_ONE_SHOT);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+ }
+}
+
+void ServiceWorkerPrivate::TerminateWorkerCallback(nsITimer* aTimer) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ MOZ_ASSERT(aTimer == this->mIdleWorkerTimer, "Invalid timer!");
+
+ // mInfo must be non-null at this point because NoteDeadServiceWorkerInfo
+ // which zeroes it calls TerminateWorker which cancels our timer which will
+ // ensure we don't get invoked even if the nsTimerEvent is in the event queue.
+ ServiceWorkerManager::LocalizeAndReportToAllClients(
+ mInfo->Scope(), "ServiceWorkerGraceTimeoutTermination",
+ nsTArray<nsString>{NS_ConvertUTF8toUTF16(mInfo->Scope())});
+
+ TerminateWorker();
+}
+
+void ServiceWorkerPrivate::RenewKeepAliveToken() {
+ // We should have an active worker if we're renewing the keep alive token.
+ MOZ_ASSERT(mControllerChild);
+
+ // If there is at least one debugger attached to the worker, the idle worker
+ // timeout was canceled when the first debugger attached to the worker. It
+ // should not be reset until the last debugger detaches from the worker.
+ if (!mDebuggerCount) {
+ ResetIdleTimeout();
+ }
+
+ if (!mIdleKeepAliveToken) {
+ mIdleKeepAliveToken = new KeepAliveToken(this);
+ }
+}
+
+void ServiceWorkerPrivate::ResetIdleTimeout() {
+ uint32_t timeout = Preferences::GetInt("dom.serviceWorkers.idle_timeout");
+ nsCOMPtr<nsITimerCallback> cb = new ServiceWorkerPrivateTimerCallback(
+ this, &ServiceWorkerPrivate::NoteIdleWorkerCallback);
+ DebugOnly<nsresult> rv =
+ mIdleWorkerTimer->InitWithCallback(cb, timeout, nsITimer::TYPE_ONE_SHOT);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+}
+
+void ServiceWorkerPrivate::AddToken() {
+ MOZ_ASSERT(NS_IsMainThread());
+ ++mTokenCount;
+}
+
+void ServiceWorkerPrivate::ReleaseToken() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ MOZ_ASSERT(mTokenCount > 0);
+ --mTokenCount;
+
+ if (IsIdle()) {
+ mIdlePromiseHolder.ResolveIfExists(true, __func__);
+
+ if (!mTokenCount) {
+ TerminateWorker();
+ }
+
+ // mInfo can be nullptr here if NoteDeadServiceWorkerInfo() is called while
+ // the KeepAliveToken is being proxy released as a runnable.
+ else if (mInfo) {
+ RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance();
+ if (swm) {
+ swm->WorkerIsIdle(mInfo);
+ }
+ }
+ }
+}
+
+already_AddRefed<KeepAliveToken>
+ServiceWorkerPrivate::CreateEventKeepAliveToken() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ // When the WorkerPrivate is in a separate process, we first hold a normal
+ // KeepAliveToken. Then, after we're notified that the worker is alive, we
+ // create the idle KeepAliveToken.
+ MOZ_ASSERT(mIdleKeepAliveToken || mControllerChild);
+
+ RefPtr<KeepAliveToken> ref = new KeepAliveToken(this);
+ return ref.forget();
+}
+
+void ServiceWorkerPrivate::SetHandlesFetch(bool aValue) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (NS_WARN_IF(!mInfo)) {
+ return;
+ }
+
+ mInfo->SetHandlesFetch(aValue);
+}
+
+RefPtr<GenericPromise> ServiceWorkerPrivate::SetSkipWaitingFlag() {
+ AssertIsOnMainThread();
+ MOZ_ASSERT(mInfo);
+
+ RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance();
+
+ if (!swm) {
+ return GenericPromise::CreateAndReject(NS_ERROR_FAILURE, __func__);
+ }
+
+ RefPtr<ServiceWorkerRegistrationInfo> regInfo =
+ swm->GetRegistration(mInfo->Principal(), mInfo->Scope());
+
+ if (!regInfo) {
+ return GenericPromise::CreateAndReject(NS_ERROR_FAILURE, __func__);
+ }
+
+ mInfo->SetSkipWaitingFlag();
+
+ RefPtr<GenericPromise::Private> promise =
+ new GenericPromise::Private(__func__);
+
+ regInfo->TryToActivateAsync([promise] { promise->Resolve(true, __func__); });
+
+ return promise;
+}
+
+/* static */
+void ServiceWorkerPrivate::UpdateRunning(int32_t aDelta, int32_t aFetchDelta) {
+ // Record values for time we were running at the current values
+ RefPtr<ServiceWorkerManager> manager(ServiceWorkerManager::GetInstance());
+ manager->RecordTelemetry(sRunningServiceWorkers, sRunningServiceWorkersFetch);
+
+ MOZ_ASSERT(((int64_t)sRunningServiceWorkers) + aDelta >= 0);
+ sRunningServiceWorkers += aDelta;
+ if (sRunningServiceWorkers > sRunningServiceWorkersMax) {
+ sRunningServiceWorkersMax = sRunningServiceWorkers;
+ LOG(("ServiceWorker max now %d", sRunningServiceWorkersMax));
+ Telemetry::ScalarSet(Telemetry::ScalarID::SERVICEWORKER_RUNNING_MAX,
+ u"All"_ns, sRunningServiceWorkersMax);
+ }
+ MOZ_ASSERT(((int64_t)sRunningServiceWorkersFetch) + aFetchDelta >= 0);
+ sRunningServiceWorkersFetch += aFetchDelta;
+ if (sRunningServiceWorkersFetch > sRunningServiceWorkersFetchMax) {
+ sRunningServiceWorkersFetchMax = sRunningServiceWorkersFetch;
+ LOG(("ServiceWorker Fetch max now %d", sRunningServiceWorkersFetchMax));
+ Telemetry::ScalarSet(Telemetry::ScalarID::SERVICEWORKER_RUNNING_MAX,
+ u"Fetch"_ns, sRunningServiceWorkersFetchMax);
+ }
+ LOG(("ServiceWorkers running now %d/%d", sRunningServiceWorkers,
+ sRunningServiceWorkersFetch));
+}
+
+void ServiceWorkerPrivate::CreationFailed() {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(mControllerChild);
+
+ if (mRemoteWorkerData.remoteType().Find(SERVICEWORKER_REMOTE_TYPE) !=
+ kNotFound) {
+ Telemetry::AccumulateTimeDelta(
+ Telemetry::SERVICE_WORKER_ISOLATED_LAUNCH_TIME,
+ mServiceWorkerLaunchTimeStart);
+ } else {
+ Telemetry::AccumulateTimeDelta(Telemetry::SERVICE_WORKER_LAUNCH_TIME_2,
+ mServiceWorkerLaunchTimeStart);
+ }
+
+ Shutdown();
+}
+
+void ServiceWorkerPrivate::CreationSucceeded() {
+ AssertIsOnMainThread();
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(mInfo);
+ MOZ_ASSERT(mControllerChild);
+
+ if (mRemoteWorkerData.remoteType().Find(SERVICEWORKER_REMOTE_TYPE) !=
+ kNotFound) {
+ Telemetry::AccumulateTimeDelta(
+ Telemetry::SERVICE_WORKER_ISOLATED_LAUNCH_TIME,
+ mServiceWorkerLaunchTimeStart);
+ } else {
+ Telemetry::AccumulateTimeDelta(Telemetry::SERVICE_WORKER_LAUNCH_TIME_2,
+ mServiceWorkerLaunchTimeStart);
+ }
+
+ RenewKeepAliveToken();
+
+ RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance();
+ nsCOMPtr<nsIPrincipal> principal = mInfo->Principal();
+ RefPtr<ServiceWorkerRegistrationInfo> regInfo =
+ swm->GetRegistration(principal, mInfo->Scope());
+ if (regInfo) {
+ // If it's already set, we're done and the running count is already set
+ if (mHandlesFetch == Unknown) {
+ if (regInfo->GetActive()) {
+ mHandlesFetch =
+ regInfo->GetActive()->HandlesFetch() ? Enabled : Disabled;
+ if (mHandlesFetch == Enabled) {
+ UpdateRunning(0, 1);
+ }
+ }
+ // else we're likely still in Evaluating state, and don't know if it
+ // handles fetch. If so, defer updating the counter for Fetch until we
+ // finish evaluation. We already updated the Running count for All in
+ // SpawnWorkerIfNeeded().
+ }
+ }
+}
+
+void ServiceWorkerPrivate::ErrorReceived(const ErrorValue& aError) {
+ AssertIsOnMainThread();
+ MOZ_ASSERT(mInfo);
+ MOZ_ASSERT(mControllerChild);
+
+ RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance();
+ MOZ_ASSERT(swm);
+
+ ServiceWorkerInfo* info = mInfo;
+
+ swm->HandleError(nullptr, info->Principal(), info->Scope(),
+ NS_ConvertUTF8toUTF16(info->ScriptSpec()), u""_ns, u""_ns,
+ u""_ns, 0, 0, nsIScriptError::errorFlag, JSEXN_ERR);
+}
+
+void ServiceWorkerPrivate::Terminated() {
+ AssertIsOnMainThread();
+ MOZ_ASSERT(mInfo);
+ MOZ_ASSERT(mControllerChild);
+
+ Shutdown();
+}
+
+void ServiceWorkerPrivate::RefreshRemoteWorkerData(
+ const RefPtr<ServiceWorkerRegistrationInfo>& aRegistration) {
+ AssertIsOnMainThread();
+ MOZ_ASSERT(mInfo);
+
+ ServiceWorkerData& serviceWorkerData =
+ mRemoteWorkerData.serviceWorkerData().get_ServiceWorkerData();
+ serviceWorkerData.descriptor() = mInfo->Descriptor().ToIPC();
+ serviceWorkerData.registrationDescriptor() =
+ aRegistration->Descriptor().ToIPC();
+}
+
+RefPtr<FetchServicePromises> ServiceWorkerPrivate::SetupNavigationPreload(
+ nsCOMPtr<nsIInterceptedChannel>& aChannel,
+ const RefPtr<ServiceWorkerRegistrationInfo>& aRegistration) {
+ MOZ_ASSERT(XRE_IsParentProcess());
+ AssertIsOnMainThread();
+
+ // create IPC request from the intercepted channel.
+ auto result = GetIPCInternalRequest(aChannel);
+ if (result.isErr()) {
+ return nullptr;
+ }
+ IPCInternalRequest ipcRequest = result.unwrap();
+
+ // Step 1. Clone the request for preload
+ // Create the InternalResponse from the created IPCRequest.
+ SafeRefPtr<InternalRequest> preloadRequest =
+ MakeSafeRefPtr<InternalRequest>(ipcRequest);
+ // Copy the request body from uploadChannel
+ nsCOMPtr<nsIUploadChannel2> uploadChannel = do_QueryInterface(aChannel);
+ if (uploadChannel) {
+ nsCOMPtr<nsIInputStream> uploadStream;
+ nsresult rv = uploadChannel->CloneUploadStream(
+ &ipcRequest.bodySize(), getter_AddRefs(uploadStream));
+ // Fail to get the request's body, stop navigation preload by returning
+ // nullptr.
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return FetchService::NetworkErrorResponse(rv);
+ }
+ preloadRequest->SetBody(uploadStream, ipcRequest.bodySize());
+ }
+
+ // Set SkipServiceWorker for the navigation preload request
+ preloadRequest->SetSkipServiceWorker();
+
+ // Step 2. Append Service-Worker-Navigation-Preload header with
+ // registration->GetNavigationPreloadState().headerValue() on
+ // request's header list.
+ IgnoredErrorResult err;
+ auto headersGuard = preloadRequest->Headers()->Guard();
+ preloadRequest->Headers()->SetGuard(HeadersGuardEnum::None, err);
+ preloadRequest->Headers()->Append(
+ "Service-Worker-Navigation-Preload"_ns,
+ aRegistration->GetNavigationPreloadState().headerValue(), err);
+ preloadRequest->Headers()->SetGuard(headersGuard, err);
+
+ // Step 3. Perform fetch through FetchService with the cloned request
+ if (!err.Failed()) {
+ nsCOMPtr<nsIChannel> underlyingChannel;
+ MOZ_ALWAYS_SUCCEEDS(
+ aChannel->GetChannel(getter_AddRefs(underlyingChannel)));
+ RefPtr<FetchService> fetchService = FetchService::GetInstance();
+ return fetchService->Fetch(std::move(preloadRequest), underlyingChannel);
+ }
+ return FetchService::NetworkErrorResponse(NS_ERROR_UNEXPECTED);
+}
+
+void ServiceWorkerPrivate::Shutdown() {
+ AssertIsOnMainThread();
+
+ if (mControllerChild) {
+ RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance();
+
+ MOZ_ASSERT(swm,
+ "All Service Workers should start shutting down before the "
+ "ServiceWorkerManager does!");
+
+ auto shutdownStateId = swm->MaybeInitServiceWorkerShutdownProgress();
+
+ RefPtr<GenericNonExclusivePromise> promise =
+ ShutdownInternal(shutdownStateId);
+ swm->BlockShutdownOn(promise, shutdownStateId);
+ }
+
+ MOZ_ASSERT(!mControllerChild);
+}
+
+RefPtr<GenericNonExclusivePromise> ServiceWorkerPrivate::ShutdownInternal(
+ uint32_t aShutdownStateId) {
+ AssertIsOnMainThread();
+ MOZ_ASSERT(mControllerChild);
+
+ mPendingFunctionalEvents.Clear();
+
+ mControllerChild->get()->RevokeObserver(this);
+
+ if (StaticPrefs::dom_serviceWorkers_testing_enabled()) {
+ nsCOMPtr<nsIObserverService> os = services::GetObserverService();
+ if (os) {
+ os->NotifyObservers(nullptr, "service-worker-shutdown", nullptr);
+ }
+ }
+
+ RefPtr<GenericNonExclusivePromise::Private> promise =
+ new GenericNonExclusivePromise::Private(__func__);
+
+ Unused << ExecServiceWorkerOp(
+ ServiceWorkerTerminateWorkerOpArgs(aShutdownStateId),
+ [promise](ServiceWorkerOpResult&& aResult) {
+ MOZ_ASSERT(aResult.type() == ServiceWorkerOpResult::Tnsresult);
+ promise->Resolve(true, __func__);
+ },
+ [promise]() { promise->Reject(NS_ERROR_DOM_ABORT_ERR, __func__); });
+
+ /**
+ * After dispatching a termination operation, no new operations should
+ * be routed through this actor anymore.
+ */
+ mControllerChild = nullptr;
+
+ // Update here, since Evaluation failures directly call ShutdownInternal
+ UpdateRunning(-1, mHandlesFetch == Enabled ? -1 : 0);
+
+ return promise;
+}
+
+nsresult ServiceWorkerPrivate::ExecServiceWorkerOp(
+ ServiceWorkerOpArgs&& aArgs,
+ std::function<void(ServiceWorkerOpResult&&)>&& aSuccessCallback,
+ std::function<void()>&& aFailureCallback) {
+ AssertIsOnMainThread();
+ MOZ_ASSERT(
+ aArgs.type() !=
+ ServiceWorkerOpArgs::TParentToChildServiceWorkerFetchEventOpArgs,
+ "FetchEvent operations should be sent through FetchEventOp(Proxy) "
+ "actors!");
+ MOZ_ASSERT(aSuccessCallback);
+
+ nsresult rv = SpawnWorkerIfNeeded();
+
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ aFailureCallback();
+ return rv;
+ }
+
+ MOZ_ASSERT(mControllerChild);
+
+ RefPtr<ServiceWorkerPrivate> self = this;
+ RefPtr<RAIIActorPtrHolder> holder = mControllerChild;
+ RefPtr<KeepAliveToken> token =
+ aArgs.type() == ServiceWorkerOpArgs::TServiceWorkerTerminateWorkerOpArgs
+ ? nullptr
+ : CreateEventKeepAliveToken();
+
+ /**
+ * NOTE: moving `aArgs` won't do anything until IPDL `SendMethod()` methods
+ * can accept rvalue references rather than just const references.
+ */
+ mControllerChild->get()->SendExecServiceWorkerOp(aArgs)->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [self = std::move(self), holder = std::move(holder),
+ token = std::move(token), onSuccess = std::move(aSuccessCallback),
+ onFailure = std::move(aFailureCallback)](
+ PRemoteWorkerControllerChild::ExecServiceWorkerOpPromise::
+ ResolveOrRejectValue&& aResult) {
+ if (NS_WARN_IF(aResult.IsReject())) {
+ onFailure();
+ return;
+ }
+
+ onSuccess(std::move(aResult.ResolveValue()));
+ });
+
+ return NS_OK;
+}
+
+} // namespace mozilla::dom
diff --git a/dom/serviceworkers/ServiceWorkerPrivate.h b/dom/serviceworkers/ServiceWorkerPrivate.h
new file mode 100644
index 0000000000..24bdc1666c
--- /dev/null
+++ b/dom/serviceworkers/ServiceWorkerPrivate.h
@@ -0,0 +1,380 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_serviceworkerprivate_h
+#define mozilla_dom_serviceworkerprivate_h
+
+#include <functional>
+#include <type_traits>
+
+#include "mozilla/Attributes.h"
+#include "mozilla/MozPromise.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/TimeStamp.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/dom/FetchService.h"
+#include "mozilla/dom/RemoteWorkerController.h"
+#include "mozilla/dom/RemoteWorkerTypes.h"
+#include "mozilla/dom/ServiceWorkerOpArgs.h"
+#include "nsCOMPtr.h"
+#include "nsISupportsImpl.h"
+#include "nsTArray.h"
+
+#define NOTIFICATION_CLICK_EVENT_NAME u"notificationclick"
+#define NOTIFICATION_CLOSE_EVENT_NAME u"notificationclose"
+
+class nsIInterceptedChannel;
+class nsIWorkerDebugger;
+
+namespace mozilla {
+
+template <typename T>
+class Maybe;
+
+class JSObjectHolder;
+
+namespace dom {
+
+class ClientInfoAndState;
+class RemoteWorkerControllerChild;
+class ServiceWorkerCloneData;
+class ServiceWorkerInfo;
+class ServiceWorkerPrivate;
+class ServiceWorkerRegistrationInfo;
+
+namespace ipc {
+class StructuredCloneData;
+} // namespace ipc
+
+class LifeCycleEventCallback : public Runnable {
+ public:
+ LifeCycleEventCallback() : Runnable("dom::LifeCycleEventCallback") {}
+
+ // Called on the worker thread.
+ virtual void SetResult(bool aResult) = 0;
+};
+
+// Used to keep track of pending waitUntil as well as in-flight extendable
+// events. When the last token is released, we attempt to terminate the worker.
+class KeepAliveToken final : public nsISupports {
+ public:
+ NS_DECL_ISUPPORTS
+
+ explicit KeepAliveToken(ServiceWorkerPrivate* aPrivate);
+
+ private:
+ ~KeepAliveToken();
+
+ RefPtr<ServiceWorkerPrivate> mPrivate;
+};
+
+class ServiceWorkerPrivate final : public RemoteWorkerObserver {
+ friend class KeepAliveToken;
+
+ public:
+ NS_INLINE_DECL_REFCOUNTING(ServiceWorkerPrivate, override);
+
+ using PromiseExtensionWorkerHasListener = MozPromise<bool, nsresult, false>;
+
+ public:
+ explicit ServiceWorkerPrivate(ServiceWorkerInfo* aInfo);
+
+ nsresult SendMessageEvent(RefPtr<ServiceWorkerCloneData>&& aData,
+ const ClientInfoAndState& aClientInfoAndState);
+
+ // This is used to validate the worker script and continue the installation
+ // process.
+ nsresult CheckScriptEvaluation(RefPtr<LifeCycleEventCallback> aCallback);
+
+ nsresult SendLifeCycleEvent(const nsAString& aEventType,
+ RefPtr<LifeCycleEventCallback> aCallback);
+
+ nsresult SendPushEvent(const nsAString& aMessageId,
+ const Maybe<nsTArray<uint8_t>>& aData,
+ RefPtr<ServiceWorkerRegistrationInfo> aRegistration);
+
+ nsresult SendPushSubscriptionChangeEvent();
+
+ nsresult SendNotificationEvent(const nsAString& aEventName,
+ const nsAString& aID, const nsAString& aTitle,
+ const nsAString& aDir, const nsAString& aLang,
+ const nsAString& aBody, const nsAString& aTag,
+ const nsAString& aIcon, const nsAString& aData,
+ const nsAString& aBehavior,
+ const nsAString& aScope);
+
+ nsresult SendFetchEvent(nsCOMPtr<nsIInterceptedChannel> aChannel,
+ nsILoadGroup* aLoadGroup, const nsAString& aClientId,
+ const nsAString& aResultingClientId);
+
+ Result<RefPtr<PromiseExtensionWorkerHasListener>, nsresult>
+ WakeForExtensionAPIEvent(const nsAString& aExtensionAPINamespace,
+ const nsAString& aEXtensionAPIEventName);
+
+ // This will terminate the current running worker thread and drop the
+ // workerPrivate reference.
+ // Called by ServiceWorkerInfo when [[Clear Registration]] is invoked
+ // or whenever the spec mandates that we terminate the worker.
+ // This is a no-op if the worker has already been stopped.
+ void TerminateWorker();
+
+ void NoteDeadServiceWorkerInfo();
+
+ void NoteStoppedControllingDocuments();
+
+ void UpdateState(ServiceWorkerState aState);
+
+ nsresult GetDebugger(nsIWorkerDebugger** aResult);
+
+ nsresult AttachDebugger();
+
+ nsresult DetachDebugger();
+
+ bool IsIdle() const;
+
+ // This promise is used schedule clearing of the owning registrations and its
+ // associated Service Workers if that registration becomes "unreachable" by
+ // the ServiceWorkerManager. This occurs under two conditions, which are the
+ // preconditions to calling this method:
+ // - The owning registration must be unregistered.
+ // - The associated Service Worker must *not* be controlling clients.
+ //
+ // Additionally, perhaps stating the obvious, the associated Service Worker
+ // must *not* be idle (whatever must be done "when idle" can just be done
+ // immediately).
+ RefPtr<GenericPromise> GetIdlePromise();
+
+ void SetHandlesFetch(bool aValue);
+
+ RefPtr<GenericPromise> SetSkipWaitingFlag();
+
+ static void RunningShutdown() {
+ // Force a final update of the number of running ServiceWorkers
+ UpdateRunning(0, 0);
+ MOZ_ASSERT(sRunningServiceWorkers == 0);
+ MOZ_ASSERT(sRunningServiceWorkersFetch == 0);
+ }
+
+ /**
+ * Update Telemetry for # of running ServiceWorkers
+ */
+ static void UpdateRunning(int32_t aDelta, int32_t aFetchDelta);
+
+ private:
+ // Timer callbacks
+ void NoteIdleWorkerCallback(nsITimer* aTimer);
+
+ void TerminateWorkerCallback(nsITimer* aTimer);
+
+ void RenewKeepAliveToken();
+
+ void ResetIdleTimeout();
+
+ void AddToken();
+
+ void ReleaseToken();
+
+ already_AddRefed<KeepAliveToken> CreateEventKeepAliveToken();
+
+ nsresult SpawnWorkerIfNeeded();
+
+ ~ServiceWorkerPrivate();
+
+ nsresult Initialize();
+
+ /**
+ * RemoteWorkerObserver
+ */
+ void CreationFailed() override;
+
+ void CreationSucceeded() override;
+
+ void ErrorReceived(const ErrorValue& aError) override;
+
+ void LockNotified(bool aCreated) final {
+ // no-op for service workers
+ }
+
+ void Terminated() override;
+
+ // Refreshes only the parts of mRemoteWorkerData that may change over time.
+ void RefreshRemoteWorkerData(
+ const RefPtr<ServiceWorkerRegistrationInfo>& aRegistration);
+
+ nsresult SendPushEventInternal(
+ RefPtr<ServiceWorkerRegistrationInfo>&& aRegistration,
+ ServiceWorkerPushEventOpArgs&& aArgs);
+
+ // Setup the navigation preload by the intercepted channel and the
+ // RegistrationInfo.
+ RefPtr<FetchServicePromises> SetupNavigationPreload(
+ nsCOMPtr<nsIInterceptedChannel>& aChannel,
+ const RefPtr<ServiceWorkerRegistrationInfo>& aRegistration);
+
+ nsresult SendFetchEventInternal(
+ RefPtr<ServiceWorkerRegistrationInfo>&& aRegistration,
+ ParentToParentServiceWorkerFetchEventOpArgs&& aArgs,
+ nsCOMPtr<nsIInterceptedChannel>&& aChannel,
+ RefPtr<FetchServicePromises>&& aPreloadResponseReadyPromises);
+
+ void Shutdown();
+
+ RefPtr<GenericNonExclusivePromise> ShutdownInternal(
+ uint32_t aShutdownStateId);
+
+ nsresult ExecServiceWorkerOp(
+ ServiceWorkerOpArgs&& aArgs,
+ std::function<void(ServiceWorkerOpResult&&)>&& aSuccessCallback,
+ std::function<void()>&& aFailureCallback = [] {});
+
+ class PendingFunctionalEvent {
+ public:
+ PendingFunctionalEvent(
+ ServiceWorkerPrivate* aOwner,
+ RefPtr<ServiceWorkerRegistrationInfo>&& aRegistration);
+
+ virtual ~PendingFunctionalEvent();
+
+ virtual nsresult Send() = 0;
+
+ protected:
+ ServiceWorkerPrivate* const MOZ_NON_OWNING_REF mOwner;
+ RefPtr<ServiceWorkerRegistrationInfo> mRegistration;
+ };
+
+ class PendingPushEvent final : public PendingFunctionalEvent {
+ public:
+ PendingPushEvent(ServiceWorkerPrivate* aOwner,
+ RefPtr<ServiceWorkerRegistrationInfo>&& aRegistration,
+ ServiceWorkerPushEventOpArgs&& aArgs);
+
+ nsresult Send() override;
+
+ private:
+ ServiceWorkerPushEventOpArgs mArgs;
+ };
+
+ class PendingFetchEvent final : public PendingFunctionalEvent {
+ public:
+ PendingFetchEvent(
+ ServiceWorkerPrivate* aOwner,
+ RefPtr<ServiceWorkerRegistrationInfo>&& aRegistration,
+ ParentToParentServiceWorkerFetchEventOpArgs&& aArgs,
+ nsCOMPtr<nsIInterceptedChannel>&& aChannel,
+ RefPtr<FetchServicePromises>&& aPreloadResponseReadyPromises);
+
+ nsresult Send() override;
+
+ ~PendingFetchEvent();
+
+ private:
+ ParentToParentServiceWorkerFetchEventOpArgs mArgs;
+ nsCOMPtr<nsIInterceptedChannel> mChannel;
+ // The promises from FetchService. It indicates if the preload response is
+ // ready or not. The promise's resolve/reject value should be handled in
+ // FetchEventOpChild, such that the preload result can be propagated to the
+ // ServiceWorker through IPC. However, FetchEventOpChild creation could be
+ // pending here, so this member is needed. And it will be forwarded to
+ // FetchEventOpChild when crearting the FetchEventOpChild.
+ RefPtr<FetchServicePromises> mPreloadResponseReadyPromises;
+ };
+
+ nsTArray<UniquePtr<PendingFunctionalEvent>> mPendingFunctionalEvents;
+
+ /**
+ * It's possible that there are still in-progress operations when a
+ * a termination operation is issued. In this case, it's important to keep
+ * the RemoteWorkerControllerChild actor alive until all pending operations
+ * have completed before destroying it with Send__delete__().
+ *
+ * RAIIActorPtrHolder holds a singular, owning reference to a
+ * RemoteWorkerControllerChild actor and is responsible for destroying the
+ * actor in its (i.e. the holder's) destructor. This implies that all
+ * in-progress operations must maintain a strong reference to their
+ * corresponding holders and release the reference once completed/canceled.
+ *
+ * Additionally a RAIIActorPtrHolder must be initialized with a non-null actor
+ * and cannot be moved or copied. Therefore, the identities of two held
+ * actors can be compared by simply comparing their holders' addresses.
+ */
+ class RAIIActorPtrHolder final {
+ public:
+ NS_INLINE_DECL_REFCOUNTING(RAIIActorPtrHolder)
+
+ explicit RAIIActorPtrHolder(
+ already_AddRefed<RemoteWorkerControllerChild> aActor);
+
+ RAIIActorPtrHolder(const RAIIActorPtrHolder& aOther) = delete;
+ RAIIActorPtrHolder& operator=(const RAIIActorPtrHolder& aOther) = delete;
+
+ RAIIActorPtrHolder(RAIIActorPtrHolder&& aOther) = delete;
+ RAIIActorPtrHolder& operator=(RAIIActorPtrHolder&& aOther) = delete;
+
+ RemoteWorkerControllerChild* operator->() const
+ MOZ_NO_ADDREF_RELEASE_ON_RETURN;
+
+ RemoteWorkerControllerChild* get() const;
+
+ RefPtr<GenericPromise> OnDestructor();
+
+ private:
+ ~RAIIActorPtrHolder();
+
+ MozPromiseHolder<GenericPromise> mDestructorPromiseHolder;
+
+ const RefPtr<RemoteWorkerControllerChild> mActor;
+ };
+
+ RefPtr<RAIIActorPtrHolder> mControllerChild;
+
+ RemoteWorkerData mRemoteWorkerData;
+
+ TimeStamp mServiceWorkerLaunchTimeStart;
+
+ // Counters for Telemetry - totals running simultaneously, and those that
+ // handle Fetch, plus Max values for each
+ static uint32_t sRunningServiceWorkers;
+ static uint32_t sRunningServiceWorkersFetch;
+ static uint32_t sRunningServiceWorkersMax;
+ static uint32_t sRunningServiceWorkersFetchMax;
+
+ // We know the state after we've evaluated the worker, and we then store
+ // it in the registration. The only valid state transition should be
+ // from Unknown to Enabled or Disabled.
+ enum { Unknown, Enabled, Disabled } mHandlesFetch{Unknown};
+
+ // The info object owns us. It is possible to outlive it for a brief period
+ // of time if there are pending waitUntil promises, in which case it
+ // will be null and |SpawnWorkerIfNeeded| will always fail.
+ ServiceWorkerInfo* MOZ_NON_OWNING_REF mInfo;
+
+ nsCOMPtr<nsITimer> mIdleWorkerTimer;
+
+ // We keep a token for |dom.serviceWorkers.idle_timeout| seconds to give the
+ // worker a grace period after each event.
+ RefPtr<KeepAliveToken> mIdleKeepAliveToken;
+
+ uint64_t mDebuggerCount;
+
+ uint64_t mTokenCount;
+
+ // Used by the owning `ServiceWorkerRegistrationInfo` when it wants to call
+ // `Clear` after being unregistered and isn't controlling any clients but this
+ // worker (i.e. the registration's active worker) isn't idle yet. Note that
+ // such an event should happen at most once in a
+ // `ServiceWorkerRegistrationInfo`s lifetime, so this promise should also only
+ // be obtained at most once.
+ MozPromiseHolder<GenericPromise> mIdlePromiseHolder;
+
+#ifdef DEBUG
+ bool mIdlePromiseObtained = false;
+#endif
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_serviceworkerprivate_h
diff --git a/dom/serviceworkers/ServiceWorkerProxy.cpp b/dom/serviceworkers/ServiceWorkerProxy.cpp
new file mode 100644
index 0000000000..aa6b77c1fa
--- /dev/null
+++ b/dom/serviceworkers/ServiceWorkerProxy.cpp
@@ -0,0 +1,122 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "ServiceWorkerProxy.h"
+#include "ServiceWorkerCloneData.h"
+#include "ServiceWorkerManager.h"
+#include "ServiceWorkerParent.h"
+
+#include "mozilla/SchedulerGroup.h"
+#include "mozilla/ScopeExit.h"
+#include "mozilla/dom/ClientState.h"
+#include "mozilla/ipc/BackgroundParent.h"
+#include "ServiceWorkerInfo.h"
+
+namespace mozilla::dom {
+
+using mozilla::ipc::AssertIsOnBackgroundThread;
+
+ServiceWorkerProxy::~ServiceWorkerProxy() {
+ // Any thread
+ MOZ_DIAGNOSTIC_ASSERT(!mActor);
+ MOZ_DIAGNOSTIC_ASSERT(!mInfo);
+}
+
+void ServiceWorkerProxy::MaybeShutdownOnBGThread() {
+ AssertIsOnBackgroundThread();
+ if (!mActor) {
+ return;
+ }
+ mActor->MaybeSendDelete();
+}
+
+void ServiceWorkerProxy::InitOnMainThread() {
+ AssertIsOnMainThread();
+
+ auto scopeExit = MakeScopeExit([&] { MaybeShutdownOnMainThread(); });
+
+ RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance();
+ NS_ENSURE_TRUE_VOID(swm);
+
+ RefPtr<ServiceWorkerRegistrationInfo> reg =
+ swm->GetRegistration(mDescriptor.PrincipalInfo(), mDescriptor.Scope());
+ NS_ENSURE_TRUE_VOID(reg);
+
+ RefPtr<ServiceWorkerInfo> info = reg->GetByDescriptor(mDescriptor);
+ NS_ENSURE_TRUE_VOID(info);
+
+ scopeExit.release();
+
+ mInfo = new nsMainThreadPtrHolder<ServiceWorkerInfo>(
+ "ServiceWorkerProxy::mInfo", info);
+}
+
+void ServiceWorkerProxy::MaybeShutdownOnMainThread() {
+ AssertIsOnMainThread();
+
+ nsCOMPtr<nsIRunnable> r = NewRunnableMethod(
+ __func__, this, &ServiceWorkerProxy::MaybeShutdownOnBGThread);
+
+ MOZ_ALWAYS_SUCCEEDS(mEventTarget->Dispatch(r.forget(), NS_DISPATCH_NORMAL));
+}
+
+void ServiceWorkerProxy::StopListeningOnMainThread() {
+ AssertIsOnMainThread();
+ mInfo = nullptr;
+}
+
+ServiceWorkerProxy::ServiceWorkerProxy(
+ const ServiceWorkerDescriptor& aDescriptor)
+ : mEventTarget(GetCurrentSerialEventTarget()), mDescriptor(aDescriptor) {}
+
+void ServiceWorkerProxy::Init(ServiceWorkerParent* aActor) {
+ AssertIsOnBackgroundThread();
+ MOZ_DIAGNOSTIC_ASSERT(aActor);
+ MOZ_DIAGNOSTIC_ASSERT(!mActor);
+ MOZ_DIAGNOSTIC_ASSERT(mEventTarget);
+
+ mActor = aActor;
+
+ // Note, this must be done from a separate Init() method and not in
+ // the constructor. If done from the constructor the runnable can
+ // execute, complete, and release its reference before the constructor
+ // returns.
+ nsCOMPtr<nsIRunnable> r = NewRunnableMethod(
+ "ServiceWorkerProxy::Init", this, &ServiceWorkerProxy::InitOnMainThread);
+ MOZ_ALWAYS_SUCCEEDS(
+ SchedulerGroup::Dispatch(TaskCategory::Other, r.forget()));
+}
+
+void ServiceWorkerProxy::RevokeActor(ServiceWorkerParent* aActor) {
+ AssertIsOnBackgroundThread();
+ MOZ_DIAGNOSTIC_ASSERT(mActor);
+ MOZ_DIAGNOSTIC_ASSERT(mActor == aActor);
+ mActor = nullptr;
+
+ nsCOMPtr<nsIRunnable> r = NewRunnableMethod(
+ __func__, this, &ServiceWorkerProxy::StopListeningOnMainThread);
+ MOZ_ALWAYS_SUCCEEDS(
+ SchedulerGroup::Dispatch(TaskCategory::Other, r.forget()));
+}
+
+void ServiceWorkerProxy::PostMessage(RefPtr<ServiceWorkerCloneData>&& aData,
+ const ClientInfo& aClientInfo,
+ const ClientState& aClientState) {
+ AssertIsOnBackgroundThread();
+ RefPtr<ServiceWorkerProxy> self = this;
+ nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction(
+ __func__,
+ [self, data = std::move(aData), aClientInfo, aClientState]() mutable {
+ if (!self->mInfo) {
+ return;
+ }
+ self->mInfo->PostMessage(std::move(data), aClientInfo, aClientState);
+ });
+ MOZ_ALWAYS_SUCCEEDS(
+ SchedulerGroup::Dispatch(TaskCategory::Other, r.forget()));
+}
+
+} // namespace mozilla::dom
diff --git a/dom/serviceworkers/ServiceWorkerProxy.h b/dom/serviceworkers/ServiceWorkerProxy.h
new file mode 100644
index 0000000000..b8b5d7145e
--- /dev/null
+++ b/dom/serviceworkers/ServiceWorkerProxy.h
@@ -0,0 +1,61 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef moz_dom_ServiceWorkerProxy_h
+#define moz_dom_ServiceWorkerProxy_h
+
+#include "nsProxyRelease.h"
+#include "ServiceWorkerDescriptor.h"
+
+namespace mozilla::dom {
+
+class ClientInfo;
+class ClientState;
+class ServiceWorkerCloneData;
+class ServiceWorkerInfo;
+class ServiceWorkerParent;
+
+class ServiceWorkerProxy final {
+ // Background thread only
+ RefPtr<ServiceWorkerParent> mActor;
+
+ // Written on background thread and read on main thread
+ nsCOMPtr<nsISerialEventTarget> mEventTarget;
+
+ // Main thread only
+ ServiceWorkerDescriptor mDescriptor;
+ nsMainThreadPtrHandle<ServiceWorkerInfo> mInfo;
+
+ ~ServiceWorkerProxy();
+
+ // Background thread methods
+ void MaybeShutdownOnBGThread();
+
+ void SetStateOnBGThread(ServiceWorkerState aState);
+
+ // Main thread methods
+ void InitOnMainThread();
+
+ void MaybeShutdownOnMainThread();
+
+ void StopListeningOnMainThread();
+
+ public:
+ explicit ServiceWorkerProxy(const ServiceWorkerDescriptor& aDescriptor);
+
+ void Init(ServiceWorkerParent* aActor);
+
+ void RevokeActor(ServiceWorkerParent* aActor);
+
+ void PostMessage(RefPtr<ServiceWorkerCloneData>&& aData,
+ const ClientInfo& aClientInfo, const ClientState& aState);
+
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(ServiceWorkerProxy);
+};
+
+} // namespace mozilla::dom
+
+#endif // moz_dom_ServiceWorkerProxy_h
diff --git a/dom/serviceworkers/ServiceWorkerQuotaUtils.cpp b/dom/serviceworkers/ServiceWorkerQuotaUtils.cpp
new file mode 100644
index 0000000000..65e051eb40
--- /dev/null
+++ b/dom/serviceworkers/ServiceWorkerQuotaUtils.cpp
@@ -0,0 +1,327 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "MainThreadUtils.h"
+#include "ServiceWorkerQuotaUtils.h"
+
+#include "mozilla/ScopeExit.h"
+#include "mozilla/Services.h"
+#include "mozilla/StaticPrefs_dom.h"
+#include "mozilla/dom/quota/QuotaManagerService.h"
+#include "nsIClearDataService.h"
+#include "nsID.h"
+#include "nsIPrincipal.h"
+#include "nsIQuotaCallbacks.h"
+#include "nsIQuotaRequests.h"
+#include "nsIQuotaResults.h"
+#include "nsISupports.h"
+#include "nsIVariant.h"
+#include "nsServiceManagerUtils.h"
+
+using mozilla::dom::quota::QuotaManagerService;
+
+namespace mozilla::dom {
+
+/*
+ * QuotaUsageChecker implements the quota usage checking algorithm.
+ *
+ * 1. Getting the given origin/group usage through QuotaManagerService.
+ * QuotaUsageCheck::Start() implements this step.
+ * 2. Checking if the group usage headroom is satisfied.
+ * It could be following three situations.
+ * a. Group headroom is satisfied without any usage mitigation.
+ * b. Group headroom is satisfied after origin usage mitigation.
+ * This invokes nsIClearDataService::DeleteDataFromPrincipal().
+ * c. Group headroom is satisfied after group usage mitigation.
+ * This invokes nsIClearDataService::DeleteDataFromBaseDomain().
+ * QuotaUsageChecker::CheckQuotaHeadroom() implements this step.
+ *
+ * If the algorithm is done or error out, the QuotaUsageCheck::mCallback will
+ * be called with a bool result for external handling.
+ */
+class QuotaUsageChecker final : public nsIQuotaCallback,
+ public nsIQuotaUsageCallback,
+ public nsIClearDataCallback {
+ public:
+ NS_DECL_ISUPPORTS
+ // For QuotaManagerService::Estimate()
+ NS_DECL_NSIQUOTACALLBACK
+
+ // For QuotaManagerService::GetUsageForPrincipal()
+ NS_DECL_NSIQUOTAUSAGECALLBACK
+
+ // For nsIClearDataService::DeleteDataFromPrincipal() and
+ // nsIClearDataService::DeleteDataFromBaseDomain()
+ NS_DECL_NSICLEARDATACALLBACK
+
+ QuotaUsageChecker(nsIPrincipal* aPrincipal,
+ ServiceWorkerQuotaMitigationCallback&& aCallback);
+
+ void Start();
+
+ void RunCallback(bool aResult);
+
+ private:
+ ~QuotaUsageChecker() = default;
+
+ // This is a general help method to get nsIQuotaResult/nsIQuotaUsageResult
+ // from nsIQuotaRequest/nsIQuotaUsageRequest
+ template <typename T, typename U>
+ nsresult GetResult(T* aRequest, U&);
+
+ void CheckQuotaHeadroom();
+
+ nsCOMPtr<nsIPrincipal> mPrincipal;
+
+ // The external callback. Calling RunCallback(bool) instead of calling it
+ // directly, RunCallback(bool) handles the internal status.
+ ServiceWorkerQuotaMitigationCallback mCallback;
+ bool mGettingOriginUsageDone;
+ bool mGettingGroupUsageDone;
+ bool mIsChecking;
+ uint64_t mOriginUsage;
+ uint64_t mGroupUsage;
+ uint64_t mGroupLimit;
+};
+
+NS_IMPL_ISUPPORTS(QuotaUsageChecker, nsIQuotaCallback, nsIQuotaUsageCallback,
+ nsIClearDataCallback)
+
+QuotaUsageChecker::QuotaUsageChecker(
+ nsIPrincipal* aPrincipal, ServiceWorkerQuotaMitigationCallback&& aCallback)
+ : mPrincipal(aPrincipal),
+ mCallback(std::move(aCallback)),
+ mGettingOriginUsageDone(false),
+ mGettingGroupUsageDone(false),
+ mIsChecking(false),
+ mOriginUsage(0),
+ mGroupUsage(0),
+ mGroupLimit(0) {}
+
+void QuotaUsageChecker::Start() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (mIsChecking) {
+ return;
+ }
+ mIsChecking = true;
+
+ RefPtr<QuotaUsageChecker> self = this;
+ auto scopeExit = MakeScopeExit([self]() { self->RunCallback(false); });
+
+ RefPtr<QuotaManagerService> qms = QuotaManagerService::GetOrCreate();
+ MOZ_ASSERT(qms);
+
+ // Asynchronious getting quota usage for principal
+ nsCOMPtr<nsIQuotaUsageRequest> usageRequest;
+ if (NS_WARN_IF(NS_FAILED(qms->GetUsageForPrincipal(
+ mPrincipal, this, false, getter_AddRefs(usageRequest))))) {
+ return;
+ }
+
+ // Asynchronious getting group usage and limit
+ nsCOMPtr<nsIQuotaRequest> request;
+ if (NS_WARN_IF(
+ NS_FAILED(qms->Estimate(mPrincipal, getter_AddRefs(request))))) {
+ return;
+ }
+ request->SetCallback(this);
+
+ scopeExit.release();
+}
+
+void QuotaUsageChecker::RunCallback(bool aResult) {
+ MOZ_ASSERT(mIsChecking && mCallback);
+ if (!mIsChecking) {
+ return;
+ }
+ mIsChecking = false;
+ mGettingOriginUsageDone = false;
+ mGettingGroupUsageDone = false;
+
+ mCallback(aResult);
+ mCallback = nullptr;
+}
+
+template <typename T, typename U>
+nsresult QuotaUsageChecker::GetResult(T* aRequest, U& aResult) {
+ nsCOMPtr<nsIVariant> result;
+ nsresult rv = aRequest->GetResult(getter_AddRefs(result));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ nsID* iid;
+ nsCOMPtr<nsISupports> supports;
+ rv = result->GetAsInterface(&iid, getter_AddRefs(supports));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ free(iid);
+
+ aResult = do_QueryInterface(supports);
+ return NS_OK;
+}
+
+void QuotaUsageChecker::CheckQuotaHeadroom() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ const uint64_t groupHeadroom =
+ static_cast<uint64_t>(
+ StaticPrefs::
+ dom_serviceWorkers_mitigations_group_usage_headroom_kb()) *
+ 1024;
+ const uint64_t groupAvailable = mGroupLimit - mGroupUsage;
+
+ // Group usage head room is satisfied, does not need the usage mitigation.
+ if (groupAvailable > groupHeadroom) {
+ RunCallback(true);
+ return;
+ }
+
+ RefPtr<QuotaUsageChecker> self = this;
+ auto scopeExit = MakeScopeExit([self]() { self->RunCallback(false); });
+
+ nsCOMPtr<nsIClearDataService> csd =
+ do_GetService("@mozilla.org/clear-data-service;1");
+ MOZ_ASSERT(csd);
+
+ // Group usage headroom is not satisfied even removing the origin usage,
+ // clear all group usage.
+ if ((groupAvailable + mOriginUsage) < groupHeadroom) {
+ nsAutoCString host;
+ nsresult rv = mPrincipal->GetHost(host);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+
+ rv = csd->DeleteDataFromBaseDomain(
+ host, false, nsIClearDataService::CLEAR_DOM_QUOTA, this);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+
+ // clear the origin usage since it makes group usage headroom be satisifed.
+ } else {
+ nsresult rv = csd->DeleteDataFromPrincipal(
+ mPrincipal, false, nsIClearDataService::CLEAR_DOM_QUOTA, this);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+ }
+
+ scopeExit.release();
+}
+
+// nsIQuotaUsageCallback implementation
+
+NS_IMETHODIMP QuotaUsageChecker::OnUsageResult(
+ nsIQuotaUsageRequest* aUsageRequest) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aUsageRequest);
+
+ RefPtr<QuotaUsageChecker> self = this;
+ auto scopeExit = MakeScopeExit([self]() { self->RunCallback(false); });
+
+ nsresult resultCode;
+ nsresult rv = aUsageRequest->GetResultCode(&resultCode);
+ if (NS_WARN_IF(NS_FAILED(rv) || NS_FAILED(resultCode))) {
+ return rv;
+ }
+
+ nsCOMPtr<nsIQuotaOriginUsageResult> usageResult;
+ rv = GetResult(aUsageRequest, usageResult);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ MOZ_ASSERT(usageResult);
+
+ rv = usageResult->GetUsage(&mOriginUsage);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ MOZ_ASSERT(!mGettingOriginUsageDone);
+ mGettingOriginUsageDone = true;
+
+ scopeExit.release();
+
+ // Call CheckQuotaHeadroom() when both
+ // QuotaManagerService::GetUsageForPrincipal() and
+ // QuotaManagerService::Estimate() are done.
+ if (mGettingOriginUsageDone && mGettingGroupUsageDone) {
+ CheckQuotaHeadroom();
+ }
+ return NS_OK;
+}
+
+// nsIQuotaCallback implementation
+
+NS_IMETHODIMP QuotaUsageChecker::OnComplete(nsIQuotaRequest* aRequest) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aRequest);
+
+ RefPtr<QuotaUsageChecker> self = this;
+ auto scopeExit = MakeScopeExit([self]() { self->RunCallback(false); });
+
+ nsresult resultCode;
+ nsresult rv = aRequest->GetResultCode(&resultCode);
+ if (NS_WARN_IF(NS_FAILED(rv) || NS_FAILED(resultCode))) {
+ return rv;
+ }
+
+ nsCOMPtr<nsIQuotaEstimateResult> estimateResult;
+ rv = GetResult(aRequest, estimateResult);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ MOZ_ASSERT(estimateResult);
+
+ rv = estimateResult->GetUsage(&mGroupUsage);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ rv = estimateResult->GetLimit(&mGroupLimit);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ MOZ_ASSERT(!mGettingGroupUsageDone);
+ mGettingGroupUsageDone = true;
+
+ scopeExit.release();
+
+ // Call CheckQuotaHeadroom() when both
+ // QuotaManagerService::GetUsageForPrincipal() and
+ // QuotaManagerService::Estimate() are done.
+ if (mGettingOriginUsageDone && mGettingGroupUsageDone) {
+ CheckQuotaHeadroom();
+ }
+ return NS_OK;
+}
+
+// nsIClearDataCallback implementation
+
+NS_IMETHODIMP QuotaUsageChecker::OnDataDeleted(uint32_t aFailedFlags) {
+ RunCallback(true);
+ return NS_OK;
+}
+
+// Helper methods in ServiceWorkerQuotaUtils.h
+
+void ClearQuotaUsageIfNeeded(nsIPrincipal* aPrincipal,
+ ServiceWorkerQuotaMitigationCallback&& aCallback) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aPrincipal);
+
+ RefPtr<QuotaUsageChecker> checker =
+ MakeRefPtr<QuotaUsageChecker>(aPrincipal, std::move(aCallback));
+ checker->Start();
+}
+
+} // namespace mozilla::dom
diff --git a/dom/serviceworkers/ServiceWorkerQuotaUtils.h b/dom/serviceworkers/ServiceWorkerQuotaUtils.h
new file mode 100644
index 0000000000..9f16f367ac
--- /dev/null
+++ b/dom/serviceworkers/ServiceWorkerQuotaUtils.h
@@ -0,0 +1,23 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#ifndef _mozilla_dom_ServiceWorkerQuotaUtils_h
+#define _mozilla_dom_ServiceWorkerQuotaUtils_h
+
+#include <functional>
+
+class nsIPrincipal;
+class nsIQuotaUsageRequest;
+
+namespace mozilla::dom {
+
+using ServiceWorkerQuotaMitigationCallback = std::function<void(bool)>;
+
+void ClearQuotaUsageIfNeeded(nsIPrincipal* aPrincipal,
+ ServiceWorkerQuotaMitigationCallback&& aCallback);
+
+} // namespace mozilla::dom
+
+#endif
diff --git a/dom/serviceworkers/ServiceWorkerRegisterJob.cpp b/dom/serviceworkers/ServiceWorkerRegisterJob.cpp
new file mode 100644
index 0000000000..9d5f6db098
--- /dev/null
+++ b/dom/serviceworkers/ServiceWorkerRegisterJob.cpp
@@ -0,0 +1,57 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "ServiceWorkerRegisterJob.h"
+
+#include "mozilla/dom/WorkerCommon.h"
+#include "ServiceWorkerManager.h"
+
+namespace mozilla::dom {
+
+ServiceWorkerRegisterJob::ServiceWorkerRegisterJob(
+ nsIPrincipal* aPrincipal, const nsACString& aScope,
+ const nsACString& aScriptSpec, ServiceWorkerUpdateViaCache aUpdateViaCache)
+ : ServiceWorkerUpdateJob(Type::Register, aPrincipal, aScope,
+ nsCString(aScriptSpec), aUpdateViaCache) {}
+
+void ServiceWorkerRegisterJob::AsyncExecute() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance();
+ if (Canceled() || !swm) {
+ FailUpdateJob(NS_ERROR_DOM_ABORT_ERR);
+ return;
+ }
+
+ RefPtr<ServiceWorkerRegistrationInfo> registration =
+ swm->GetRegistration(mPrincipal, mScope);
+
+ if (registration) {
+ bool sameUVC = GetUpdateViaCache() == registration->GetUpdateViaCache();
+ registration->SetUpdateViaCache(GetUpdateViaCache());
+
+ RefPtr<ServiceWorkerInfo> newest = registration->Newest();
+ if (newest && mScriptSpec.Equals(newest->ScriptSpec()) && sameUVC) {
+ SetRegistration(registration);
+ Finish(NS_OK);
+ return;
+ }
+ } else {
+ registration =
+ swm->CreateNewRegistration(mScope, mPrincipal, GetUpdateViaCache());
+ if (!registration) {
+ FailUpdateJob(NS_ERROR_DOM_ABORT_ERR);
+ return;
+ }
+ }
+
+ SetRegistration(registration);
+ Update();
+}
+
+ServiceWorkerRegisterJob::~ServiceWorkerRegisterJob() = default;
+
+} // namespace mozilla::dom
diff --git a/dom/serviceworkers/ServiceWorkerRegisterJob.h b/dom/serviceworkers/ServiceWorkerRegisterJob.h
new file mode 100644
index 0000000000..ab4259e606
--- /dev/null
+++ b/dom/serviceworkers/ServiceWorkerRegisterJob.h
@@ -0,0 +1,33 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_serviceworkerregisterjob_h
+#define mozilla_dom_serviceworkerregisterjob_h
+
+#include "ServiceWorkerUpdateJob.h"
+
+namespace mozilla::dom {
+
+// The register job. This implements the steps in the spec Register algorithm,
+// but then uses ServiceWorkerUpdateJob to implement the Update and Install
+// spec algorithms.
+class ServiceWorkerRegisterJob final : public ServiceWorkerUpdateJob {
+ public:
+ ServiceWorkerRegisterJob(nsIPrincipal* aPrincipal, const nsACString& aScope,
+ const nsACString& aScriptSpec,
+ ServiceWorkerUpdateViaCache aUpdateViaCache);
+
+ private:
+ // Implement the Register algorithm steps and then call the parent class
+ // Update() to complete the job execution.
+ virtual void AsyncExecute() override;
+
+ virtual ~ServiceWorkerRegisterJob();
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_serviceworkerregisterjob_h
diff --git a/dom/serviceworkers/ServiceWorkerRegistrar.cpp b/dom/serviceworkers/ServiceWorkerRegistrar.cpp
new file mode 100644
index 0000000000..cc2b490c5c
--- /dev/null
+++ b/dom/serviceworkers/ServiceWorkerRegistrar.cpp
@@ -0,0 +1,1458 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "ServiceWorkerRegistrar.h"
+#include "ServiceWorkerManager.h"
+#include "mozilla/dom/ServiceWorkerRegistrarTypes.h"
+#include "mozilla/dom/DOMException.h"
+#include "mozilla/net/MozURL.h"
+#include "mozilla/StaticPrefs_dom.h"
+
+#include "nsIEventTarget.h"
+#include "nsIInputStream.h"
+#include "nsILineInputStream.h"
+#include "nsIObserverService.h"
+#include "nsIOutputStream.h"
+#include "nsISafeOutputStream.h"
+#include "nsIServiceWorkerManager.h"
+#include "nsIWritablePropertyBag2.h"
+
+#include "MainThreadUtils.h"
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/CycleCollectedJSContext.h"
+#include "mozilla/dom/StorageActivityService.h"
+#include "mozilla/ErrorNames.h"
+#include "mozilla/ipc/BackgroundChild.h"
+#include "mozilla/ipc/BackgroundParent.h"
+#include "mozilla/ipc/PBackgroundChild.h"
+#include "mozilla/ModuleUtils.h"
+#include "mozilla/Result.h"
+#include "mozilla/ResultExtensions.h"
+#include "mozilla/Services.h"
+#include "mozilla/StaticPtr.h"
+#include "nsAppDirectoryServiceDefs.h"
+#include "nsComponentManagerUtils.h"
+#include "nsContentUtils.h"
+#include "nsDirectoryServiceUtils.h"
+#include "nsNetCID.h"
+#include "nsNetUtil.h"
+#include "nsServiceManagerUtils.h"
+#include "nsThreadUtils.h"
+#include "nsXULAppAPI.h"
+#include "ServiceWorkerUtils.h"
+
+using namespace mozilla::ipc;
+
+extern mozilla::LazyLogModule sWorkerTelemetryLog;
+
+#ifdef LOG
+# undef LOG
+#endif
+#define LOG(_args) MOZ_LOG(sWorkerTelemetryLog, LogLevel::Debug, _args);
+
+namespace mozilla::dom {
+
+namespace {
+
+static const char* gSupportedRegistrarVersions[] = {
+ SERVICEWORKERREGISTRAR_VERSION, "8", "7", "6", "5", "4", "3", "2"};
+
+static const uint32_t kInvalidGeneration = static_cast<uint32_t>(-1);
+
+StaticRefPtr<ServiceWorkerRegistrar> gServiceWorkerRegistrar;
+
+nsresult GetOriginAndBaseDomain(const nsACString& aURL, nsACString& aOrigin,
+ nsACString& aBaseDomain) {
+ RefPtr<net::MozURL> url;
+ nsresult rv = net::MozURL::Init(getter_AddRefs(url), aURL);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ url->Origin(aOrigin);
+
+ rv = url->BaseDomain(aBaseDomain);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ return NS_OK;
+}
+
+nsresult ReadLine(nsILineInputStream* aStream, nsACString& aValue) {
+ bool hasMoreLines;
+ nsresult rv = aStream->ReadLine(aValue, &hasMoreLines);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ if (NS_WARN_IF(!hasMoreLines)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ return NS_OK;
+}
+
+nsresult CreatePrincipalInfo(nsILineInputStream* aStream,
+ ServiceWorkerRegistrationData* aEntry,
+ bool aSkipSpec = false) {
+ nsAutoCString suffix;
+ nsresult rv = ReadLine(aStream, suffix);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ OriginAttributes attrs;
+ if (!attrs.PopulateFromSuffix(suffix)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ if (aSkipSpec) {
+ nsAutoCString unused;
+ nsresult rv = ReadLine(aStream, unused);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ }
+
+ rv = ReadLine(aStream, aEntry->scope());
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ nsCString origin;
+ nsCString baseDomain;
+ rv = GetOriginAndBaseDomain(aEntry->scope(), origin, baseDomain);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ aEntry->principal() = mozilla::ipc::ContentPrincipalInfo(
+ attrs, origin, aEntry->scope(), Nothing(), baseDomain);
+
+ return NS_OK;
+}
+
+const IPCNavigationPreloadState gDefaultNavigationPreloadState(false,
+ "true"_ns);
+
+} // namespace
+
+NS_IMPL_ISUPPORTS(ServiceWorkerRegistrar, nsIObserver, nsIAsyncShutdownBlocker)
+
+void ServiceWorkerRegistrar::Initialize() {
+ MOZ_ASSERT(!gServiceWorkerRegistrar);
+
+ if (!XRE_IsParentProcess()) {
+ return;
+ }
+
+ gServiceWorkerRegistrar = new ServiceWorkerRegistrar();
+ ClearOnShutdown(&gServiceWorkerRegistrar);
+
+ nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
+ if (obs) {
+ DebugOnly<nsresult> rv = obs->AddObserver(gServiceWorkerRegistrar,
+ "profile-after-change", false);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+ }
+}
+
+/* static */
+already_AddRefed<ServiceWorkerRegistrar> ServiceWorkerRegistrar::Get() {
+ MOZ_ASSERT(XRE_IsParentProcess());
+
+ MOZ_ASSERT(gServiceWorkerRegistrar);
+ RefPtr<ServiceWorkerRegistrar> service = gServiceWorkerRegistrar.get();
+ return service.forget();
+}
+
+ServiceWorkerRegistrar::ServiceWorkerRegistrar()
+ : mMonitor("ServiceWorkerRegistrar.mMonitor"),
+ mDataLoaded(false),
+ mDataGeneration(kInvalidGeneration),
+ mFileGeneration(kInvalidGeneration),
+ mRetryCount(0),
+ mShuttingDown(false),
+ mSaveDataRunnableDispatched(false) {
+ MOZ_ASSERT(NS_IsMainThread());
+}
+
+ServiceWorkerRegistrar::~ServiceWorkerRegistrar() {
+ MOZ_ASSERT(!mSaveDataRunnableDispatched);
+}
+
+void ServiceWorkerRegistrar::GetRegistrations(
+ nsTArray<ServiceWorkerRegistrationData>& aValues) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aValues.IsEmpty());
+
+ MonitorAutoLock lock(mMonitor);
+
+ // If we don't have the profile directory, profile is not started yet (and
+ // probably we are in a utest).
+ if (!mProfileDir) {
+ return;
+ }
+
+ // We care just about the first execution because this can be blocked by
+ // loading data from disk.
+ static bool firstTime = true;
+ TimeStamp startTime;
+
+ if (firstTime) {
+ startTime = TimeStamp::NowLoRes();
+ }
+
+ // Waiting for data loaded.
+ mMonitor.AssertCurrentThreadOwns();
+ while (!mDataLoaded) {
+ mMonitor.Wait();
+ }
+
+ aValues.AppendElements(mData);
+
+ MaybeResetGeneration();
+ MOZ_DIAGNOSTIC_ASSERT(mDataGeneration != kInvalidGeneration);
+ MOZ_DIAGNOSTIC_ASSERT(mFileGeneration != kInvalidGeneration);
+
+ if (firstTime) {
+ firstTime = false;
+ Telemetry::AccumulateTimeDelta(
+ Telemetry::SERVICE_WORKER_REGISTRATION_LOADING, startTime);
+ }
+}
+
+namespace {
+
+bool Equivalent(const ServiceWorkerRegistrationData& aLeft,
+ const ServiceWorkerRegistrationData& aRight) {
+ MOZ_ASSERT(aLeft.principal().type() ==
+ mozilla::ipc::PrincipalInfo::TContentPrincipalInfo);
+ MOZ_ASSERT(aRight.principal().type() ==
+ mozilla::ipc::PrincipalInfo::TContentPrincipalInfo);
+
+ const auto& leftPrincipal = aLeft.principal().get_ContentPrincipalInfo();
+ const auto& rightPrincipal = aRight.principal().get_ContentPrincipalInfo();
+
+ // Only compare the attributes, not the spec part of the principal.
+ // The scope comparison above already covers the origin and codebase
+ // principals include the full path in their spec which is not what
+ // we want here.
+ return aLeft.scope() == aRight.scope() &&
+ leftPrincipal.attrs() == rightPrincipal.attrs();
+}
+
+} // anonymous namespace
+
+void ServiceWorkerRegistrar::RegisterServiceWorker(
+ const ServiceWorkerRegistrationData& aData) {
+ AssertIsOnBackgroundThread();
+
+ if (mShuttingDown) {
+ NS_WARNING("Failed to register a serviceWorker during shutting down.");
+ return;
+ }
+
+ {
+ MonitorAutoLock lock(mMonitor);
+ MOZ_ASSERT(mDataLoaded);
+ RegisterServiceWorkerInternal(aData);
+ }
+
+ MaybeScheduleSaveData();
+ StorageActivityService::SendActivity(aData.principal());
+}
+
+void ServiceWorkerRegistrar::UnregisterServiceWorker(
+ const PrincipalInfo& aPrincipalInfo, const nsACString& aScope) {
+ AssertIsOnBackgroundThread();
+
+ if (mShuttingDown) {
+ NS_WARNING("Failed to unregister a serviceWorker during shutting down.");
+ return;
+ }
+
+ bool deleted = false;
+
+ {
+ MonitorAutoLock lock(mMonitor);
+ MOZ_ASSERT(mDataLoaded);
+
+ ServiceWorkerRegistrationData tmp;
+ tmp.principal() = aPrincipalInfo;
+ tmp.scope() = aScope;
+
+ for (uint32_t i = 0; i < mData.Length(); ++i) {
+ if (Equivalent(tmp, mData[i])) {
+ gServiceWorkersRegistered--;
+ if (mData[i].currentWorkerHandlesFetch()) {
+ gServiceWorkersRegisteredFetch--;
+ }
+ // Update Telemetry
+ Telemetry::ScalarSet(Telemetry::ScalarID::SERVICEWORKER_REGISTRATIONS,
+ u"All"_ns, gServiceWorkersRegistered);
+ Telemetry::ScalarSet(Telemetry::ScalarID::SERVICEWORKER_REGISTRATIONS,
+ u"Fetch"_ns, gServiceWorkersRegisteredFetch);
+ LOG(("Unregister ServiceWorker: %u, fetch %u\n",
+ gServiceWorkersRegistered, gServiceWorkersRegisteredFetch));
+
+ mData.RemoveElementAt(i);
+ mDataGeneration = GetNextGeneration();
+ deleted = true;
+ break;
+ }
+ }
+ }
+
+ if (deleted) {
+ MaybeScheduleSaveData();
+ StorageActivityService::SendActivity(aPrincipalInfo);
+ }
+}
+
+void ServiceWorkerRegistrar::RemoveAll() {
+ AssertIsOnBackgroundThread();
+
+ if (mShuttingDown) {
+ NS_WARNING("Failed to remove all the serviceWorkers during shutting down.");
+ return;
+ }
+
+ bool deleted = false;
+
+ nsTArray<ServiceWorkerRegistrationData> data;
+ {
+ MonitorAutoLock lock(mMonitor);
+ MOZ_ASSERT(mDataLoaded);
+
+ // Let's take a copy in order to inform StorageActivityService.
+ data = mData.Clone();
+
+ deleted = !mData.IsEmpty();
+ mData.Clear();
+
+ mDataGeneration = GetNextGeneration();
+ }
+
+ if (!deleted) {
+ return;
+ }
+
+ MaybeScheduleSaveData();
+
+ for (uint32_t i = 0, len = data.Length(); i < len; ++i) {
+ StorageActivityService::SendActivity(data[i].principal());
+ }
+}
+
+void ServiceWorkerRegistrar::LoadData() {
+ MOZ_ASSERT(!NS_IsMainThread());
+#ifdef DEBUG
+ {
+ MonitorAutoLock lock(mMonitor);
+ MOZ_ASSERT(!mDataLoaded);
+ }
+#endif
+
+ nsresult rv = ReadData();
+
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ DeleteData();
+ // Also if the reading failed we have to notify what is waiting for data.
+ }
+
+ MonitorAutoLock lock(mMonitor);
+ MOZ_ASSERT(!mDataLoaded);
+ mDataLoaded = true;
+ mMonitor.Notify();
+}
+
+bool ServiceWorkerRegistrar::ReloadDataForTest() {
+ if (NS_WARN_IF(!StaticPrefs::dom_serviceWorkers_testing_enabled())) {
+ return false;
+ }
+
+ MOZ_ASSERT(NS_IsMainThread());
+ MonitorAutoLock lock(mMonitor);
+ mData.Clear();
+ mDataLoaded = false;
+
+ nsCOMPtr<nsIEventTarget> target =
+ do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID);
+ MOZ_ASSERT(target, "Must have stream transport service");
+
+ nsCOMPtr<nsIRunnable> runnable =
+ NewRunnableMethod("dom::ServiceWorkerRegistrar::LoadData", this,
+ &ServiceWorkerRegistrar::LoadData);
+ nsresult rv = target->Dispatch(runnable.forget(), NS_DISPATCH_NORMAL);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("Failed to dispatch the LoadDataRunnable.");
+ return false;
+ }
+
+ mMonitor.AssertCurrentThreadOwns();
+ while (!mDataLoaded) {
+ mMonitor.Wait();
+ }
+
+ return mDataLoaded;
+}
+
+nsresult ServiceWorkerRegistrar::ReadData() {
+ // We cannot assert about the correct thread because normally this method
+ // runs on a IO thread, but in gTests we call it from the main-thread.
+
+ nsCOMPtr<nsIFile> file;
+
+ {
+ MonitorAutoLock lock(mMonitor);
+
+ if (!mProfileDir) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsresult rv = mProfileDir->Clone(getter_AddRefs(file));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ }
+
+ nsresult rv = file->Append(nsLiteralString(SERVICEWORKERREGISTRAR_FILE));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ bool exists;
+ rv = file->Exists(&exists);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ if (!exists) {
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIInputStream> stream;
+ rv = NS_NewLocalFileInputStream(getter_AddRefs(stream), file);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ nsCOMPtr<nsILineInputStream> lineInputStream = do_QueryInterface(stream);
+ MOZ_ASSERT(lineInputStream);
+
+ nsAutoCString version;
+ bool hasMoreLines;
+ rv = lineInputStream->ReadLine(version, &hasMoreLines);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ if (!IsSupportedVersion(version)) {
+ nsContentUtils::LogMessageToConsole(
+ nsPrintfCString("Unsupported service worker registrar version: %s",
+ version.get())
+ .get());
+ return NS_ERROR_FAILURE;
+ }
+
+ nsTArray<ServiceWorkerRegistrationData> tmpData;
+
+ bool overwrite = false;
+ bool dedupe = false;
+ while (hasMoreLines) {
+ ServiceWorkerRegistrationData* entry = tmpData.AppendElement();
+
+#define GET_LINE(x) \
+ rv = lineInputStream->ReadLine(x, &hasMoreLines); \
+ if (NS_WARN_IF(NS_FAILED(rv))) { \
+ return rv; \
+ } \
+ if (NS_WARN_IF(!hasMoreLines)) { \
+ return NS_ERROR_FAILURE; \
+ }
+
+ nsAutoCString line;
+ if (version.EqualsLiteral(SERVICEWORKERREGISTRAR_VERSION)) {
+ rv = CreatePrincipalInfo(lineInputStream, entry);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ GET_LINE(entry->currentWorkerURL());
+
+ nsAutoCString fetchFlag;
+ GET_LINE(fetchFlag);
+ if (!fetchFlag.EqualsLiteral(SERVICEWORKERREGISTRAR_TRUE) &&
+ !fetchFlag.EqualsLiteral(SERVICEWORKERREGISTRAR_FALSE)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+ entry->currentWorkerHandlesFetch() =
+ fetchFlag.EqualsLiteral(SERVICEWORKERREGISTRAR_TRUE);
+
+ nsAutoCString cacheName;
+ GET_LINE(cacheName);
+ CopyUTF8toUTF16(cacheName, entry->cacheName());
+
+ nsAutoCString updateViaCache;
+ GET_LINE(updateViaCache);
+ entry->updateViaCache() = updateViaCache.ToInteger(&rv, 16);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ if (entry->updateViaCache() >
+ nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_NONE) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ nsAutoCString installedTimeStr;
+ GET_LINE(installedTimeStr);
+ int64_t installedTime = installedTimeStr.ToInteger64(&rv);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ entry->currentWorkerInstalledTime() = installedTime;
+
+ nsAutoCString activatedTimeStr;
+ GET_LINE(activatedTimeStr);
+ int64_t activatedTime = activatedTimeStr.ToInteger64(&rv);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ entry->currentWorkerActivatedTime() = activatedTime;
+
+ nsAutoCString lastUpdateTimeStr;
+ GET_LINE(lastUpdateTimeStr);
+ int64_t lastUpdateTime = lastUpdateTimeStr.ToInteger64(&rv);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ entry->lastUpdateTime() = lastUpdateTime;
+
+ nsAutoCString navigationPreloadEnabledStr;
+ GET_LINE(navigationPreloadEnabledStr);
+ bool navigationPreloadEnabled =
+ navigationPreloadEnabledStr.ToInteger(&rv);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ entry->navigationPreloadState().enabled() = navigationPreloadEnabled;
+
+ GET_LINE(entry->navigationPreloadState().headerValue());
+ } else if (version.EqualsLiteral("8")) {
+ rv = CreatePrincipalInfo(lineInputStream, entry);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ GET_LINE(entry->currentWorkerURL());
+
+ nsAutoCString fetchFlag;
+ GET_LINE(fetchFlag);
+ if (!fetchFlag.EqualsLiteral(SERVICEWORKERREGISTRAR_TRUE) &&
+ !fetchFlag.EqualsLiteral(SERVICEWORKERREGISTRAR_FALSE)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+ entry->currentWorkerHandlesFetch() =
+ fetchFlag.EqualsLiteral(SERVICEWORKERREGISTRAR_TRUE);
+
+ nsAutoCString cacheName;
+ GET_LINE(cacheName);
+ CopyUTF8toUTF16(cacheName, entry->cacheName());
+
+ nsAutoCString updateViaCache;
+ GET_LINE(updateViaCache);
+ entry->updateViaCache() = updateViaCache.ToInteger(&rv, 16);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ if (entry->updateViaCache() >
+ nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_NONE) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ nsAutoCString installedTimeStr;
+ GET_LINE(installedTimeStr);
+ int64_t installedTime = installedTimeStr.ToInteger64(&rv);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ entry->currentWorkerInstalledTime() = installedTime;
+
+ nsAutoCString activatedTimeStr;
+ GET_LINE(activatedTimeStr);
+ int64_t activatedTime = activatedTimeStr.ToInteger64(&rv);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ entry->currentWorkerActivatedTime() = activatedTime;
+
+ nsAutoCString lastUpdateTimeStr;
+ GET_LINE(lastUpdateTimeStr);
+ int64_t lastUpdateTime = lastUpdateTimeStr.ToInteger64(&rv);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ entry->lastUpdateTime() = lastUpdateTime;
+
+ entry->navigationPreloadState() = gDefaultNavigationPreloadState;
+ } else if (version.EqualsLiteral("7")) {
+ rv = CreatePrincipalInfo(lineInputStream, entry);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ GET_LINE(entry->currentWorkerURL());
+
+ nsAutoCString fetchFlag;
+ GET_LINE(fetchFlag);
+ if (!fetchFlag.EqualsLiteral(SERVICEWORKERREGISTRAR_TRUE) &&
+ !fetchFlag.EqualsLiteral(SERVICEWORKERREGISTRAR_FALSE)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+ entry->currentWorkerHandlesFetch() =
+ fetchFlag.EqualsLiteral(SERVICEWORKERREGISTRAR_TRUE);
+
+ nsAutoCString cacheName;
+ GET_LINE(cacheName);
+ CopyUTF8toUTF16(cacheName, entry->cacheName());
+
+ nsAutoCString loadFlags;
+ GET_LINE(loadFlags);
+ entry->updateViaCache() =
+ loadFlags.ToInteger(&rv, 16) == nsIRequest::LOAD_NORMAL
+ ? nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_ALL
+ : nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_IMPORTS;
+
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ nsAutoCString installedTimeStr;
+ GET_LINE(installedTimeStr);
+ int64_t installedTime = installedTimeStr.ToInteger64(&rv);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ entry->currentWorkerInstalledTime() = installedTime;
+
+ nsAutoCString activatedTimeStr;
+ GET_LINE(activatedTimeStr);
+ int64_t activatedTime = activatedTimeStr.ToInteger64(&rv);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ entry->currentWorkerActivatedTime() = activatedTime;
+
+ nsAutoCString lastUpdateTimeStr;
+ GET_LINE(lastUpdateTimeStr);
+ int64_t lastUpdateTime = lastUpdateTimeStr.ToInteger64(&rv);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ entry->lastUpdateTime() = lastUpdateTime;
+
+ entry->navigationPreloadState() = gDefaultNavigationPreloadState;
+ } else if (version.EqualsLiteral("6")) {
+ rv = CreatePrincipalInfo(lineInputStream, entry);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ GET_LINE(entry->currentWorkerURL());
+
+ nsAutoCString fetchFlag;
+ GET_LINE(fetchFlag);
+ if (!fetchFlag.EqualsLiteral(SERVICEWORKERREGISTRAR_TRUE) &&
+ !fetchFlag.EqualsLiteral(SERVICEWORKERREGISTRAR_FALSE)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+ entry->currentWorkerHandlesFetch() =
+ fetchFlag.EqualsLiteral(SERVICEWORKERREGISTRAR_TRUE);
+
+ nsAutoCString cacheName;
+ GET_LINE(cacheName);
+ CopyUTF8toUTF16(cacheName, entry->cacheName());
+
+ nsAutoCString loadFlags;
+ GET_LINE(loadFlags);
+ entry->updateViaCache() =
+ loadFlags.ToInteger(&rv, 16) == nsIRequest::LOAD_NORMAL
+ ? nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_ALL
+ : nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_IMPORTS;
+
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ entry->currentWorkerInstalledTime() = 0;
+ entry->currentWorkerActivatedTime() = 0;
+ entry->lastUpdateTime() = 0;
+
+ entry->navigationPreloadState() = gDefaultNavigationPreloadState;
+ } else if (version.EqualsLiteral("5")) {
+ overwrite = true;
+ dedupe = true;
+
+ rv = CreatePrincipalInfo(lineInputStream, entry);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ GET_LINE(entry->currentWorkerURL());
+
+ nsAutoCString fetchFlag;
+ GET_LINE(fetchFlag);
+ if (!fetchFlag.EqualsLiteral(SERVICEWORKERREGISTRAR_TRUE) &&
+ !fetchFlag.EqualsLiteral(SERVICEWORKERREGISTRAR_FALSE)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+ entry->currentWorkerHandlesFetch() =
+ fetchFlag.EqualsLiteral(SERVICEWORKERREGISTRAR_TRUE);
+
+ nsAutoCString cacheName;
+ GET_LINE(cacheName);
+ CopyUTF8toUTF16(cacheName, entry->cacheName());
+
+ entry->updateViaCache() =
+ nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_IMPORTS;
+
+ entry->currentWorkerInstalledTime() = 0;
+ entry->currentWorkerActivatedTime() = 0;
+ entry->lastUpdateTime() = 0;
+
+ entry->navigationPreloadState() = gDefaultNavigationPreloadState;
+ } else if (version.EqualsLiteral("4")) {
+ overwrite = true;
+ dedupe = true;
+
+ rv = CreatePrincipalInfo(lineInputStream, entry);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ GET_LINE(entry->currentWorkerURL());
+
+ // default handlesFetch flag to Enabled
+ entry->currentWorkerHandlesFetch() = true;
+
+ nsAutoCString cacheName;
+ GET_LINE(cacheName);
+ CopyUTF8toUTF16(cacheName, entry->cacheName());
+
+ entry->updateViaCache() =
+ nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_IMPORTS;
+
+ entry->currentWorkerInstalledTime() = 0;
+ entry->currentWorkerActivatedTime() = 0;
+ entry->lastUpdateTime() = 0;
+
+ entry->navigationPreloadState() = gDefaultNavigationPreloadState;
+ } else if (version.EqualsLiteral("3")) {
+ overwrite = true;
+ dedupe = true;
+
+ rv = CreatePrincipalInfo(lineInputStream, entry, true);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ GET_LINE(entry->currentWorkerURL());
+
+ // default handlesFetch flag to Enabled
+ entry->currentWorkerHandlesFetch() = true;
+
+ nsAutoCString cacheName;
+ GET_LINE(cacheName);
+ CopyUTF8toUTF16(cacheName, entry->cacheName());
+
+ entry->updateViaCache() =
+ nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_IMPORTS;
+
+ entry->currentWorkerInstalledTime() = 0;
+ entry->currentWorkerActivatedTime() = 0;
+ entry->lastUpdateTime() = 0;
+
+ entry->navigationPreloadState() = gDefaultNavigationPreloadState;
+ } else if (version.EqualsLiteral("2")) {
+ overwrite = true;
+ dedupe = true;
+
+ rv = CreatePrincipalInfo(lineInputStream, entry, true);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ // scriptSpec is no more used in latest version.
+ nsAutoCString unused;
+ GET_LINE(unused);
+
+ GET_LINE(entry->currentWorkerURL());
+
+ // default handlesFetch flag to Enabled
+ entry->currentWorkerHandlesFetch() = true;
+
+ nsAutoCString cacheName;
+ GET_LINE(cacheName);
+ CopyUTF8toUTF16(cacheName, entry->cacheName());
+
+ // waitingCacheName is no more used in latest version.
+ GET_LINE(unused);
+
+ entry->updateViaCache() =
+ nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_IMPORTS;
+
+ entry->currentWorkerInstalledTime() = 0;
+ entry->currentWorkerActivatedTime() = 0;
+ entry->lastUpdateTime() = 0;
+
+ entry->navigationPreloadState() = gDefaultNavigationPreloadState;
+ } else {
+ MOZ_ASSERT_UNREACHABLE("Should never get here!");
+ }
+
+#undef GET_LINE
+
+ rv = lineInputStream->ReadLine(line, &hasMoreLines);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ if (!line.EqualsLiteral(SERVICEWORKERREGISTRAR_TERMINATOR)) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ stream->Close();
+
+ // We currently only call this at startup where we block the main thread
+ // preventing further operation until it completes, however take the lock
+ // in case that changes
+
+ {
+ MonitorAutoLock lock(mMonitor);
+ // Copy data over to mData.
+ for (uint32_t i = 0; i < tmpData.Length(); ++i) {
+ // Older versions could sometimes write out empty, useless entries.
+ // Prune those here.
+ if (!ServiceWorkerRegistrationDataIsValid(tmpData[i])) {
+ continue;
+ }
+
+ bool match = false;
+ if (dedupe) {
+ MOZ_ASSERT(overwrite);
+ // If this is an old profile, then we might need to deduplicate. In
+ // theory this can be removed in the future (Bug 1248449)
+ for (uint32_t j = 0; j < mData.Length(); ++j) {
+ // Use same comparison as RegisterServiceWorker. Scope contains
+ // basic origin information. Combine with any principal attributes.
+ if (Equivalent(tmpData[i], mData[j])) {
+ // Last match wins, just like legacy loading used to do in
+ // the ServiceWorkerManager.
+ mData[j] = tmpData[i];
+ // Dupe found, so overwrite file with reduced list.
+ match = true;
+ break;
+ }
+ }
+ } else {
+#ifdef DEBUG
+ // Otherwise assert no duplications in debug builds.
+ for (uint32_t j = 0; j < mData.Length(); ++j) {
+ MOZ_ASSERT(!Equivalent(tmpData[i], mData[j]));
+ }
+#endif
+ }
+ if (!match) {
+ mData.AppendElement(tmpData[i]);
+ }
+ }
+ }
+ // Overwrite previous version.
+ // Cannot call SaveData directly because gtest uses main-thread.
+
+ // XXX NOTE: if we could be accessed multi-threaded here, we would need to
+ // find a way to lock around access to mData. Since we can't, suppress the
+ // thread-safety warnings.
+ MOZ_PUSH_IGNORE_THREAD_SAFETY
+ if (overwrite && NS_FAILED(WriteData(mData))) {
+ NS_WARNING("Failed to write data for the ServiceWorker Registations.");
+ DeleteData();
+ }
+ MOZ_POP_THREAD_SAFETY
+
+ return NS_OK;
+}
+
+void ServiceWorkerRegistrar::DeleteData() {
+ // We cannot assert about the correct thread because normally this method
+ // runs on a IO thread, but in gTests we call it from the main-thread.
+
+ nsCOMPtr<nsIFile> file;
+
+ {
+ MonitorAutoLock lock(mMonitor);
+ mData.Clear();
+
+ if (!mProfileDir) {
+ return;
+ }
+
+ nsresult rv = mProfileDir->Clone(getter_AddRefs(file));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+ }
+
+ nsresult rv = file->Append(nsLiteralString(SERVICEWORKERREGISTRAR_FILE));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+
+ rv = file->Remove(false);
+ if (rv == NS_ERROR_FILE_NOT_FOUND) {
+ return;
+ }
+
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+}
+
+void ServiceWorkerRegistrar::RegisterServiceWorkerInternal(
+ const ServiceWorkerRegistrationData& aData) {
+ bool found = false;
+ for (uint32_t i = 0, len = mData.Length(); i < len; ++i) {
+ if (Equivalent(aData, mData[i])) {
+ found = true;
+ if (mData[i].currentWorkerHandlesFetch()) {
+ // Decrement here if we found it, in case the new registration no
+ // longer handles Fetch. If it continues to handle fetch, we'll
+ // bump it back later.
+ gServiceWorkersRegisteredFetch--;
+ }
+ mData[i] = aData;
+ break;
+ }
+ }
+
+ if (!found) {
+ MOZ_ASSERT(ServiceWorkerRegistrationDataIsValid(aData));
+ mData.AppendElement(aData);
+ // We didn't find an entry to update, so we have 1 more
+ gServiceWorkersRegistered++;
+ }
+ // Handles bumping both for new registrations and updates
+ if (aData.currentWorkerHandlesFetch()) {
+ gServiceWorkersRegisteredFetch++;
+ }
+ // Update Telemetry
+ Telemetry::ScalarSet(Telemetry::ScalarID::SERVICEWORKER_REGISTRATIONS,
+ u"All"_ns, gServiceWorkersRegistered);
+ Telemetry::ScalarSet(Telemetry::ScalarID::SERVICEWORKER_REGISTRATIONS,
+ u"Fetch"_ns, gServiceWorkersRegisteredFetch);
+ LOG(("Register: %u, fetch %u\n", gServiceWorkersRegistered,
+ gServiceWorkersRegisteredFetch));
+
+ mDataGeneration = GetNextGeneration();
+}
+
+class ServiceWorkerRegistrarSaveDataRunnable final : public Runnable {
+ nsCOMPtr<nsIEventTarget> mEventTarget;
+ const nsTArray<ServiceWorkerRegistrationData> mData;
+ const uint32_t mGeneration;
+
+ public:
+ ServiceWorkerRegistrarSaveDataRunnable(
+ nsTArray<ServiceWorkerRegistrationData>&& aData, uint32_t aGeneration)
+ : Runnable("dom::ServiceWorkerRegistrarSaveDataRunnable"),
+ mEventTarget(GetCurrentEventTarget()),
+ mData(std::move(aData)),
+ mGeneration(aGeneration) {
+ AssertIsOnBackgroundThread();
+ MOZ_DIAGNOSTIC_ASSERT(mGeneration != kInvalidGeneration);
+ }
+
+ NS_IMETHOD
+ Run() override {
+ RefPtr<ServiceWorkerRegistrar> service = ServiceWorkerRegistrar::Get();
+ MOZ_ASSERT(service);
+
+ uint32_t fileGeneration = kInvalidGeneration;
+
+ if (NS_SUCCEEDED(service->SaveData(mData))) {
+ fileGeneration = mGeneration;
+ }
+
+ RefPtr<Runnable> runnable = NewRunnableMethod<uint32_t>(
+ "ServiceWorkerRegistrar::DataSaved", service,
+ &ServiceWorkerRegistrar::DataSaved, fileGeneration);
+ MOZ_ALWAYS_SUCCEEDS(
+ mEventTarget->Dispatch(runnable.forget(), NS_DISPATCH_NORMAL));
+
+ return NS_OK;
+ }
+};
+
+void ServiceWorkerRegistrar::MaybeScheduleSaveData() {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(!mShuttingDown);
+
+ if (mShuttingDown || mSaveDataRunnableDispatched ||
+ mDataGeneration <= mFileGeneration) {
+ return;
+ }
+
+ nsCOMPtr<nsIEventTarget> target =
+ do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID);
+ MOZ_ASSERT(target, "Must have stream transport service");
+
+ uint32_t generation = kInvalidGeneration;
+ nsTArray<ServiceWorkerRegistrationData> data;
+
+ {
+ MonitorAutoLock lock(mMonitor);
+ generation = mDataGeneration;
+ data.AppendElements(mData);
+ }
+
+ RefPtr<Runnable> runnable =
+ new ServiceWorkerRegistrarSaveDataRunnable(std::move(data), generation);
+ nsresult rv = target->Dispatch(runnable.forget(), NS_DISPATCH_NORMAL);
+ NS_ENSURE_SUCCESS_VOID(rv);
+
+ mSaveDataRunnableDispatched = true;
+}
+
+void ServiceWorkerRegistrar::ShutdownCompleted() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ DebugOnly<nsresult> rv = GetShutdownPhase()->RemoveBlocker(this);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+}
+
+nsresult ServiceWorkerRegistrar::SaveData(
+ const nsTArray<ServiceWorkerRegistrationData>& aData) {
+ MOZ_ASSERT(!NS_IsMainThread());
+
+ nsresult rv = WriteData(aData);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("Failed to write data for the ServiceWorker Registations.");
+ // Don't touch the file or in-memory state. Writing files can
+ // sometimes fail due to virus scanning, etc. We should just leave
+ // things as is so the next save operation can pick up any changes
+ // without losing data.
+ }
+ return rv;
+}
+
+void ServiceWorkerRegistrar::DataSaved(uint32_t aFileGeneration) {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(mSaveDataRunnableDispatched);
+
+ mSaveDataRunnableDispatched = false;
+
+ // Check for shutdown before possibly triggering any more saves
+ // runnables.
+ MaybeScheduleShutdownCompleted();
+ if (mShuttingDown) {
+ return;
+ }
+
+ // If we got a valid generation, then the save was successful.
+ if (aFileGeneration != kInvalidGeneration) {
+ // Update the file generation. We also check to see if we
+ // can reset the generation back to zero if the file and data
+ // are now in sync. This allows us to avoid dealing with wrap
+ // around of the generation count.
+ mFileGeneration = aFileGeneration;
+ MaybeResetGeneration();
+
+ // Successful write resets the retry count.
+ mRetryCount = 0;
+
+ // Possibly schedule another save operation if more data
+ // has come in while processing this one.
+ MaybeScheduleSaveData();
+
+ return;
+ }
+
+ // Otherwise, the save failed since the generation is invalid. We
+ // want to retry the save, but only a limited number of times.
+ static const uint32_t kMaxRetryCount = 2;
+ if (mRetryCount >= kMaxRetryCount) {
+ return;
+ }
+
+ mRetryCount += 1;
+ MaybeScheduleSaveData();
+}
+
+void ServiceWorkerRegistrar::MaybeScheduleShutdownCompleted() {
+ AssertIsOnBackgroundThread();
+
+ if (mSaveDataRunnableDispatched || !mShuttingDown) {
+ return;
+ }
+
+ RefPtr<Runnable> runnable =
+ NewRunnableMethod("dom::ServiceWorkerRegistrar::ShutdownCompleted", this,
+ &ServiceWorkerRegistrar::ShutdownCompleted);
+ MOZ_ALWAYS_SUCCEEDS(NS_DispatchToMainThread(runnable.forget()));
+}
+
+uint32_t ServiceWorkerRegistrar::GetNextGeneration() {
+ uint32_t ret = mDataGeneration + 1;
+ if (ret == kInvalidGeneration) {
+ ret += 1;
+ }
+ return ret;
+}
+
+void ServiceWorkerRegistrar::MaybeResetGeneration() {
+ if (mDataGeneration != mFileGeneration) {
+ return;
+ }
+ mDataGeneration = mFileGeneration = 0;
+}
+
+bool ServiceWorkerRegistrar::IsSupportedVersion(
+ const nsACString& aVersion) const {
+ uint32_t numVersions = ArrayLength(gSupportedRegistrarVersions);
+ for (uint32_t i = 0; i < numVersions; i++) {
+ if (aVersion.EqualsASCII(gSupportedRegistrarVersions[i])) {
+ return true;
+ }
+ }
+ return false;
+}
+
+nsresult ServiceWorkerRegistrar::WriteData(
+ const nsTArray<ServiceWorkerRegistrationData>& aData) {
+ // We cannot assert about the correct thread because normally this method
+ // runs on a IO thread, but in gTests we call it from the main-thread.
+
+ nsCOMPtr<nsIFile> file;
+
+ {
+ MonitorAutoLock lock(mMonitor);
+
+ if (!mProfileDir) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsresult rv = mProfileDir->Clone(getter_AddRefs(file));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ }
+
+ nsresult rv = file->Append(nsLiteralString(SERVICEWORKERREGISTRAR_FILE));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ nsCOMPtr<nsIOutputStream> stream;
+ rv = NS_NewSafeLocalFileOutputStream(getter_AddRefs(stream), file);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ nsAutoCString buffer;
+ buffer.AppendLiteral(SERVICEWORKERREGISTRAR_VERSION);
+ buffer.Append('\n');
+
+ uint32_t count;
+ rv = stream->Write(buffer.Data(), buffer.Length(), &count);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ if (count != buffer.Length()) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ for (uint32_t i = 0, len = aData.Length(); i < len; ++i) {
+ // We have an assertion further up the stack, but as a last
+ // resort avoid writing out broken entries here.
+ if (!ServiceWorkerRegistrationDataIsValid(aData[i])) {
+ continue;
+ }
+
+ const mozilla::ipc::PrincipalInfo& info = aData[i].principal();
+
+ MOZ_ASSERT(info.type() ==
+ mozilla::ipc::PrincipalInfo::TContentPrincipalInfo);
+
+ const mozilla::ipc::ContentPrincipalInfo& cInfo =
+ info.get_ContentPrincipalInfo();
+
+ nsAutoCString suffix;
+ cInfo.attrs().CreateSuffix(suffix);
+
+ buffer.Truncate();
+ buffer.Append(suffix.get());
+ buffer.Append('\n');
+
+ buffer.Append(aData[i].scope());
+ buffer.Append('\n');
+
+ buffer.Append(aData[i].currentWorkerURL());
+ buffer.Append('\n');
+
+ buffer.Append(aData[i].currentWorkerHandlesFetch()
+ ? SERVICEWORKERREGISTRAR_TRUE
+ : SERVICEWORKERREGISTRAR_FALSE);
+ buffer.Append('\n');
+
+ buffer.Append(NS_ConvertUTF16toUTF8(aData[i].cacheName()));
+ buffer.Append('\n');
+
+ buffer.AppendInt(aData[i].updateViaCache(), 16);
+ buffer.Append('\n');
+ MOZ_DIAGNOSTIC_ASSERT(
+ aData[i].updateViaCache() ==
+ nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_IMPORTS ||
+ aData[i].updateViaCache() ==
+ nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_ALL ||
+ aData[i].updateViaCache() ==
+ nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_NONE);
+
+ static_assert(nsIRequest::LOAD_NORMAL == 0,
+ "LOAD_NORMAL matches serialized value.");
+ static_assert(nsIRequest::VALIDATE_ALWAYS == (1 << 11),
+ "VALIDATE_ALWAYS matches serialized value");
+
+ buffer.AppendInt(aData[i].currentWorkerInstalledTime());
+ buffer.Append('\n');
+
+ buffer.AppendInt(aData[i].currentWorkerActivatedTime());
+ buffer.Append('\n');
+
+ buffer.AppendInt(aData[i].lastUpdateTime());
+ buffer.Append('\n');
+
+ buffer.AppendInt(
+ static_cast<int32_t>(aData[i].navigationPreloadState().enabled()));
+ buffer.Append('\n');
+
+ buffer.Append(aData[i].navigationPreloadState().headerValue());
+ buffer.Append('\n');
+
+ buffer.AppendLiteral(SERVICEWORKERREGISTRAR_TERMINATOR);
+ buffer.Append('\n');
+
+ rv = stream->Write(buffer.Data(), buffer.Length(), &count);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ if (count != buffer.Length()) {
+ return NS_ERROR_UNEXPECTED;
+ }
+ }
+
+ nsCOMPtr<nsISafeOutputStream> safeStream = do_QueryInterface(stream);
+ MOZ_ASSERT(safeStream);
+
+ rv = safeStream->Finish();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ return NS_OK;
+}
+
+void ServiceWorkerRegistrar::ProfileStarted() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ MonitorAutoLock lock(mMonitor);
+ MOZ_DIAGNOSTIC_ASSERT(!mProfileDir);
+
+ nsresult rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR,
+ getter_AddRefs(mProfileDir));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+
+ nsAutoString blockerName;
+ MOZ_ALWAYS_SUCCEEDS(GetName(blockerName));
+
+ rv = GetShutdownPhase()->AddBlocker(
+ this, NS_LITERAL_STRING_FROM_CSTRING(__FILE__), __LINE__, blockerName);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+
+ nsCOMPtr<nsIEventTarget> target =
+ do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID);
+ MOZ_ASSERT(target, "Must have stream transport service");
+
+ nsCOMPtr<nsIRunnable> runnable =
+ NewRunnableMethod("dom::ServiceWorkerRegistrar::LoadData", this,
+ &ServiceWorkerRegistrar::LoadData);
+ rv = target->Dispatch(runnable.forget(), NS_DISPATCH_NORMAL);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("Failed to dispatch the LoadDataRunnable.");
+ }
+}
+
+void ServiceWorkerRegistrar::ProfileStopped() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ MonitorAutoLock lock(mMonitor);
+
+ if (!mProfileDir) {
+ nsresult rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR,
+ getter_AddRefs(mProfileDir));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ // If we do not have a profile directory, we are somehow screwed.
+ MOZ_DIAGNOSTIC_ASSERT(
+ false,
+ "NS_GetSpecialDirectory for NS_APP_USER_PROFILE_50_DIR failed!");
+ }
+ }
+
+ // Mutations to the ServiceWorkerRegistrar happen on the PBackground thread,
+ // issued by the ServiceWorkerManagerService, so the appropriate place to
+ // trigger shutdown is on that thread.
+ //
+ // However, it's quite possible that the PBackground thread was not brought
+ // into existence for xpcshell tests. We don't cause it to be created
+ // ourselves for any reason, for example.
+ //
+ // In this scenario, we know that:
+ // - We will receive exactly one call to ourself from BlockShutdown() and
+ // BlockShutdown() will be called (at most) once.
+ // - The only way our Shutdown() method gets called is via
+ // BackgroundParentImpl::RecvShutdownServiceWorkerRegistrar() being
+ // invoked, which only happens if we get to that send below here that we
+ // can't get to.
+ // - All Shutdown() does is set mShuttingDown=true (essential for
+ // invariants) and invoke MaybeScheduleShutdownCompleted().
+ // - Since there is no PBackground thread, mSaveDataRunnableDispatched must
+ // be false because only MaybeScheduleSaveData() set it and it only runs
+ // on the background thread, so it cannot have run. And so we would
+ // expect MaybeScheduleShutdownCompleted() to schedule an invocation of
+ // ShutdownCompleted on the main thread.
+ PBackgroundChild* child = BackgroundChild::GetForCurrentThread();
+ if (mProfileDir && child) {
+ if (child->SendShutdownServiceWorkerRegistrar()) {
+ // Normal shutdown sequence has been initiated, go home.
+ return;
+ }
+ // If we get here, the PBackground thread has probably gone nuts and we
+ // want to know it.
+ MOZ_DIAGNOSTIC_ASSERT(
+ false, "Unable to send the ShutdownServiceWorkerRegistrar message.");
+ }
+
+ // On any error it's appropriate to set mShuttingDown=true (as Shutdown
+ // would do) and directly invoke ShutdownCompleted() (as Shutdown would
+ // indirectly do via MaybeScheduleShutdownCompleted) in order to unblock
+ // shutdown.
+ mShuttingDown = true;
+ ShutdownCompleted();
+}
+
+// Async shutdown blocker methods
+
+NS_IMETHODIMP
+ServiceWorkerRegistrar::BlockShutdown(nsIAsyncShutdownClient* aClient) {
+ ProfileStopped();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+ServiceWorkerRegistrar::GetName(nsAString& aName) {
+ aName = u"ServiceWorkerRegistrar: Flushing data"_ns;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+ServiceWorkerRegistrar::GetState(nsIPropertyBag** aBagOut) {
+ nsCOMPtr<nsIWritablePropertyBag2> propertyBag =
+ do_CreateInstance("@mozilla.org/hash-property-bag;1");
+
+ MOZ_TRY(propertyBag->SetPropertyAsBool(u"shuttingDown"_ns, mShuttingDown));
+
+ MOZ_TRY(propertyBag->SetPropertyAsBool(u"saveDataRunnableDispatched"_ns,
+ mSaveDataRunnableDispatched));
+
+ propertyBag.forget(aBagOut);
+
+ return NS_OK;
+}
+
+#define RELEASE_ASSERT_SUCCEEDED(rv, name) \
+ do { \
+ if (NS_FAILED(rv)) { \
+ if (rv == NS_ERROR_XPC_JAVASCRIPT_ERROR_WITH_DETAILS) { \
+ if (auto* context = CycleCollectedJSContext::Get()) { \
+ if (RefPtr<Exception> exn = context->GetPendingException()) { \
+ MOZ_CRASH_UNSAFE_PRINTF("Failed to get " name ": %s", \
+ exn->GetMessageMoz().get()); \
+ } \
+ } \
+ } \
+ \
+ nsAutoCString errorName; \
+ GetErrorName(rv, errorName); \
+ MOZ_CRASH_UNSAFE_PRINTF("Failed to get " name ": %s", errorName.get()); \
+ } \
+ } while (0)
+
+nsCOMPtr<nsIAsyncShutdownClient> ServiceWorkerRegistrar::GetShutdownPhase()
+ const {
+ nsresult rv;
+ nsCOMPtr<nsIAsyncShutdownService> svc =
+ do_GetService("@mozilla.org/async-shutdown-service;1", &rv);
+ // If this fails, something is very wrong on the JS side (or we're out of
+ // memory), and there's no point in continuing startup. Include as much
+ // information as possible in the crash report.
+ RELEASE_ASSERT_SUCCEEDED(rv, "async shutdown service");
+
+ nsCOMPtr<nsIAsyncShutdownClient> client;
+ rv = svc->GetProfileBeforeChange(getter_AddRefs(client));
+ RELEASE_ASSERT_SUCCEEDED(rv, "profileBeforeChange shutdown blocker");
+ return client;
+}
+
+#undef RELEASE_ASSERT_SUCCEEDED
+
+void ServiceWorkerRegistrar::Shutdown() {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(!mShuttingDown);
+
+ mShuttingDown = true;
+ MaybeScheduleShutdownCompleted();
+}
+
+NS_IMETHODIMP
+ServiceWorkerRegistrar::Observe(nsISupports* aSubject, const char* aTopic,
+ const char16_t* aData) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (!strcmp(aTopic, "profile-after-change")) {
+ nsCOMPtr<nsIObserverService> observerService =
+ services::GetObserverService();
+ observerService->RemoveObserver(this, "profile-after-change");
+
+ // The profile is fully loaded, now we can proceed with the loading of data
+ // from disk.
+ ProfileStarted();
+
+ return NS_OK;
+ }
+
+ MOZ_ASSERT(false, "ServiceWorkerRegistrar got unexpected topic!");
+ return NS_ERROR_UNEXPECTED;
+}
+
+} // namespace mozilla::dom
diff --git a/dom/serviceworkers/ServiceWorkerRegistrar.h b/dom/serviceworkers/ServiceWorkerRegistrar.h
new file mode 100644
index 0000000000..20c1cc530c
--- /dev/null
+++ b/dom/serviceworkers/ServiceWorkerRegistrar.h
@@ -0,0 +1,119 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_ServiceWorkerRegistrar_h
+#define mozilla_dom_ServiceWorkerRegistrar_h
+
+#include "mozilla/Monitor.h"
+#include "mozilla/Telemetry.h"
+#include "nsClassHashtable.h"
+#include "nsIAsyncShutdown.h"
+#include "nsIObserver.h"
+#include "nsCOMPtr.h"
+#include "nsString.h"
+#include "nsTArray.h"
+
+#define SERVICEWORKERREGISTRAR_FILE u"serviceworker.txt"
+#define SERVICEWORKERREGISTRAR_VERSION "9"
+#define SERVICEWORKERREGISTRAR_TERMINATOR "#"
+#define SERVICEWORKERREGISTRAR_TRUE "true"
+#define SERVICEWORKERREGISTRAR_FALSE "false"
+
+class nsIFile;
+
+namespace mozilla {
+
+namespace ipc {
+class PrincipalInfo;
+} // namespace ipc
+
+namespace dom {
+
+class ServiceWorkerRegistrationData;
+}
+} // namespace mozilla
+
+namespace mozilla::dom {
+
+class ServiceWorkerRegistrar : public nsIObserver,
+ public nsIAsyncShutdownBlocker {
+ friend class ServiceWorkerRegistrarSaveDataRunnable;
+
+ public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIOBSERVER
+ NS_DECL_NSIASYNCSHUTDOWNBLOCKER
+
+ static void Initialize();
+
+ void Shutdown();
+
+ void DataSaved(uint32_t aFileGeneration);
+
+ static already_AddRefed<ServiceWorkerRegistrar> Get();
+
+ void GetRegistrations(nsTArray<ServiceWorkerRegistrationData>& aValues);
+
+ void RegisterServiceWorker(const ServiceWorkerRegistrationData& aData);
+ void UnregisterServiceWorker(
+ const mozilla::ipc::PrincipalInfo& aPrincipalInfo,
+ const nsACString& aScope);
+ void RemoveAll();
+
+ bool ReloadDataForTest();
+
+ protected:
+ // These methods are protected because we test this class using gTest
+ // subclassing it.
+ void LoadData();
+ nsresult SaveData(const nsTArray<ServiceWorkerRegistrationData>& aData);
+
+ nsresult ReadData();
+ nsresult WriteData(const nsTArray<ServiceWorkerRegistrationData>& aData);
+ void DeleteData();
+
+ void RegisterServiceWorkerInternal(const ServiceWorkerRegistrationData& aData)
+ MOZ_REQUIRES(mMonitor);
+
+ ServiceWorkerRegistrar();
+ virtual ~ServiceWorkerRegistrar();
+
+ private:
+ void ProfileStarted();
+ void ProfileStopped();
+
+ void MaybeScheduleSaveData();
+ void ShutdownCompleted();
+ void MaybeScheduleShutdownCompleted();
+
+ uint32_t GetNextGeneration();
+ void MaybeResetGeneration();
+
+ nsCOMPtr<nsIAsyncShutdownClient> GetShutdownPhase() const;
+
+ bool IsSupportedVersion(const nsACString& aVersion) const;
+
+ protected:
+ mozilla::Monitor mMonitor;
+
+ // protected by mMonitor.
+ nsCOMPtr<nsIFile> mProfileDir MOZ_GUARDED_BY(mMonitor);
+ // Read on mainthread, modified on background thread EXCEPT for
+ // ReloadDataForTest() AND for gtest, which modifies this on MainThread.
+ nsTArray<ServiceWorkerRegistrationData> mData MOZ_GUARDED_BY(mMonitor);
+ bool mDataLoaded MOZ_GUARDED_BY(mMonitor);
+
+ // PBackground thread only
+ uint32_t mDataGeneration;
+ uint32_t mFileGeneration;
+ uint32_t mRetryCount;
+ bool mShuttingDown;
+ bool mSaveDataRunnableDispatched;
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_ServiceWorkerRegistrar_h
diff --git a/dom/serviceworkers/ServiceWorkerRegistrarTypes.ipdlh b/dom/serviceworkers/ServiceWorkerRegistrarTypes.ipdlh
new file mode 100644
index 0000000000..d84b324797
--- /dev/null
+++ b/dom/serviceworkers/ServiceWorkerRegistrarTypes.ipdlh
@@ -0,0 +1,33 @@
+/* -*- Mode: C++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 8 -*- */
+/* vim: set sw=4 ts=8 et tw=80 ft=cpp : */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+include IPCNavigationPreloadState;
+include PBackgroundSharedTypes;
+
+namespace mozilla {
+namespace dom {
+
+struct ServiceWorkerRegistrationData
+{
+ nsCString scope;
+ nsCString currentWorkerURL;
+ bool currentWorkerHandlesFetch;
+
+ nsString cacheName;
+
+ PrincipalInfo principal;
+
+ uint16_t updateViaCache;
+
+ int64_t currentWorkerInstalledTime;
+ int64_t currentWorkerActivatedTime;
+ int64_t lastUpdateTime;
+
+ IPCNavigationPreloadState navigationPreloadState;
+};
+
+} // namespace dom
+} // namespace mozilla
diff --git a/dom/serviceworkers/ServiceWorkerRegistration.cpp b/dom/serviceworkers/ServiceWorkerRegistration.cpp
new file mode 100644
index 0000000000..9225400139
--- /dev/null
+++ b/dom/serviceworkers/ServiceWorkerRegistration.cpp
@@ -0,0 +1,688 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "ServiceWorkerRegistration.h"
+
+#include "mozilla/dom/DOMMozPromiseRequestHolder.h"
+#include "mozilla/dom/NavigationPreloadManager.h"
+#include "mozilla/dom/NavigationPreloadManagerBinding.h"
+#include "mozilla/dom/Notification.h"
+#include "mozilla/dom/Promise.h"
+#include "mozilla/dom/PushManager.h"
+#include "mozilla/ipc/PBackgroundSharedTypes.h"
+#include "mozilla/dom/ServiceWorker.h"
+#include "mozilla/dom/ServiceWorkerRegistrationBinding.h"
+#include "mozilla/dom/ServiceWorkerUtils.h"
+#include "mozilla/dom/WorkerPrivate.h"
+#include "mozilla/ipc/PBackgroundChild.h"
+#include "mozilla/ipc/BackgroundChild.h"
+#include "mozilla/ScopeExit.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsPIDOMWindow.h"
+#include "ServiceWorkerRegistrationChild.h"
+
+using mozilla::ipc::ResponseRejectReason;
+
+namespace mozilla::dom {
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(ServiceWorkerRegistration,
+ DOMEventTargetHelper, mInstallingWorker,
+ mWaitingWorker, mActiveWorker,
+ mNavigationPreloadManager, mPushManager);
+
+NS_IMPL_ADDREF_INHERITED(ServiceWorkerRegistration, DOMEventTargetHelper)
+NS_IMPL_RELEASE_INHERITED(ServiceWorkerRegistration, DOMEventTargetHelper)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ServiceWorkerRegistration)
+ NS_INTERFACE_MAP_ENTRY(ServiceWorkerRegistration)
+NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper)
+
+namespace {
+const uint64_t kInvalidUpdateFoundId = 0;
+} // anonymous namespace
+
+ServiceWorkerRegistration::ServiceWorkerRegistration(
+ nsIGlobalObject* aGlobal,
+ const ServiceWorkerRegistrationDescriptor& aDescriptor)
+ : DOMEventTargetHelper(aGlobal),
+ mDescriptor(aDescriptor),
+ mShutdown(false),
+ mScheduledUpdateFoundId(kInvalidUpdateFoundId),
+ mDispatchedUpdateFoundId(kInvalidUpdateFoundId) {
+ ::mozilla::ipc::PBackgroundChild* parentActor =
+ ::mozilla::ipc::BackgroundChild::GetOrCreateForCurrentThread();
+ if (NS_WARN_IF(!parentActor)) {
+ Shutdown();
+ return;
+ }
+
+ auto actor = ServiceWorkerRegistrationChild::Create();
+ if (NS_WARN_IF(!actor)) {
+ Shutdown();
+ return;
+ }
+
+ PServiceWorkerRegistrationChild* sentActor =
+ parentActor->SendPServiceWorkerRegistrationConstructor(
+ actor, aDescriptor.ToIPC());
+ if (NS_WARN_IF(!sentActor)) {
+ Shutdown();
+ return;
+ }
+ MOZ_DIAGNOSTIC_ASSERT(sentActor == actor);
+
+ mActor = std::move(actor);
+ mActor->SetOwner(this);
+
+ KeepAliveIfHasListenersFor(nsGkAtoms::onupdatefound);
+}
+
+ServiceWorkerRegistration::~ServiceWorkerRegistration() { Shutdown(); }
+
+JSObject* ServiceWorkerRegistration::WrapObject(
+ JSContext* aCx, JS::Handle<JSObject*> aGivenProto) {
+ return ServiceWorkerRegistration_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+/* static */
+already_AddRefed<ServiceWorkerRegistration>
+ServiceWorkerRegistration::CreateForMainThread(
+ nsPIDOMWindowInner* aWindow,
+ const ServiceWorkerRegistrationDescriptor& aDescriptor) {
+ MOZ_ASSERT(aWindow);
+ MOZ_ASSERT(NS_IsMainThread());
+
+ RefPtr<ServiceWorkerRegistration> registration =
+ new ServiceWorkerRegistration(aWindow->AsGlobal(), aDescriptor);
+ // This is not called from within the constructor, as it may call content code
+ // which can cause the deletion of the registration, so we need to keep a
+ // strong reference while calling it.
+ registration->UpdateState(aDescriptor);
+
+ return registration.forget();
+}
+
+/* static */
+already_AddRefed<ServiceWorkerRegistration>
+ServiceWorkerRegistration::CreateForWorker(
+ WorkerPrivate* aWorkerPrivate, nsIGlobalObject* aGlobal,
+ const ServiceWorkerRegistrationDescriptor& aDescriptor) {
+ MOZ_DIAGNOSTIC_ASSERT(aWorkerPrivate);
+ MOZ_DIAGNOSTIC_ASSERT(aGlobal);
+ aWorkerPrivate->AssertIsOnWorkerThread();
+
+ RefPtr<ServiceWorkerRegistration> registration =
+ new ServiceWorkerRegistration(aGlobal, aDescriptor);
+ // This is not called from within the constructor, as it may call content code
+ // which can cause the deletion of the registration, so we need to keep a
+ // strong reference while calling it.
+ registration->UpdateState(aDescriptor);
+
+ return registration.forget();
+}
+
+void ServiceWorkerRegistration::DisconnectFromOwner() {
+ DOMEventTargetHelper::DisconnectFromOwner();
+}
+
+void ServiceWorkerRegistration::RegistrationCleared() {
+ // Its possible that the registration will fail to install and be
+ // immediately removed. In that case we may never receive the
+ // UpdateState() call if the actor was too slow to connect, etc.
+ // Ensure that we force all our known actors to redundant so that
+ // the appropriate statechange events are fired. If we got the
+ // UpdateState() already then this will be a no-op.
+ UpdateStateInternal(Maybe<ServiceWorkerDescriptor>(),
+ Maybe<ServiceWorkerDescriptor>(),
+ Maybe<ServiceWorkerDescriptor>());
+
+ // Our underlying registration was removed from SWM, so we
+ // will never get an updatefound event again. We can let
+ // the object GC if content is not holding it alive.
+ IgnoreKeepAliveIfHasListenersFor(nsGkAtoms::onupdatefound);
+}
+
+already_AddRefed<ServiceWorker> ServiceWorkerRegistration::GetInstalling()
+ const {
+ RefPtr<ServiceWorker> ref = mInstallingWorker;
+ return ref.forget();
+}
+
+already_AddRefed<ServiceWorker> ServiceWorkerRegistration::GetWaiting() const {
+ RefPtr<ServiceWorker> ref = mWaitingWorker;
+ return ref.forget();
+}
+
+already_AddRefed<ServiceWorker> ServiceWorkerRegistration::GetActive() const {
+ RefPtr<ServiceWorker> ref = mActiveWorker;
+ return ref.forget();
+}
+
+already_AddRefed<NavigationPreloadManager>
+ServiceWorkerRegistration::NavigationPreload() {
+ RefPtr<ServiceWorkerRegistration> reg = this;
+ if (!mNavigationPreloadManager) {
+ mNavigationPreloadManager = MakeRefPtr<NavigationPreloadManager>(reg);
+ }
+ RefPtr<NavigationPreloadManager> ref = mNavigationPreloadManager;
+ return ref.forget();
+}
+
+void ServiceWorkerRegistration::UpdateState(
+ const ServiceWorkerRegistrationDescriptor& aDescriptor) {
+ MOZ_DIAGNOSTIC_ASSERT(MatchesDescriptor(aDescriptor));
+
+ mDescriptor = aDescriptor;
+
+ UpdateStateInternal(aDescriptor.GetInstalling(), aDescriptor.GetWaiting(),
+ aDescriptor.GetActive());
+
+ nsTArray<UniquePtr<VersionCallback>> callbackList =
+ std::move(mVersionCallbackList);
+ for (auto& cb : callbackList) {
+ if (cb->mVersion > mDescriptor.Version()) {
+ mVersionCallbackList.AppendElement(std::move(cb));
+ continue;
+ }
+
+ cb->mFunc(cb->mVersion == mDescriptor.Version());
+ }
+}
+
+bool ServiceWorkerRegistration::MatchesDescriptor(
+ const ServiceWorkerRegistrationDescriptor& aDescriptor) const {
+ return aDescriptor.Id() == mDescriptor.Id() &&
+ aDescriptor.PrincipalInfo() == mDescriptor.PrincipalInfo() &&
+ aDescriptor.Scope() == mDescriptor.Scope();
+}
+
+void ServiceWorkerRegistration::GetScope(nsAString& aScope) const {
+ CopyUTF8toUTF16(mDescriptor.Scope(), aScope);
+}
+
+ServiceWorkerUpdateViaCache ServiceWorkerRegistration::GetUpdateViaCache(
+ ErrorResult& aRv) const {
+ return mDescriptor.UpdateViaCache();
+}
+
+already_AddRefed<Promise> ServiceWorkerRegistration::Update(ErrorResult& aRv) {
+ nsIGlobalObject* global = GetParentObject();
+ if (!global) {
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return nullptr;
+ }
+
+ RefPtr<Promise> outer = Promise::Create(global, aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return nullptr;
+ }
+
+ // `ServiceWorker` objects are not exposed on worker threads yet, so calling
+ // `ServiceWorkerRegistration::Get{Installing,Waiting,Active}` won't work.
+ const Maybe<ServiceWorkerDescriptor> newestWorkerDescriptor =
+ mDescriptor.Newest();
+
+ // "If newestWorker is null, return a promise rejected with an
+ // "InvalidStateError" DOMException and abort these steps."
+ if (newestWorkerDescriptor.isNothing()) {
+ outer->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return outer.forget();
+ }
+
+ // "If the context object’s relevant settings object’s global object
+ // globalObject is a ServiceWorkerGlobalScope object, and globalObject’s
+ // associated service worker's state is "installing", return a promise
+ // rejected with an "InvalidStateError" DOMException and abort these steps."
+ if (!NS_IsMainThread()) {
+ WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate();
+ MOZ_ASSERT(workerPrivate);
+
+ if (workerPrivate->IsServiceWorker() &&
+ (workerPrivate->GetServiceWorkerDescriptor().State() ==
+ ServiceWorkerState::Installing)) {
+ outer->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return outer.forget();
+ }
+ }
+
+ RefPtr<ServiceWorkerRegistration> self = this;
+
+ if (!mActor) {
+ outer->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return outer.forget();
+ }
+
+ mActor->SendUpdate(
+ newestWorkerDescriptor.ref().ScriptURL(),
+ [outer,
+ self](const IPCServiceWorkerRegistrationDescriptorOrCopyableErrorResult&
+ aResult) {
+ if (aResult.type() ==
+ IPCServiceWorkerRegistrationDescriptorOrCopyableErrorResult::
+ TCopyableErrorResult) {
+ // application layer error
+ const auto& rv = aResult.get_CopyableErrorResult();
+ MOZ_DIAGNOSTIC_ASSERT(rv.Failed());
+ outer->MaybeReject(CopyableErrorResult(rv));
+ return;
+ }
+ // success
+ const auto& ipcDesc =
+ aResult.get_IPCServiceWorkerRegistrationDescriptor();
+ nsIGlobalObject* global = self->GetParentObject();
+ // It's possible this binding was detached from the global. In cases
+ // where we use IPC with Promise callbacks, we use
+ // DOMMozPromiseRequestHolder in order to auto-disconnect the promise
+ // that would hold these callbacks. However in bug 1466681 we changed
+ // this call to use (synchronous) callbacks because the use of
+ // MozPromise introduced an additional runnable scheduling which made
+ // it very difficult to maintain ordering required by the standard.
+ //
+ // If we were to delete this actor at the time of DETH detaching, we
+ // would not need to do this check because the IPC callback of the
+ // RemoteServiceWorkerRegistrationImpl lambdas would never occur.
+ // However, its actors currently depend on asking the parent to delete
+ // the actor for us. Given relaxations in the IPC lifecyle, we could
+ // potentially issue a direct termination, but that requires additional
+ // evaluation.
+ if (!global) {
+ outer->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return;
+ }
+ RefPtr<ServiceWorkerRegistration> ref =
+ global->GetOrCreateServiceWorkerRegistration(
+ ServiceWorkerRegistrationDescriptor(ipcDesc));
+ if (!ref) {
+ outer->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return;
+ }
+ outer->MaybeResolve(ref);
+ },
+ [outer](ResponseRejectReason&& aReason) {
+ // IPC layer error
+ outer->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR);
+ });
+
+ return outer.forget();
+}
+
+already_AddRefed<Promise> ServiceWorkerRegistration::Unregister(
+ ErrorResult& aRv) {
+ nsIGlobalObject* global = GetParentObject();
+ if (NS_WARN_IF(!global)) {
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return nullptr;
+ }
+
+ RefPtr<Promise> outer = Promise::Create(global, aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return nullptr;
+ }
+
+ if (!mActor) {
+ outer->MaybeResolve(false);
+ return outer.forget();
+ }
+
+ mActor->SendUnregister(
+ [outer](Tuple<bool, CopyableErrorResult>&& aResult) {
+ if (Get<1>(aResult).Failed()) {
+ // application layer error
+ // register() should be resilient and resolve false instead of
+ // rejecting in most cases.
+ Get<1>(aResult).SuppressException();
+ outer->MaybeResolve(false);
+ return;
+ }
+ // success
+ outer->MaybeResolve(Get<0>(aResult));
+ },
+ [outer](ResponseRejectReason&& aReason) {
+ // IPC layer error
+ outer->MaybeResolve(false);
+ });
+
+ return outer.forget();
+}
+
+already_AddRefed<PushManager> ServiceWorkerRegistration::GetPushManager(
+ JSContext* aCx, ErrorResult& aRv) {
+ if (!mPushManager) {
+ nsCOMPtr<nsIGlobalObject> globalObject = GetParentObject();
+
+ if (!globalObject) {
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return nullptr;
+ }
+
+ GlobalObject global(aCx, globalObject->GetGlobalJSObject());
+ mPushManager = PushManager::Constructor(
+ global, NS_ConvertUTF8toUTF16(mDescriptor.Scope()), aRv);
+ if (aRv.Failed()) {
+ return nullptr;
+ }
+ }
+
+ RefPtr<PushManager> ret = mPushManager;
+ return ret.forget();
+}
+
+already_AddRefed<Promise> ServiceWorkerRegistration::ShowNotification(
+ JSContext* aCx, const nsAString& aTitle,
+ const NotificationOptions& aOptions, ErrorResult& aRv) {
+ nsIGlobalObject* global = GetParentObject();
+ if (!global) {
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return nullptr;
+ }
+
+ // Until we ship ServiceWorker objects on worker threads the active
+ // worker will always be nullptr. So limit this check to main
+ // thread for now.
+ if (mDescriptor.GetActive().isNothing() && NS_IsMainThread()) {
+ aRv.ThrowTypeError<MSG_NO_ACTIVE_WORKER>(mDescriptor.Scope());
+ return nullptr;
+ }
+
+ NS_ConvertUTF8toUTF16 scope(mDescriptor.Scope());
+
+ RefPtr<Promise> p = Notification::ShowPersistentNotification(
+ aCx, global, scope, aTitle, aOptions, mDescriptor, aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return nullptr;
+ }
+
+ return p.forget();
+}
+
+already_AddRefed<Promise> ServiceWorkerRegistration::GetNotifications(
+ const GetNotificationOptions& aOptions, ErrorResult& aRv) {
+ nsIGlobalObject* global = GetParentObject();
+ if (!global) {
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return nullptr;
+ }
+
+ NS_ConvertUTF8toUTF16 scope(mDescriptor.Scope());
+
+ if (NS_IsMainThread()) {
+ nsCOMPtr<nsPIDOMWindowInner> window = do_QueryInterface(global);
+ if (NS_WARN_IF(!window)) {
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return nullptr;
+ }
+ return Notification::Get(window, aOptions, scope, aRv);
+ }
+
+ WorkerPrivate* worker = GetCurrentThreadWorkerPrivate();
+ worker->AssertIsOnWorkerThread();
+ return Notification::WorkerGet(worker, aOptions, scope, aRv);
+}
+
+void ServiceWorkerRegistration::SetNavigationPreloadEnabled(
+ bool aEnabled, ServiceWorkerBoolCallback&& aSuccessCB,
+ ServiceWorkerFailureCallback&& aFailureCB) {
+ if (!mActor) {
+ aFailureCB(CopyableErrorResult(NS_ERROR_DOM_INVALID_STATE_ERR));
+ return;
+ }
+
+ mActor->SendSetNavigationPreloadEnabled(
+ aEnabled,
+ [successCB = std::move(aSuccessCB), aFailureCB](bool aResult) {
+ if (!aResult) {
+ aFailureCB(CopyableErrorResult(NS_ERROR_DOM_INVALID_STATE_ERR));
+ return;
+ }
+ successCB(aResult);
+ },
+ [aFailureCB](ResponseRejectReason&& aReason) {
+ aFailureCB(CopyableErrorResult(NS_ERROR_DOM_INVALID_STATE_ERR));
+ });
+}
+
+void ServiceWorkerRegistration::SetNavigationPreloadHeader(
+ const nsCString& aHeader, ServiceWorkerBoolCallback&& aSuccessCB,
+ ServiceWorkerFailureCallback&& aFailureCB) {
+ if (!mActor) {
+ aFailureCB(CopyableErrorResult(NS_ERROR_DOM_INVALID_STATE_ERR));
+ return;
+ }
+
+ mActor->SendSetNavigationPreloadHeader(
+ aHeader,
+ [successCB = std::move(aSuccessCB), aFailureCB](bool aResult) {
+ if (!aResult) {
+ aFailureCB(CopyableErrorResult(NS_ERROR_DOM_INVALID_STATE_ERR));
+ return;
+ }
+ successCB(aResult);
+ },
+ [aFailureCB](ResponseRejectReason&& aReason) {
+ aFailureCB(CopyableErrorResult(NS_ERROR_DOM_INVALID_STATE_ERR));
+ });
+}
+
+void ServiceWorkerRegistration::GetNavigationPreloadState(
+ NavigationPreloadGetStateCallback&& aSuccessCB,
+ ServiceWorkerFailureCallback&& aFailureCB) {
+ if (!mActor) {
+ aFailureCB(CopyableErrorResult(NS_ERROR_DOM_INVALID_STATE_ERR));
+ return;
+ }
+
+ mActor->SendGetNavigationPreloadState(
+ [successCB = std::move(aSuccessCB),
+ aFailureCB](Maybe<IPCNavigationPreloadState>&& aState) {
+ if (NS_WARN_IF(!aState)) {
+ aFailureCB(CopyableErrorResult(NS_ERROR_DOM_INVALID_STATE_ERR));
+ return;
+ }
+
+ NavigationPreloadState state;
+ state.mEnabled = aState.ref().enabled();
+ state.mHeaderValue.Construct(std::move(aState.ref().headerValue()));
+ successCB(std::move(state));
+ },
+ [aFailureCB](ResponseRejectReason&& aReason) {
+ aFailureCB(CopyableErrorResult(NS_ERROR_DOM_INVALID_STATE_ERR));
+ });
+}
+
+const ServiceWorkerRegistrationDescriptor&
+ServiceWorkerRegistration::Descriptor() const {
+ return mDescriptor;
+}
+
+void ServiceWorkerRegistration::WhenVersionReached(
+ uint64_t aVersion, ServiceWorkerBoolCallback&& aCallback) {
+ if (aVersion <= mDescriptor.Version()) {
+ aCallback(aVersion == mDescriptor.Version());
+ return;
+ }
+
+ mVersionCallbackList.AppendElement(
+ MakeUnique<VersionCallback>(aVersion, std::move(aCallback)));
+}
+
+void ServiceWorkerRegistration::MaybeScheduleUpdateFound(
+ const Maybe<ServiceWorkerDescriptor>& aInstallingDescriptor) {
+ // This function sets mScheduledUpdateFoundId to note when we were told about
+ // a new installing worker. We rely on a call to
+ // MaybeDispatchUpdateFoundRunnable (called indirectly from UpdateJobCallback)
+ // to actually fire the event.
+ uint64_t newId = aInstallingDescriptor.isSome()
+ ? aInstallingDescriptor.ref().Id()
+ : kInvalidUpdateFoundId;
+
+ if (mScheduledUpdateFoundId != kInvalidUpdateFoundId) {
+ if (mScheduledUpdateFoundId == newId) {
+ return;
+ }
+ MaybeDispatchUpdateFound();
+ MOZ_DIAGNOSTIC_ASSERT(mScheduledUpdateFoundId == kInvalidUpdateFoundId);
+ }
+
+ bool updateFound =
+ newId != kInvalidUpdateFoundId && mDispatchedUpdateFoundId != newId;
+
+ if (!updateFound) {
+ return;
+ }
+
+ mScheduledUpdateFoundId = newId;
+}
+
+void ServiceWorkerRegistration::MaybeDispatchUpdateFoundRunnable() {
+ if (mScheduledUpdateFoundId == kInvalidUpdateFoundId) {
+ return;
+ }
+
+ nsIGlobalObject* global = GetParentObject();
+ NS_ENSURE_TRUE_VOID(global);
+
+ nsCOMPtr<nsIRunnable> r = NewCancelableRunnableMethod(
+ "ServiceWorkerRegistration::MaybeDispatchUpdateFound", this,
+ &ServiceWorkerRegistration::MaybeDispatchUpdateFound);
+
+ Unused << global->EventTargetFor(TaskCategory::Other)
+ ->Dispatch(r.forget(), NS_DISPATCH_NORMAL);
+}
+
+void ServiceWorkerRegistration::MaybeDispatchUpdateFound() {
+ uint64_t scheduledId = mScheduledUpdateFoundId;
+ mScheduledUpdateFoundId = kInvalidUpdateFoundId;
+
+ if (scheduledId == kInvalidUpdateFoundId ||
+ scheduledId == mDispatchedUpdateFoundId) {
+ return;
+ }
+
+ mDispatchedUpdateFoundId = scheduledId;
+ DispatchTrustedEvent(u"updatefound"_ns);
+}
+
+void ServiceWorkerRegistration::UpdateStateInternal(
+ const Maybe<ServiceWorkerDescriptor>& aInstalling,
+ const Maybe<ServiceWorkerDescriptor>& aWaiting,
+ const Maybe<ServiceWorkerDescriptor>& aActive) {
+ // Do this immediately as it may flush an already pending updatefound
+ // event. In that case we want to fire the pending event before
+ // modifying any of the registration properties.
+ MaybeScheduleUpdateFound(aInstalling);
+
+ // Move the currently exposed workers into a separate list
+ // of "old" workers. We will then potentially add them
+ // back to the registration properties below based on the
+ // given descriptor. Any that are not restored will need
+ // to be moved to the redundant state.
+ AutoTArray<RefPtr<ServiceWorker>, 3> oldWorkerList({
+ std::move(mInstallingWorker),
+ std::move(mWaitingWorker),
+ std::move(mActiveWorker),
+ });
+
+ // Its important that all state changes are actually applied before
+ // dispatching any statechange events. Each ServiceWorker object
+ // should be in the correct state and the ServiceWorkerRegistration
+ // properties need to be set correctly as well. To accomplish this
+ // we use a ScopeExit to dispatch any statechange events.
+ auto scopeExit = MakeScopeExit([&] {
+ // Check to see if any of the "old" workers was completely discarded.
+ // Set these workers to the redundant state.
+ for (auto& oldWorker : oldWorkerList) {
+ if (!oldWorker || oldWorker == mInstallingWorker ||
+ oldWorker == mWaitingWorker || oldWorker == mActiveWorker) {
+ continue;
+ }
+
+ oldWorker->SetState(ServiceWorkerState::Redundant);
+ }
+
+ // Check each worker to see if it needs a statechange event dispatched.
+ if (mInstallingWorker) {
+ mInstallingWorker->MaybeDispatchStateChangeEvent();
+ }
+ if (mWaitingWorker) {
+ mWaitingWorker->MaybeDispatchStateChangeEvent();
+ }
+ if (mActiveWorker) {
+ mActiveWorker->MaybeDispatchStateChangeEvent();
+ }
+
+ // We also check the "old" workers to see if they need a statechange
+ // event as well. Note, these may overlap with the known worker properties
+ // above, but MaybeDispatchStateChangeEvent() will ignore duplicated calls.
+ for (auto& oldWorker : oldWorkerList) {
+ if (!oldWorker) {
+ continue;
+ }
+
+ oldWorker->MaybeDispatchStateChangeEvent();
+ }
+ });
+
+ // Clear all workers if the registration has been detached from the global.
+ // Also, we cannot expose ServiceWorker objects on worker threads yet, so
+ // do the same on when off-main-thread. This main thread check should be
+ // removed as part of bug 1113522.
+ nsCOMPtr<nsIGlobalObject> global = GetParentObject();
+ if (!global || !NS_IsMainThread()) {
+ return;
+ }
+
+ if (aActive.isSome()) {
+ if ((mActiveWorker = global->GetOrCreateServiceWorker(aActive.ref()))) {
+ mActiveWorker->SetState(aActive.ref().State());
+ }
+ } else {
+ mActiveWorker = nullptr;
+ }
+
+ if (aWaiting.isSome()) {
+ if ((mWaitingWorker = global->GetOrCreateServiceWorker(aWaiting.ref()))) {
+ mWaitingWorker->SetState(aWaiting.ref().State());
+ }
+ } else {
+ mWaitingWorker = nullptr;
+ }
+
+ if (aInstalling.isSome()) {
+ if ((mInstallingWorker =
+ global->GetOrCreateServiceWorker(aInstalling.ref()))) {
+ mInstallingWorker->SetState(aInstalling.ref().State());
+ }
+ } else {
+ mInstallingWorker = nullptr;
+ }
+}
+
+void ServiceWorkerRegistration::RevokeActor(
+ ServiceWorkerRegistrationChild* aActor) {
+ MOZ_DIAGNOSTIC_ASSERT(mActor);
+ MOZ_DIAGNOSTIC_ASSERT(mActor == aActor);
+ mActor->RevokeOwner(this);
+ mActor = nullptr;
+
+ mShutdown = true;
+
+ RegistrationCleared();
+}
+
+void ServiceWorkerRegistration::Shutdown() {
+ if (mShutdown) {
+ return;
+ }
+ mShutdown = true;
+
+ if (mActor) {
+ mActor->RevokeOwner(this);
+ mActor->MaybeStartTeardown();
+ mActor = nullptr;
+ }
+}
+
+} // namespace mozilla::dom
diff --git a/dom/serviceworkers/ServiceWorkerRegistration.h b/dom/serviceworkers/ServiceWorkerRegistration.h
new file mode 100644
index 0000000000..fa638d37b6
--- /dev/null
+++ b/dom/serviceworkers/ServiceWorkerRegistration.h
@@ -0,0 +1,162 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_ServiceWorkerRegistration_h
+#define mozilla_dom_ServiceWorkerRegistration_h
+
+#include "mozilla/DOMEventTargetHelper.h"
+#include "mozilla/dom/ServiceWorkerBinding.h"
+#include "mozilla/dom/ServiceWorkerRegistrationBinding.h"
+#include "mozilla/dom/ServiceWorkerRegistrationDescriptor.h"
+#include "mozilla/dom/ServiceWorkerUtils.h"
+
+// Support for Notification API extension.
+#include "mozilla/dom/NotificationBinding.h"
+
+class nsIGlobalObject;
+
+namespace mozilla::dom {
+
+class NavigationPreloadManager;
+class Promise;
+class PushManager;
+class WorkerPrivate;
+class ServiceWorker;
+class ServiceWorkerRegistrationChild;
+
+#define NS_DOM_SERVICEWORKERREGISTRATION_IID \
+ { \
+ 0x4578a90e, 0xa427, 0x4237, { \
+ 0x98, 0x4a, 0xbd, 0x98, 0xe4, 0xcd, 0x5f, 0x3a \
+ } \
+ }
+
+class ServiceWorkerRegistration final : public DOMEventTargetHelper {
+ public:
+ NS_DECLARE_STATIC_IID_ACCESSOR(NS_DOM_SERVICEWORKERREGISTRATION_IID)
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(ServiceWorkerRegistration,
+ DOMEventTargetHelper)
+
+ IMPL_EVENT_HANDLER(updatefound)
+
+ static already_AddRefed<ServiceWorkerRegistration> CreateForMainThread(
+ nsPIDOMWindowInner* aWindow,
+ const ServiceWorkerRegistrationDescriptor& aDescriptor);
+
+ static already_AddRefed<ServiceWorkerRegistration> CreateForWorker(
+ WorkerPrivate* aWorkerPrivate, nsIGlobalObject* aGlobal,
+ const ServiceWorkerRegistrationDescriptor& aDescriptor);
+
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ void DisconnectFromOwner() override;
+
+ void RegistrationCleared();
+
+ already_AddRefed<ServiceWorker> GetInstalling() const;
+
+ already_AddRefed<ServiceWorker> GetWaiting() const;
+
+ already_AddRefed<ServiceWorker> GetActive() const;
+
+ already_AddRefed<NavigationPreloadManager> NavigationPreload();
+
+ void UpdateState(const ServiceWorkerRegistrationDescriptor& aDescriptor);
+
+ bool MatchesDescriptor(
+ const ServiceWorkerRegistrationDescriptor& aDescriptor) const;
+
+ void GetScope(nsAString& aScope) const;
+
+ ServiceWorkerUpdateViaCache GetUpdateViaCache(ErrorResult& aRv) const;
+
+ already_AddRefed<Promise> Update(ErrorResult& aRv);
+
+ already_AddRefed<Promise> Unregister(ErrorResult& aRv);
+
+ already_AddRefed<PushManager> GetPushManager(JSContext* aCx,
+ ErrorResult& aRv);
+
+ already_AddRefed<Promise> ShowNotification(
+ JSContext* aCx, const nsAString& aTitle,
+ const NotificationOptions& aOptions, ErrorResult& aRv);
+
+ already_AddRefed<Promise> GetNotifications(
+ const GetNotificationOptions& aOptions, ErrorResult& aRv);
+
+ void SetNavigationPreloadEnabled(bool aEnabled,
+ ServiceWorkerBoolCallback&& aSuccessCB,
+ ServiceWorkerFailureCallback&& aFailureCB);
+
+ void SetNavigationPreloadHeader(const nsCString& aHeader,
+ ServiceWorkerBoolCallback&& aSuccessCB,
+ ServiceWorkerFailureCallback&& aFailureCB);
+
+ void GetNavigationPreloadState(NavigationPreloadGetStateCallback&& aSuccessCB,
+ ServiceWorkerFailureCallback&& aFailureCB);
+
+ const ServiceWorkerRegistrationDescriptor& Descriptor() const;
+
+ void WhenVersionReached(uint64_t aVersion,
+ ServiceWorkerBoolCallback&& aCallback);
+
+ void MaybeDispatchUpdateFoundRunnable();
+
+ void RevokeActor(ServiceWorkerRegistrationChild* aActor);
+
+ void FireUpdateFound();
+
+ private:
+ ServiceWorkerRegistration(
+ nsIGlobalObject* aGlobal,
+ const ServiceWorkerRegistrationDescriptor& aDescriptor);
+
+ ~ServiceWorkerRegistration();
+
+ void UpdateStateInternal(const Maybe<ServiceWorkerDescriptor>& aInstalling,
+ const Maybe<ServiceWorkerDescriptor>& aWaiting,
+ const Maybe<ServiceWorkerDescriptor>& aActive);
+
+ void MaybeScheduleUpdateFound(
+ const Maybe<ServiceWorkerDescriptor>& aInstallingDescriptor);
+
+ void MaybeDispatchUpdateFound();
+
+ void Shutdown();
+
+ ServiceWorkerRegistrationDescriptor mDescriptor;
+ RefPtr<ServiceWorkerRegistrationChild> mActor;
+ bool mShutdown;
+
+ RefPtr<ServiceWorker> mInstallingWorker;
+ RefPtr<ServiceWorker> mWaitingWorker;
+ RefPtr<ServiceWorker> mActiveWorker;
+ RefPtr<NavigationPreloadManager> mNavigationPreloadManager;
+ RefPtr<PushManager> mPushManager;
+
+ struct VersionCallback {
+ uint64_t mVersion;
+ ServiceWorkerBoolCallback mFunc;
+
+ VersionCallback(uint64_t aVersion, ServiceWorkerBoolCallback&& aFunc)
+ : mVersion(aVersion), mFunc(std::move(aFunc)) {
+ MOZ_DIAGNOSTIC_ASSERT(mFunc);
+ }
+ };
+ nsTArray<UniquePtr<VersionCallback>> mVersionCallbackList;
+
+ uint64_t mScheduledUpdateFoundId;
+ uint64_t mDispatchedUpdateFoundId;
+};
+
+NS_DEFINE_STATIC_IID_ACCESSOR(ServiceWorkerRegistration,
+ NS_DOM_SERVICEWORKERREGISTRATION_IID)
+
+} // namespace mozilla::dom
+
+#endif /* mozilla_dom_ServiceWorkerRegistration_h */
diff --git a/dom/serviceworkers/ServiceWorkerRegistrationChild.cpp b/dom/serviceworkers/ServiceWorkerRegistrationChild.cpp
new file mode 100644
index 0000000000..b382f6dcfa
--- /dev/null
+++ b/dom/serviceworkers/ServiceWorkerRegistrationChild.cpp
@@ -0,0 +1,91 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "ServiceWorkerRegistrationChild.h"
+
+#include "ServiceWorkerRegistration.h"
+#include "mozilla/dom/WorkerCommon.h"
+#include "mozilla/dom/WorkerRef.h"
+
+namespace mozilla::dom {
+
+using mozilla::ipc::IPCResult;
+
+void ServiceWorkerRegistrationChild::ActorDestroy(ActorDestroyReason aReason) {
+ mIPCWorkerRef = nullptr;
+
+ if (mOwner) {
+ mOwner->RevokeActor(this);
+ MOZ_DIAGNOSTIC_ASSERT(!mOwner);
+ }
+}
+
+IPCResult ServiceWorkerRegistrationChild::RecvUpdateState(
+ const IPCServiceWorkerRegistrationDescriptor& aDescriptor) {
+ if (mOwner) {
+ RefPtr<ServiceWorkerRegistration> owner = mOwner;
+ owner->UpdateState(ServiceWorkerRegistrationDescriptor(aDescriptor));
+ }
+ return IPC_OK();
+}
+
+IPCResult ServiceWorkerRegistrationChild::RecvFireUpdateFound() {
+ if (mOwner) {
+ mOwner->MaybeDispatchUpdateFoundRunnable();
+ }
+ return IPC_OK();
+}
+
+// static
+RefPtr<ServiceWorkerRegistrationChild>
+ServiceWorkerRegistrationChild::Create() {
+ RefPtr actor = new ServiceWorkerRegistrationChild;
+
+ if (!NS_IsMainThread()) {
+ WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate();
+ MOZ_DIAGNOSTIC_ASSERT(workerPrivate);
+
+ RefPtr<IPCWorkerRefHelper<ServiceWorkerRegistrationChild>> helper =
+ new IPCWorkerRefHelper<ServiceWorkerRegistrationChild>(actor);
+
+ actor->mIPCWorkerRef = IPCWorkerRef::Create(
+ workerPrivate, "ServiceWorkerRegistrationChild",
+ [helper] { helper->Actor()->MaybeStartTeardown(); });
+
+ if (NS_WARN_IF(!actor->mIPCWorkerRef)) {
+ return nullptr;
+ }
+ }
+
+ return actor;
+}
+
+ServiceWorkerRegistrationChild::ServiceWorkerRegistrationChild()
+ : mOwner(nullptr), mTeardownStarted(false) {}
+
+void ServiceWorkerRegistrationChild::SetOwner(
+ ServiceWorkerRegistration* aOwner) {
+ MOZ_DIAGNOSTIC_ASSERT(!mOwner);
+ MOZ_DIAGNOSTIC_ASSERT(aOwner);
+ mOwner = aOwner;
+}
+
+void ServiceWorkerRegistrationChild::RevokeOwner(
+ ServiceWorkerRegistration* aOwner) {
+ MOZ_DIAGNOSTIC_ASSERT(mOwner);
+ MOZ_DIAGNOSTIC_ASSERT(aOwner == mOwner);
+ mOwner = nullptr;
+}
+
+void ServiceWorkerRegistrationChild::MaybeStartTeardown() {
+ if (mTeardownStarted) {
+ return;
+ }
+ mTeardownStarted = true;
+ Unused << SendTeardown();
+}
+
+} // namespace mozilla::dom
diff --git a/dom/serviceworkers/ServiceWorkerRegistrationChild.h b/dom/serviceworkers/ServiceWorkerRegistrationChild.h
new file mode 100644
index 0000000000..a26dfd6de9
--- /dev/null
+++ b/dom/serviceworkers/ServiceWorkerRegistrationChild.h
@@ -0,0 +1,52 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_serviceworkerregistrationchild_h__
+#define mozilla_dom_serviceworkerregistrationchild_h__
+
+#include "mozilla/dom/PServiceWorkerRegistrationChild.h"
+
+// XXX Avoid including this here by moving function bodies to the cpp file
+#include "mozilla/dom/WorkerRef.h"
+
+namespace mozilla::dom {
+
+class IPCWorkerRef;
+class ServiceWorkerRegistration;
+
+class ServiceWorkerRegistrationChild final
+ : public PServiceWorkerRegistrationChild {
+ RefPtr<IPCWorkerRef> mIPCWorkerRef;
+ ServiceWorkerRegistration* mOwner;
+ bool mTeardownStarted;
+
+ ServiceWorkerRegistrationChild();
+
+ ~ServiceWorkerRegistrationChild() = default;
+
+ // PServiceWorkerRegistrationChild
+ void ActorDestroy(ActorDestroyReason aReason) override;
+
+ mozilla::ipc::IPCResult RecvUpdateState(
+ const IPCServiceWorkerRegistrationDescriptor& aDescriptor) override;
+
+ mozilla::ipc::IPCResult RecvFireUpdateFound() override;
+
+ public:
+ NS_INLINE_DECL_REFCOUNTING(ServiceWorkerRegistrationChild, override);
+
+ static RefPtr<ServiceWorkerRegistrationChild> Create();
+
+ void SetOwner(ServiceWorkerRegistration* aOwner);
+
+ void RevokeOwner(ServiceWorkerRegistration* aOwner);
+
+ void MaybeStartTeardown();
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_serviceworkerregistrationchild_h__
diff --git a/dom/serviceworkers/ServiceWorkerRegistrationDescriptor.cpp b/dom/serviceworkers/ServiceWorkerRegistrationDescriptor.cpp
new file mode 100644
index 0000000000..1988df8c4a
--- /dev/null
+++ b/dom/serviceworkers/ServiceWorkerRegistrationDescriptor.cpp
@@ -0,0 +1,274 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/ServiceWorkerRegistrationDescriptor.h"
+
+#include "mozilla/dom/IPCServiceWorkerRegistrationDescriptor.h"
+#include "mozilla/ipc/PBackgroundSharedTypes.h"
+#include "ServiceWorkerInfo.h"
+
+namespace mozilla::dom {
+
+using mozilla::ipc::PrincipalInfo;
+using mozilla::ipc::PrincipalInfoToPrincipal;
+
+Maybe<IPCServiceWorkerDescriptor>
+ServiceWorkerRegistrationDescriptor::NewestInternal() const {
+ Maybe<IPCServiceWorkerDescriptor> result;
+ if (mData->installing().isSome()) {
+ result.emplace(mData->installing().ref());
+ } else if (mData->waiting().isSome()) {
+ result.emplace(mData->waiting().ref());
+ } else if (mData->active().isSome()) {
+ result.emplace(mData->active().ref());
+ }
+ return result;
+}
+
+ServiceWorkerRegistrationDescriptor::ServiceWorkerRegistrationDescriptor(
+ uint64_t aId, uint64_t aVersion, nsIPrincipal* aPrincipal,
+ const nsACString& aScope, ServiceWorkerUpdateViaCache aUpdateViaCache)
+ : mData(MakeUnique<IPCServiceWorkerRegistrationDescriptor>()) {
+ MOZ_ALWAYS_SUCCEEDS(
+ PrincipalToPrincipalInfo(aPrincipal, &mData->principalInfo()));
+
+ mData->id() = aId;
+ mData->version() = aVersion;
+ mData->scope() = aScope;
+ mData->updateViaCache() = aUpdateViaCache;
+ mData->installing() = Nothing();
+ mData->waiting() = Nothing();
+ mData->active() = Nothing();
+}
+
+ServiceWorkerRegistrationDescriptor::ServiceWorkerRegistrationDescriptor(
+ uint64_t aId, uint64_t aVersion,
+ const mozilla::ipc::PrincipalInfo& aPrincipalInfo, const nsACString& aScope,
+ ServiceWorkerUpdateViaCache aUpdateViaCache)
+ : mData(MakeUnique<IPCServiceWorkerRegistrationDescriptor>(
+ aId, aVersion, aPrincipalInfo, nsCString(aScope), aUpdateViaCache,
+ Nothing(), Nothing(), Nothing())) {}
+
+ServiceWorkerRegistrationDescriptor::ServiceWorkerRegistrationDescriptor(
+ const IPCServiceWorkerRegistrationDescriptor& aDescriptor)
+ : mData(MakeUnique<IPCServiceWorkerRegistrationDescriptor>(aDescriptor)) {
+ MOZ_DIAGNOSTIC_ASSERT(IsValid());
+}
+
+ServiceWorkerRegistrationDescriptor::ServiceWorkerRegistrationDescriptor(
+ const ServiceWorkerRegistrationDescriptor& aRight) {
+ // UniquePtr doesn't have a default copy constructor, so we can't rely
+ // on default copy construction. Use the assignment operator to
+ // minimize duplication.
+ operator=(aRight);
+}
+
+ServiceWorkerRegistrationDescriptor&
+ServiceWorkerRegistrationDescriptor::operator=(
+ const ServiceWorkerRegistrationDescriptor& aRight) {
+ if (this == &aRight) {
+ return *this;
+ }
+ mData.reset();
+ mData = MakeUnique<IPCServiceWorkerRegistrationDescriptor>(*aRight.mData);
+ MOZ_DIAGNOSTIC_ASSERT(IsValid());
+ return *this;
+}
+
+ServiceWorkerRegistrationDescriptor::ServiceWorkerRegistrationDescriptor(
+ ServiceWorkerRegistrationDescriptor&& aRight)
+ : mData(std::move(aRight.mData)) {
+ MOZ_DIAGNOSTIC_ASSERT(IsValid());
+}
+
+ServiceWorkerRegistrationDescriptor&
+ServiceWorkerRegistrationDescriptor::operator=(
+ ServiceWorkerRegistrationDescriptor&& aRight) {
+ mData.reset();
+ mData = std::move(aRight.mData);
+ MOZ_DIAGNOSTIC_ASSERT(IsValid());
+ return *this;
+}
+
+ServiceWorkerRegistrationDescriptor::~ServiceWorkerRegistrationDescriptor() {
+ // Non-default destructor to avoid exposing the IPC type in the header.
+}
+
+bool ServiceWorkerRegistrationDescriptor::operator==(
+ const ServiceWorkerRegistrationDescriptor& aRight) const {
+ return *mData == *aRight.mData;
+}
+
+uint64_t ServiceWorkerRegistrationDescriptor::Id() const { return mData->id(); }
+
+uint64_t ServiceWorkerRegistrationDescriptor::Version() const {
+ return mData->version();
+}
+
+ServiceWorkerUpdateViaCache
+ServiceWorkerRegistrationDescriptor::UpdateViaCache() const {
+ return mData->updateViaCache();
+}
+
+const mozilla::ipc::PrincipalInfo&
+ServiceWorkerRegistrationDescriptor::PrincipalInfo() const {
+ return mData->principalInfo();
+}
+
+Result<nsCOMPtr<nsIPrincipal>, nsresult>
+ServiceWorkerRegistrationDescriptor::GetPrincipal() const {
+ AssertIsOnMainThread();
+ return PrincipalInfoToPrincipal(mData->principalInfo());
+}
+
+const nsCString& ServiceWorkerRegistrationDescriptor::Scope() const {
+ return mData->scope();
+}
+
+Maybe<ServiceWorkerDescriptor>
+ServiceWorkerRegistrationDescriptor::GetInstalling() const {
+ Maybe<ServiceWorkerDescriptor> result;
+
+ if (mData->installing().isSome()) {
+ result.emplace(ServiceWorkerDescriptor(mData->installing().ref()));
+ }
+
+ return result;
+}
+
+Maybe<ServiceWorkerDescriptor> ServiceWorkerRegistrationDescriptor::GetWaiting()
+ const {
+ Maybe<ServiceWorkerDescriptor> result;
+
+ if (mData->waiting().isSome()) {
+ result.emplace(ServiceWorkerDescriptor(mData->waiting().ref()));
+ }
+
+ return result;
+}
+
+Maybe<ServiceWorkerDescriptor> ServiceWorkerRegistrationDescriptor::GetActive()
+ const {
+ Maybe<ServiceWorkerDescriptor> result;
+
+ if (mData->active().isSome()) {
+ result.emplace(ServiceWorkerDescriptor(mData->active().ref()));
+ }
+
+ return result;
+}
+
+Maybe<ServiceWorkerDescriptor> ServiceWorkerRegistrationDescriptor::Newest()
+ const {
+ Maybe<ServiceWorkerDescriptor> result;
+ Maybe<IPCServiceWorkerDescriptor> newest(NewestInternal());
+ if (newest.isSome()) {
+ result.emplace(ServiceWorkerDescriptor(newest.ref()));
+ }
+ return result;
+}
+
+bool ServiceWorkerRegistrationDescriptor::HasWorker(
+ const ServiceWorkerDescriptor& aDescriptor) const {
+ Maybe<ServiceWorkerDescriptor> installing = GetInstalling();
+ Maybe<ServiceWorkerDescriptor> waiting = GetWaiting();
+ Maybe<ServiceWorkerDescriptor> active = GetActive();
+ return (installing.isSome() && installing.ref().Matches(aDescriptor)) ||
+ (waiting.isSome() && waiting.ref().Matches(aDescriptor)) ||
+ (active.isSome() && active.ref().Matches(aDescriptor));
+}
+
+namespace {
+
+bool IsValidWorker(
+ const Maybe<IPCServiceWorkerDescriptor>& aWorker, const nsACString& aScope,
+ const mozilla::ipc::ContentPrincipalInfo& aContentPrincipal) {
+ if (aWorker.isNothing()) {
+ return true;
+ }
+
+ auto& worker = aWorker.ref();
+ if (worker.scope() != aScope) {
+ return false;
+ }
+
+ auto& principalInfo = worker.principalInfo();
+ if (principalInfo.type() !=
+ mozilla::ipc::PrincipalInfo::TContentPrincipalInfo) {
+ return false;
+ }
+
+ auto& contentPrincipal = principalInfo.get_ContentPrincipalInfo();
+ if (contentPrincipal.originNoSuffix() != aContentPrincipal.originNoSuffix() ||
+ contentPrincipal.attrs() != aContentPrincipal.attrs()) {
+ return false;
+ }
+
+ return true;
+}
+
+} // anonymous namespace
+
+bool ServiceWorkerRegistrationDescriptor::IsValid() const {
+ auto& principalInfo = PrincipalInfo();
+ if (principalInfo.type() !=
+ mozilla::ipc::PrincipalInfo::TContentPrincipalInfo) {
+ return false;
+ }
+
+ auto& contentPrincipal = principalInfo.get_ContentPrincipalInfo();
+ if (!IsValidWorker(mData->installing(), Scope(), contentPrincipal) ||
+ !IsValidWorker(mData->waiting(), Scope(), contentPrincipal) ||
+ !IsValidWorker(mData->active(), Scope(), contentPrincipal)) {
+ return false;
+ }
+
+ return true;
+}
+
+void ServiceWorkerRegistrationDescriptor::SetUpdateViaCache(
+ ServiceWorkerUpdateViaCache aUpdateViaCache) {
+ mData->updateViaCache() = aUpdateViaCache;
+}
+
+void ServiceWorkerRegistrationDescriptor::SetWorkers(
+ ServiceWorkerInfo* aInstalling, ServiceWorkerInfo* aWaiting,
+ ServiceWorkerInfo* aActive) {
+ if (aInstalling) {
+ aInstalling->SetRegistrationVersion(Version());
+ mData->installing() = Some(aInstalling->Descriptor().ToIPC());
+ } else {
+ mData->installing() = Nothing();
+ }
+
+ if (aWaiting) {
+ aWaiting->SetRegistrationVersion(Version());
+ mData->waiting() = Some(aWaiting->Descriptor().ToIPC());
+ } else {
+ mData->waiting() = Nothing();
+ }
+
+ if (aActive) {
+ aActive->SetRegistrationVersion(Version());
+ mData->active() = Some(aActive->Descriptor().ToIPC());
+ } else {
+ mData->active() = Nothing();
+ }
+
+ MOZ_DIAGNOSTIC_ASSERT(IsValid());
+}
+
+void ServiceWorkerRegistrationDescriptor::SetVersion(uint64_t aVersion) {
+ MOZ_DIAGNOSTIC_ASSERT(aVersion > mData->version());
+ mData->version() = aVersion;
+}
+
+const IPCServiceWorkerRegistrationDescriptor&
+ServiceWorkerRegistrationDescriptor::ToIPC() const {
+ return *mData;
+}
+
+} // namespace mozilla::dom
diff --git a/dom/serviceworkers/ServiceWorkerRegistrationDescriptor.h b/dom/serviceworkers/ServiceWorkerRegistrationDescriptor.h
new file mode 100644
index 0000000000..ec0f969868
--- /dev/null
+++ b/dom/serviceworkers/ServiceWorkerRegistrationDescriptor.h
@@ -0,0 +1,103 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#ifndef _mozilla_dom_ServiceWorkerRegistrationDescriptor_h
+#define _mozilla_dom_ServiceWorkerRegistrationDescriptor_h
+
+#include "mozilla/Maybe.h"
+#include "mozilla/dom/ServiceWorkerDescriptor.h"
+#include "mozilla/UniquePtr.h"
+
+namespace mozilla {
+
+namespace ipc {
+class PrincipalInfo;
+} // namespace ipc
+
+namespace dom {
+
+class IPCServiceWorkerRegistrationDescriptor;
+class ServiceWorkerInfo;
+enum class ServiceWorkerUpdateViaCache : uint8_t;
+
+// This class represents a snapshot of a particular
+// ServiceWorkerRegistrationInfo object. It is threadsafe and can be
+// transferred across processes.
+class ServiceWorkerRegistrationDescriptor final {
+ // This class is largely a wrapper wround an IPDL generated struct. We
+ // need the wrapper class since IPDL generated code includes windows.h
+ // which is in turn incompatible with bindings code.
+ UniquePtr<IPCServiceWorkerRegistrationDescriptor> mData;
+
+ Maybe<IPCServiceWorkerDescriptor> NewestInternal() const;
+
+ public:
+ ServiceWorkerRegistrationDescriptor(
+ uint64_t aId, uint64_t aVersion, nsIPrincipal* aPrincipal,
+ const nsACString& aScope, ServiceWorkerUpdateViaCache aUpdateViaCache);
+
+ ServiceWorkerRegistrationDescriptor(
+ uint64_t aId, uint64_t aVersion,
+ const mozilla::ipc::PrincipalInfo& aPrincipalInfo,
+ const nsACString& aScope, ServiceWorkerUpdateViaCache aUpdateViaCache);
+
+ explicit ServiceWorkerRegistrationDescriptor(
+ const IPCServiceWorkerRegistrationDescriptor& aDescriptor);
+
+ ServiceWorkerRegistrationDescriptor(
+ const ServiceWorkerRegistrationDescriptor& aRight);
+
+ ServiceWorkerRegistrationDescriptor& operator=(
+ const ServiceWorkerRegistrationDescriptor& aRight);
+
+ ServiceWorkerRegistrationDescriptor(
+ ServiceWorkerRegistrationDescriptor&& aRight);
+
+ ServiceWorkerRegistrationDescriptor& operator=(
+ ServiceWorkerRegistrationDescriptor&& aRight);
+
+ ~ServiceWorkerRegistrationDescriptor();
+
+ bool operator==(const ServiceWorkerRegistrationDescriptor& aRight) const;
+
+ uint64_t Id() const;
+
+ uint64_t Version() const;
+
+ ServiceWorkerUpdateViaCache UpdateViaCache() const;
+
+ const mozilla::ipc::PrincipalInfo& PrincipalInfo() const;
+
+ Result<nsCOMPtr<nsIPrincipal>, nsresult> GetPrincipal() const;
+
+ const nsCString& Scope() const;
+
+ Maybe<ServiceWorkerDescriptor> GetInstalling() const;
+
+ Maybe<ServiceWorkerDescriptor> GetWaiting() const;
+
+ Maybe<ServiceWorkerDescriptor> GetActive() const;
+
+ Maybe<ServiceWorkerDescriptor> Newest() const;
+
+ bool HasWorker(const ServiceWorkerDescriptor& aDescriptor) const;
+
+ bool IsValid() const;
+
+ void SetUpdateViaCache(ServiceWorkerUpdateViaCache aUpdateViaCache);
+
+ void SetWorkers(ServiceWorkerInfo* aInstalling, ServiceWorkerInfo* aWaiting,
+ ServiceWorkerInfo* aActive);
+
+ void SetVersion(uint64_t aVersion);
+
+ // Expose the underlying IPC type so that it can be passed via IPC.
+ const IPCServiceWorkerRegistrationDescriptor& ToIPC() const;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // _mozilla_dom_ServiceWorkerRegistrationDescriptor_h
diff --git a/dom/serviceworkers/ServiceWorkerRegistrationInfo.cpp b/dom/serviceworkers/ServiceWorkerRegistrationInfo.cpp
new file mode 100644
index 0000000000..cf8c7543ee
--- /dev/null
+++ b/dom/serviceworkers/ServiceWorkerRegistrationInfo.cpp
@@ -0,0 +1,907 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "ServiceWorkerRegistrationInfo.h"
+
+#include "ServiceWorkerManager.h"
+#include "ServiceWorkerPrivate.h"
+#include "ServiceWorkerRegistrationListener.h"
+
+#include "mozilla/Preferences.h"
+#include "mozilla/SchedulerGroup.h"
+#include "mozilla/StaticPrefs_dom.h"
+
+namespace mozilla::dom {
+
+namespace {
+
+class ContinueActivateRunnable final : public LifeCycleEventCallback {
+ nsMainThreadPtrHandle<ServiceWorkerRegistrationInfo> mRegistration;
+ bool mSuccess;
+
+ public:
+ explicit ContinueActivateRunnable(
+ const nsMainThreadPtrHandle<ServiceWorkerRegistrationInfo>& aRegistration)
+ : mRegistration(aRegistration), mSuccess(false) {
+ MOZ_ASSERT(NS_IsMainThread());
+ }
+
+ void SetResult(bool aResult) override { mSuccess = aResult; }
+
+ NS_IMETHOD
+ Run() override {
+ MOZ_ASSERT(NS_IsMainThread());
+ mRegistration->FinishActivate(mSuccess);
+ mRegistration = nullptr;
+ return NS_OK;
+ }
+};
+
+} // anonymous namespace
+
+void ServiceWorkerRegistrationInfo::ShutdownWorkers() {
+ ForEachWorker([](RefPtr<ServiceWorkerInfo>& aWorker) {
+ aWorker->WorkerPrivate()->NoteDeadServiceWorkerInfo();
+ aWorker = nullptr;
+ });
+}
+
+void ServiceWorkerRegistrationInfo::Clear() {
+ ForEachWorker([](RefPtr<ServiceWorkerInfo>& aWorker) {
+ aWorker->UpdateState(ServiceWorkerState::Redundant);
+ aWorker->UpdateRedundantTime();
+ });
+
+ // FIXME: Abort any inflight requests from installing worker.
+
+ ShutdownWorkers();
+ UpdateRegistrationState();
+ NotifyChromeRegistrationListeners();
+ NotifyCleared();
+}
+
+void ServiceWorkerRegistrationInfo::ClearAsCorrupt() {
+ mCorrupt = true;
+ Clear();
+}
+
+bool ServiceWorkerRegistrationInfo::IsCorrupt() const { return mCorrupt; }
+
+ServiceWorkerRegistrationInfo::ServiceWorkerRegistrationInfo(
+ const nsACString& aScope, nsIPrincipal* aPrincipal,
+ ServiceWorkerUpdateViaCache aUpdateViaCache,
+ IPCNavigationPreloadState&& aNavigationPreloadState)
+ : mPrincipal(aPrincipal),
+ mDescriptor(GetNextId(), GetNextVersion(), aPrincipal, aScope,
+ aUpdateViaCache),
+ mControlledClientsCounter(0),
+ mDelayMultiplier(0),
+ mUpdateState(NoUpdate),
+ mCreationTime(PR_Now()),
+ mCreationTimeStamp(TimeStamp::Now()),
+ mLastUpdateTime(0),
+ mUnregistered(false),
+ mCorrupt(false),
+ mNavigationPreloadState(std::move(aNavigationPreloadState)) {
+ MOZ_ASSERT(XRE_GetProcessType() == GeckoProcessType_Default);
+}
+
+ServiceWorkerRegistrationInfo::~ServiceWorkerRegistrationInfo() {
+ MOZ_DIAGNOSTIC_ASSERT(!IsControllingClients());
+}
+
+void ServiceWorkerRegistrationInfo::AddInstance(
+ ServiceWorkerRegistrationListener* aInstance,
+ const ServiceWorkerRegistrationDescriptor& aDescriptor) {
+ MOZ_DIAGNOSTIC_ASSERT(aInstance);
+ MOZ_ASSERT(!mInstanceList.Contains(aInstance));
+ MOZ_DIAGNOSTIC_ASSERT(aDescriptor.Id() == mDescriptor.Id());
+ MOZ_DIAGNOSTIC_ASSERT(aDescriptor.PrincipalInfo() ==
+ mDescriptor.PrincipalInfo());
+ MOZ_DIAGNOSTIC_ASSERT(aDescriptor.Scope() == mDescriptor.Scope());
+ MOZ_DIAGNOSTIC_ASSERT(aDescriptor.Version() <= mDescriptor.Version());
+ uint64_t lastVersion = aDescriptor.Version();
+ for (auto& entry : mVersionList) {
+ if (lastVersion > entry->mDescriptor.Version()) {
+ continue;
+ }
+ lastVersion = entry->mDescriptor.Version();
+ aInstance->UpdateState(entry->mDescriptor);
+ }
+ // Note, the mDescriptor may be contained in the version list. Since the
+ // version list is aged out, though, it may also not be in the version list.
+ // So always check for the mDescriptor update here.
+ if (lastVersion < mDescriptor.Version()) {
+ aInstance->UpdateState(mDescriptor);
+ }
+ mInstanceList.AppendElement(aInstance);
+}
+
+void ServiceWorkerRegistrationInfo::RemoveInstance(
+ ServiceWorkerRegistrationListener* aInstance) {
+ MOZ_DIAGNOSTIC_ASSERT(aInstance);
+ DebugOnly<bool> removed = mInstanceList.RemoveElement(aInstance);
+ MOZ_ASSERT(removed);
+}
+
+const nsCString& ServiceWorkerRegistrationInfo::Scope() const {
+ return mDescriptor.Scope();
+}
+
+nsIPrincipal* ServiceWorkerRegistrationInfo::Principal() const {
+ return mPrincipal;
+}
+
+bool ServiceWorkerRegistrationInfo::IsUnregistered() const {
+ return mUnregistered;
+}
+
+void ServiceWorkerRegistrationInfo::SetUnregistered() {
+#ifdef DEBUG
+ MOZ_ASSERT(!mUnregistered);
+
+ RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance();
+ MOZ_ASSERT(swm);
+
+ RefPtr<ServiceWorkerRegistrationInfo> registration =
+ swm->GetRegistration(Principal(), Scope());
+ MOZ_ASSERT(registration != this);
+#endif
+
+ mUnregistered = true;
+ NotifyChromeRegistrationListeners();
+}
+
+NS_IMPL_ISUPPORTS(ServiceWorkerRegistrationInfo,
+ nsIServiceWorkerRegistrationInfo)
+
+NS_IMETHODIMP
+ServiceWorkerRegistrationInfo::GetPrincipal(nsIPrincipal** aPrincipal) {
+ MOZ_ASSERT(NS_IsMainThread());
+ NS_ADDREF(*aPrincipal = mPrincipal);
+ return NS_OK;
+}
+
+NS_IMETHODIMP ServiceWorkerRegistrationInfo::GetUnregistered(
+ bool* aUnregistered) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aUnregistered);
+ *aUnregistered = mUnregistered;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+ServiceWorkerRegistrationInfo::GetScope(nsAString& aScope) {
+ MOZ_ASSERT(NS_IsMainThread());
+ CopyUTF8toUTF16(Scope(), aScope);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+ServiceWorkerRegistrationInfo::GetScriptSpec(nsAString& aScriptSpec) {
+ MOZ_ASSERT(NS_IsMainThread());
+ RefPtr<ServiceWorkerInfo> newest = NewestIncludingEvaluating();
+ if (newest) {
+ CopyUTF8toUTF16(newest->ScriptSpec(), aScriptSpec);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+ServiceWorkerRegistrationInfo::GetUpdateViaCache(uint16_t* aUpdateViaCache) {
+ *aUpdateViaCache = static_cast<uint16_t>(GetUpdateViaCache());
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+ServiceWorkerRegistrationInfo::GetLastUpdateTime(PRTime* _retval) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(_retval);
+ *_retval = mLastUpdateTime;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+ServiceWorkerRegistrationInfo::GetEvaluatingWorker(
+ nsIServiceWorkerInfo** aResult) {
+ MOZ_ASSERT(NS_IsMainThread());
+ RefPtr<ServiceWorkerInfo> info = mEvaluatingWorker;
+ info.forget(aResult);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+ServiceWorkerRegistrationInfo::GetInstallingWorker(
+ nsIServiceWorkerInfo** aResult) {
+ MOZ_ASSERT(NS_IsMainThread());
+ RefPtr<ServiceWorkerInfo> info = mInstallingWorker;
+ info.forget(aResult);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+ServiceWorkerRegistrationInfo::GetWaitingWorker(
+ nsIServiceWorkerInfo** aResult) {
+ MOZ_ASSERT(NS_IsMainThread());
+ RefPtr<ServiceWorkerInfo> info = mWaitingWorker;
+ info.forget(aResult);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+ServiceWorkerRegistrationInfo::GetActiveWorker(nsIServiceWorkerInfo** aResult) {
+ MOZ_ASSERT(NS_IsMainThread());
+ RefPtr<ServiceWorkerInfo> info = mActiveWorker;
+ info.forget(aResult);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+ServiceWorkerRegistrationInfo::GetQuotaUsageCheckCount(
+ int32_t* aQuotaUsageCheckCount) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aQuotaUsageCheckCount);
+
+ // This value is actually stored on SWM's internal-only
+ // RegistrationDataPerPrincipal structure, but we expose it here for
+ // simplicity for our consumers, so we have to ask SWM to look it up for us.
+ RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance();
+ MOZ_ASSERT(swm);
+
+ *aQuotaUsageCheckCount = swm->GetPrincipalQuotaUsageCheckCount(mPrincipal);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+ServiceWorkerRegistrationInfo::GetWorkerByID(uint64_t aID,
+ nsIServiceWorkerInfo** aResult) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aResult);
+
+ RefPtr<ServiceWorkerInfo> info = GetServiceWorkerInfoById(aID);
+ // It is ok to return null for a missing service worker info.
+ info.forget(aResult);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+ServiceWorkerRegistrationInfo::AddListener(
+ nsIServiceWorkerRegistrationInfoListener* aListener) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (!aListener || mListeners.Contains(aListener)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ mListeners.AppendElement(aListener);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+ServiceWorkerRegistrationInfo::RemoveListener(
+ nsIServiceWorkerRegistrationInfoListener* aListener) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (!aListener || !mListeners.Contains(aListener)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ mListeners.RemoveElement(aListener);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+ServiceWorkerRegistrationInfo::ForceShutdown() {
+ ClearInstalling();
+ ShutdownWorkers();
+ return NS_OK;
+}
+
+already_AddRefed<ServiceWorkerInfo>
+ServiceWorkerRegistrationInfo::GetServiceWorkerInfoById(uint64_t aId) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ RefPtr<ServiceWorkerInfo> serviceWorker;
+ if (mEvaluatingWorker && mEvaluatingWorker->ID() == aId) {
+ serviceWorker = mEvaluatingWorker;
+ } else if (mInstallingWorker && mInstallingWorker->ID() == aId) {
+ serviceWorker = mInstallingWorker;
+ } else if (mWaitingWorker && mWaitingWorker->ID() == aId) {
+ serviceWorker = mWaitingWorker;
+ } else if (mActiveWorker && mActiveWorker->ID() == aId) {
+ serviceWorker = mActiveWorker;
+ }
+
+ return serviceWorker.forget();
+}
+
+void ServiceWorkerRegistrationInfo::TryToActivateAsync(
+ TryToActivateCallback&& aCallback) {
+ MOZ_ALWAYS_SUCCEEDS(NS_DispatchToMainThread(
+ NewRunnableMethod<StoreCopyPassByRRef<TryToActivateCallback>>(
+ "ServiceWorkerRegistrationInfo::TryToActivate", this,
+ &ServiceWorkerRegistrationInfo::TryToActivate,
+ std::move(aCallback))));
+}
+
+/*
+ * TryToActivate should not be called directly, use TryToActivateAsync instead.
+ */
+void ServiceWorkerRegistrationInfo::TryToActivate(
+ TryToActivateCallback&& aCallback) {
+ MOZ_ASSERT(NS_IsMainThread());
+ bool controlling = IsControllingClients();
+ bool skipWaiting = mWaitingWorker && mWaitingWorker->SkipWaitingFlag();
+ bool idle = IsIdle();
+ if (idle && (!controlling || skipWaiting)) {
+ Activate();
+ }
+
+ if (aCallback) {
+ aCallback();
+ }
+}
+
+void ServiceWorkerRegistrationInfo::Activate() {
+ if (!mWaitingWorker) {
+ return;
+ }
+
+ RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance();
+ if (!swm) {
+ // browser shutdown began during async activation step
+ return;
+ }
+
+ TransitionWaitingToActive();
+
+ // FIXME(nsm): Unlink appcache if there is one.
+
+ // "Queue a task to fire a simple event named controllerchange..."
+ MOZ_DIAGNOSTIC_ASSERT(mActiveWorker);
+ swm->UpdateClientControllers(this);
+
+ nsMainThreadPtrHandle<ServiceWorkerRegistrationInfo> handle(
+ new nsMainThreadPtrHolder<ServiceWorkerRegistrationInfo>(
+ "ServiceWorkerRegistrationInfoProxy", this));
+ RefPtr<LifeCycleEventCallback> callback =
+ new ContinueActivateRunnable(handle);
+
+ ServiceWorkerPrivate* workerPrivate = mActiveWorker->WorkerPrivate();
+ MOZ_ASSERT(workerPrivate);
+ nsresult rv = workerPrivate->SendLifeCycleEvent(u"activate"_ns, callback);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ nsCOMPtr<nsIRunnable> failRunnable = NewRunnableMethod<bool>(
+ "dom::ServiceWorkerRegistrationInfo::FinishActivate", this,
+ &ServiceWorkerRegistrationInfo::FinishActivate, false /* success */);
+ MOZ_ALWAYS_SUCCEEDS(NS_DispatchToMainThread(failRunnable.forget()));
+ return;
+ }
+}
+
+void ServiceWorkerRegistrationInfo::FinishActivate(bool aSuccess) {
+ if (mUnregistered || !mActiveWorker ||
+ mActiveWorker->State() != ServiceWorkerState::Activating) {
+ return;
+ }
+
+ // Activation never fails, so aSuccess is ignored.
+ mActiveWorker->UpdateState(ServiceWorkerState::Activated);
+ mActiveWorker->UpdateActivatedTime();
+
+ UpdateRegistrationState();
+ NotifyChromeRegistrationListeners();
+
+ RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance();
+ if (!swm) {
+ // browser shutdown started during async activation completion step
+ return;
+ }
+ swm->StoreRegistration(mPrincipal, this);
+}
+
+void ServiceWorkerRegistrationInfo::RefreshLastUpdateCheckTime() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ mLastUpdateTime =
+ mCreationTime +
+ static_cast<PRTime>(
+ (TimeStamp::Now() - mCreationTimeStamp).ToMicroseconds());
+ NotifyChromeRegistrationListeners();
+}
+
+bool ServiceWorkerRegistrationInfo::IsLastUpdateCheckTimeOverOneDay() const {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ // For testing.
+ if (Preferences::GetBool("dom.serviceWorkers.testUpdateOverOneDay")) {
+ return true;
+ }
+
+ const int64_t kSecondsPerDay = 86400;
+ const int64_t nowMicros =
+ mCreationTime +
+ static_cast<PRTime>(
+ (TimeStamp::Now() - mCreationTimeStamp).ToMicroseconds());
+
+ // now < mLastUpdateTime if the system time is reset between storing
+ // and loading mLastUpdateTime from ServiceWorkerRegistrar.
+ if (nowMicros < mLastUpdateTime ||
+ (nowMicros - mLastUpdateTime) / PR_USEC_PER_SEC > kSecondsPerDay) {
+ return true;
+ }
+ return false;
+}
+
+void ServiceWorkerRegistrationInfo::UpdateRegistrationState() {
+ UpdateRegistrationState(mDescriptor.UpdateViaCache());
+}
+
+void ServiceWorkerRegistrationInfo::UpdateRegistrationState(
+ ServiceWorkerUpdateViaCache aUpdateViaCache) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ TimeStamp oldest = TimeStamp::Now() - TimeDuration::FromSeconds(30);
+ if (!mVersionList.IsEmpty() && mVersionList[0]->mTimeStamp < oldest) {
+ nsTArray<UniquePtr<VersionEntry>> list = std::move(mVersionList);
+ for (auto& entry : list) {
+ if (entry->mTimeStamp >= oldest) {
+ mVersionList.AppendElement(std::move(entry));
+ }
+ }
+ }
+ mVersionList.AppendElement(MakeUnique<VersionEntry>(mDescriptor));
+
+ // We are going to modify the descriptor, so increase its version number.
+ mDescriptor.SetVersion(GetNextVersion());
+
+ // Note, this also sets the new version number on the ServiceWorkerInfo
+ // objects before we copy over their updated descriptors.
+ mDescriptor.SetWorkers(mInstallingWorker, mWaitingWorker, mActiveWorker);
+
+ mDescriptor.SetUpdateViaCache(aUpdateViaCache);
+
+ for (RefPtr<ServiceWorkerRegistrationListener> pinnedTarget :
+ mInstanceList.ForwardRange()) {
+ pinnedTarget->UpdateState(mDescriptor);
+ }
+}
+
+void ServiceWorkerRegistrationInfo::NotifyChromeRegistrationListeners() {
+ nsTArray<nsCOMPtr<nsIServiceWorkerRegistrationInfoListener>> listeners(
+ mListeners.Clone());
+ for (size_t index = 0; index < listeners.Length(); ++index) {
+ listeners[index]->OnChange();
+ }
+}
+
+void ServiceWorkerRegistrationInfo::MaybeScheduleTimeCheckAndUpdate() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance();
+ if (!swm) {
+ // shutting down, do nothing
+ return;
+ }
+
+ if (mUpdateState == NoUpdate) {
+ mUpdateState = NeedTimeCheckAndUpdate;
+ }
+
+ swm->ScheduleUpdateTimer(mPrincipal, Scope());
+}
+
+void ServiceWorkerRegistrationInfo::MaybeScheduleUpdate() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance();
+ if (!swm) {
+ // shutting down, do nothing
+ return;
+ }
+
+ // When reach the navigation fault threshold, calling unregister instead of
+ // scheduling update.
+ if (mActiveWorker && !mUnregistered) {
+ uint32_t navigationFaultCount;
+ mActiveWorker->GetNavigationFaultCount(&navigationFaultCount);
+ const auto navigationFaultThreshold = StaticPrefs::
+ dom_serviceWorkers_mitigations_navigation_fault_threshold();
+ // Disable unregister mitigation when navigation fault threshold is 0.
+ if (navigationFaultThreshold <= navigationFaultCount &&
+ navigationFaultThreshold != 0) {
+ CheckQuotaUsage();
+ swm->Unregister(mPrincipal, nullptr, NS_ConvertUTF8toUTF16(Scope()));
+ return;
+ }
+ }
+
+ mUpdateState = NeedUpdate;
+
+ swm->ScheduleUpdateTimer(mPrincipal, Scope());
+}
+
+bool ServiceWorkerRegistrationInfo::CheckAndClearIfUpdateNeeded() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ bool result =
+ mUpdateState == NeedUpdate || (mUpdateState == NeedTimeCheckAndUpdate &&
+ IsLastUpdateCheckTimeOverOneDay());
+
+ mUpdateState = NoUpdate;
+
+ return result;
+}
+
+ServiceWorkerInfo* ServiceWorkerRegistrationInfo::GetEvaluating() const {
+ MOZ_ASSERT(NS_IsMainThread());
+ return mEvaluatingWorker;
+}
+
+ServiceWorkerInfo* ServiceWorkerRegistrationInfo::GetInstalling() const {
+ MOZ_ASSERT(NS_IsMainThread());
+ return mInstallingWorker;
+}
+
+ServiceWorkerInfo* ServiceWorkerRegistrationInfo::GetWaiting() const {
+ MOZ_ASSERT(NS_IsMainThread());
+ return mWaitingWorker;
+}
+
+ServiceWorkerInfo* ServiceWorkerRegistrationInfo::GetActive() const {
+ MOZ_ASSERT(NS_IsMainThread());
+ return mActiveWorker;
+}
+
+ServiceWorkerInfo* ServiceWorkerRegistrationInfo::GetByDescriptor(
+ const ServiceWorkerDescriptor& aDescriptor) const {
+ if (mActiveWorker && mActiveWorker->Descriptor().Matches(aDescriptor)) {
+ return mActiveWorker;
+ }
+ if (mWaitingWorker && mWaitingWorker->Descriptor().Matches(aDescriptor)) {
+ return mWaitingWorker;
+ }
+ if (mInstallingWorker &&
+ mInstallingWorker->Descriptor().Matches(aDescriptor)) {
+ return mInstallingWorker;
+ }
+ if (mEvaluatingWorker &&
+ mEvaluatingWorker->Descriptor().Matches(aDescriptor)) {
+ return mEvaluatingWorker;
+ }
+ return nullptr;
+}
+
+void ServiceWorkerRegistrationInfo::SetEvaluating(
+ ServiceWorkerInfo* aServiceWorker) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aServiceWorker);
+ MOZ_ASSERT(!mEvaluatingWorker);
+ MOZ_ASSERT(!mInstallingWorker);
+ MOZ_ASSERT(mWaitingWorker != aServiceWorker);
+ MOZ_ASSERT(mActiveWorker != aServiceWorker);
+
+ mEvaluatingWorker = aServiceWorker;
+
+ // We don't call UpdateRegistrationState() here because the evaluating worker
+ // is currently not exposed to content on the registration, so calling it here
+ // would produce redundant IPC traffic.
+ NotifyChromeRegistrationListeners();
+}
+
+void ServiceWorkerRegistrationInfo::ClearEvaluating() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (!mEvaluatingWorker) {
+ return;
+ }
+
+ mEvaluatingWorker->UpdateState(ServiceWorkerState::Redundant);
+ // We don't update the redundant time for the sw here, since we've not expose
+ // evalutingWorker yet.
+ mEvaluatingWorker = nullptr;
+
+ // As for SetEvaluating, UpdateRegistrationState() does not need to be called.
+ NotifyChromeRegistrationListeners();
+}
+
+void ServiceWorkerRegistrationInfo::ClearInstalling() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (!mInstallingWorker) {
+ return;
+ }
+
+ RefPtr<ServiceWorkerInfo> installing = std::move(mInstallingWorker);
+ installing->UpdateState(ServiceWorkerState::Redundant);
+ installing->UpdateRedundantTime();
+
+ UpdateRegistrationState();
+ NotifyChromeRegistrationListeners();
+}
+
+void ServiceWorkerRegistrationInfo::TransitionEvaluatingToInstalling() {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(mEvaluatingWorker);
+ MOZ_ASSERT(!mInstallingWorker);
+
+ mInstallingWorker = std::move(mEvaluatingWorker);
+ mInstallingWorker->UpdateState(ServiceWorkerState::Installing);
+
+ UpdateRegistrationState();
+ NotifyChromeRegistrationListeners();
+}
+
+void ServiceWorkerRegistrationInfo::TransitionInstallingToWaiting() {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(mInstallingWorker);
+
+ if (mWaitingWorker) {
+ MOZ_ASSERT(mInstallingWorker->CacheName() != mWaitingWorker->CacheName());
+ mWaitingWorker->UpdateState(ServiceWorkerState::Redundant);
+ mWaitingWorker->UpdateRedundantTime();
+ }
+
+ mWaitingWorker = std::move(mInstallingWorker);
+ mWaitingWorker->UpdateState(ServiceWorkerState::Installed);
+ mWaitingWorker->UpdateInstalledTime();
+
+ UpdateRegistrationState();
+ NotifyChromeRegistrationListeners();
+
+ // TODO: When bug 1426401 is implemented we will need to call
+ // StoreRegistration() here to persist the waiting worker.
+}
+
+void ServiceWorkerRegistrationInfo::SetActive(
+ ServiceWorkerInfo* aServiceWorker) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aServiceWorker);
+
+ // TODO: Assert installing, waiting, and active are nullptr once the SWM
+ // moves to the parent process. After that happens this code will
+ // only run for browser initialization and not for cross-process
+ // overrides.
+ MOZ_ASSERT(mInstallingWorker != aServiceWorker);
+ MOZ_ASSERT(mWaitingWorker != aServiceWorker);
+ MOZ_ASSERT(mActiveWorker != aServiceWorker);
+
+ if (mActiveWorker) {
+ MOZ_ASSERT(aServiceWorker->CacheName() != mActiveWorker->CacheName());
+ mActiveWorker->UpdateState(ServiceWorkerState::Redundant);
+ mActiveWorker->UpdateRedundantTime();
+ }
+
+ // The active worker is being overriden due to initial load or
+ // another process activating a worker. Move straight to the
+ // Activated state.
+ mActiveWorker = aServiceWorker;
+ mActiveWorker->SetActivateStateUncheckedWithoutEvent(
+ ServiceWorkerState::Activated);
+
+ // We don't need to update activated time when we load registration from
+ // registrar.
+ UpdateRegistrationState();
+ NotifyChromeRegistrationListeners();
+}
+
+void ServiceWorkerRegistrationInfo::TransitionWaitingToActive() {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(mWaitingWorker);
+
+ if (mActiveWorker) {
+ MOZ_ASSERT(mWaitingWorker->CacheName() != mActiveWorker->CacheName());
+ mActiveWorker->UpdateState(ServiceWorkerState::Redundant);
+ mActiveWorker->UpdateRedundantTime();
+ }
+
+ // We are transitioning from waiting to active normally, so go to
+ // the activating state.
+ mActiveWorker = std::move(mWaitingWorker);
+ mActiveWorker->UpdateState(ServiceWorkerState::Activating);
+
+ nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction(
+ "ServiceWorkerRegistrationInfo::TransitionWaitingToActive", [] {
+ RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance();
+ if (swm) {
+ swm->CheckPendingReadyPromises();
+ }
+ });
+ MOZ_ALWAYS_SUCCEEDS(
+ SchedulerGroup::Dispatch(TaskCategory::Other, r.forget()));
+
+ UpdateRegistrationState();
+ NotifyChromeRegistrationListeners();
+}
+
+bool ServiceWorkerRegistrationInfo::IsIdle() const {
+ return !mActiveWorker || mActiveWorker->WorkerPrivate()->IsIdle();
+}
+
+ServiceWorkerUpdateViaCache ServiceWorkerRegistrationInfo::GetUpdateViaCache()
+ const {
+ return mDescriptor.UpdateViaCache();
+}
+
+void ServiceWorkerRegistrationInfo::SetUpdateViaCache(
+ ServiceWorkerUpdateViaCache aUpdateViaCache) {
+ UpdateRegistrationState(aUpdateViaCache);
+}
+
+int64_t ServiceWorkerRegistrationInfo::GetLastUpdateTime() const {
+ return mLastUpdateTime;
+}
+
+void ServiceWorkerRegistrationInfo::SetLastUpdateTime(const int64_t aTime) {
+ if (aTime == 0) {
+ return;
+ }
+
+ mLastUpdateTime = aTime;
+}
+
+const ServiceWorkerRegistrationDescriptor&
+ServiceWorkerRegistrationInfo::Descriptor() const {
+ return mDescriptor;
+}
+
+uint64_t ServiceWorkerRegistrationInfo::Id() const { return mDescriptor.Id(); }
+
+uint64_t ServiceWorkerRegistrationInfo::Version() const {
+ return mDescriptor.Version();
+}
+
+uint32_t ServiceWorkerRegistrationInfo::GetUpdateDelay(
+ const bool aWithMultiplier) {
+ uint32_t delay = Preferences::GetInt("dom.serviceWorkers.update_delay", 1000);
+
+ if (!aWithMultiplier) {
+ return delay;
+ }
+
+ // This can potentially happen if you spam registration->Update(). We don't
+ // want to wrap to a lower value.
+ if (mDelayMultiplier >= INT_MAX / (delay ? delay : 1)) {
+ return INT_MAX;
+ }
+
+ delay *= mDelayMultiplier;
+
+ if (!mControlledClientsCounter && mDelayMultiplier < (INT_MAX / 30)) {
+ mDelayMultiplier = (mDelayMultiplier ? mDelayMultiplier : 1) * 30;
+ }
+
+ return delay;
+}
+
+void ServiceWorkerRegistrationInfo::FireUpdateFound() {
+ for (RefPtr<ServiceWorkerRegistrationListener> pinnedTarget :
+ mInstanceList.ForwardRange()) {
+ pinnedTarget->FireUpdateFound();
+ }
+}
+
+void ServiceWorkerRegistrationInfo::NotifyCleared() {
+ for (RefPtr<ServiceWorkerRegistrationListener> pinnedTarget :
+ mInstanceList.ForwardRange()) {
+ pinnedTarget->RegistrationCleared();
+ }
+}
+
+void ServiceWorkerRegistrationInfo::ClearWhenIdle() {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(IsUnregistered());
+ MOZ_ASSERT(!IsControllingClients());
+ MOZ_ASSERT(!IsIdle(), "Already idle!");
+
+ RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance();
+ MOZ_ASSERT(swm);
+
+ swm->AddOrphanedRegistration(this);
+
+ /**
+ * Although a Service Worker will transition to idle many times during its
+ * lifetime, the promise is only resolved once `GetIdlePromise` has been
+ * called, populating the `MozPromiseHolder`. Additionally, this is the only
+ * time this method will be called for the given ServiceWorker. This means we
+ * will be notified to the transition we are interested in, and there are no
+ * other callers to get confused.
+ *
+ * Note that because we are using `MozPromise`, our callback will be invoked
+ * as a separate task, so there is a small potential for races in the event
+ * code if things are still holding onto the ServiceWorker binding and using
+ * `postMessage()` or other mechanisms to schedule new events on it, which
+ * would make it non-idle. However, this is a race inherent in the spec which
+ * does not deal with the reality of multiple threads in "Try Clear
+ * Registration".
+ */
+ GetActive()->WorkerPrivate()->GetIdlePromise()->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [self = RefPtr<ServiceWorkerRegistrationInfo>(this)](
+ const GenericPromise::ResolveOrRejectValue& aResult) {
+ MOZ_ASSERT(aResult.IsResolve());
+ // This registration was already unregistered and not controlling
+ // clients when `ClearWhenIdle` was called, so there should be no way
+ // that more clients were acquired.
+ MOZ_ASSERT(!self->IsControllingClients());
+ MOZ_ASSERT(self->IsIdle());
+ self->Clear();
+
+ RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance();
+ if (swm) {
+ swm->RemoveOrphanedRegistration(self);
+ }
+ });
+}
+
+const nsID& ServiceWorkerRegistrationInfo::AgentClusterId() const {
+ return mAgentClusterId;
+}
+
+void ServiceWorkerRegistrationInfo::SetNavigationPreloadEnabled(
+ const bool& aEnabled) {
+ MOZ_ASSERT(NS_IsMainThread());
+ mNavigationPreloadState.enabled() = aEnabled;
+}
+
+void ServiceWorkerRegistrationInfo::SetNavigationPreloadHeader(
+ const nsCString& aHeader) {
+ MOZ_ASSERT(NS_IsMainThread());
+ mNavigationPreloadState.headerValue() = aHeader;
+}
+
+IPCNavigationPreloadState
+ServiceWorkerRegistrationInfo::GetNavigationPreloadState() const {
+ MOZ_ASSERT(NS_IsMainThread());
+ return mNavigationPreloadState;
+}
+
+// static
+uint64_t ServiceWorkerRegistrationInfo::GetNextId() {
+ MOZ_ASSERT(NS_IsMainThread());
+ static uint64_t sNextId = 0;
+ return ++sNextId;
+}
+
+// static
+uint64_t ServiceWorkerRegistrationInfo::GetNextVersion() {
+ MOZ_ASSERT(NS_IsMainThread());
+ static uint64_t sNextVersion = 0;
+ return ++sNextVersion;
+}
+
+void ServiceWorkerRegistrationInfo::ForEachWorker(
+ void (*aFunc)(RefPtr<ServiceWorkerInfo>&)) {
+ if (mEvaluatingWorker) {
+ aFunc(mEvaluatingWorker);
+ }
+
+ if (mInstallingWorker) {
+ aFunc(mInstallingWorker);
+ }
+
+ if (mWaitingWorker) {
+ aFunc(mWaitingWorker);
+ }
+
+ if (mActiveWorker) {
+ aFunc(mActiveWorker);
+ }
+}
+
+void ServiceWorkerRegistrationInfo::CheckQuotaUsage() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance();
+ MOZ_ASSERT(swm);
+
+ swm->CheckPrincipalQuotaUsage(mPrincipal, Scope());
+}
+
+} // namespace mozilla::dom
diff --git a/dom/serviceworkers/ServiceWorkerRegistrationInfo.h b/dom/serviceworkers/ServiceWorkerRegistrationInfo.h
new file mode 100644
index 0000000000..b8bd75cf71
--- /dev/null
+++ b/dom/serviceworkers/ServiceWorkerRegistrationInfo.h
@@ -0,0 +1,268 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_serviceworkerregistrationinfo_h
+#define mozilla_dom_serviceworkerregistrationinfo_h
+
+#include <functional>
+
+#include "mozilla/dom/IPCNavigationPreloadState.h"
+#include "mozilla/dom/ServiceWorkerInfo.h"
+#include "mozilla/dom/ServiceWorkerRegistrationBinding.h"
+#include "mozilla/dom/ServiceWorkerRegistrationDescriptor.h"
+#include "nsProxyRelease.h"
+#include "nsTObserverArray.h"
+
+namespace mozilla::dom {
+
+class ServiceWorkerRegistrationListener;
+
+class ServiceWorkerRegistrationInfo final
+ : public nsIServiceWorkerRegistrationInfo {
+ nsCOMPtr<nsIPrincipal> mPrincipal;
+ ServiceWorkerRegistrationDescriptor mDescriptor;
+ nsTArray<nsCOMPtr<nsIServiceWorkerRegistrationInfoListener>> mListeners;
+ nsTObserverArray<ServiceWorkerRegistrationListener*> mInstanceList;
+
+ struct VersionEntry {
+ const ServiceWorkerRegistrationDescriptor mDescriptor;
+ TimeStamp mTimeStamp;
+
+ explicit VersionEntry(
+ const ServiceWorkerRegistrationDescriptor& aDescriptor)
+ : mDescriptor(aDescriptor), mTimeStamp(TimeStamp::Now()) {}
+ };
+ nsTArray<UniquePtr<VersionEntry>> mVersionList;
+
+ const nsID mAgentClusterId = nsID::GenerateUUID();
+
+ uint32_t mControlledClientsCounter;
+ uint32_t mDelayMultiplier;
+
+ enum { NoUpdate, NeedTimeCheckAndUpdate, NeedUpdate } mUpdateState;
+
+ // Timestamp to track SWR's last update time
+ PRTime mCreationTime;
+ TimeStamp mCreationTimeStamp;
+ // The time of update is 0, if SWR've never been updated yet.
+ PRTime mLastUpdateTime;
+
+ RefPtr<ServiceWorkerInfo> mEvaluatingWorker;
+ RefPtr<ServiceWorkerInfo> mActiveWorker;
+ RefPtr<ServiceWorkerInfo> mWaitingWorker;
+ RefPtr<ServiceWorkerInfo> mInstallingWorker;
+
+ virtual ~ServiceWorkerRegistrationInfo();
+
+ // When unregister() is called on a registration, it is removed from the
+ // "scope to registration map" but not immediately "cleared" (i.e. its workers
+ // terminated, updated to the redundant state, etc.) because it may still be
+ // controlling clients. It is marked as unregistered and when all controlled
+ // clients go away, cleared. This way we can tell if a registration
+ // is unregistered by querying the object itself rather than incurring a table
+ // lookup (in the case when the registrations are passed around as pointers).
+ bool mUnregistered;
+
+ bool mCorrupt;
+
+ IPCNavigationPreloadState mNavigationPreloadState;
+
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSISERVICEWORKERREGISTRATIONINFO
+
+ using TryToActivateCallback = std::function<void()>;
+
+ ServiceWorkerRegistrationInfo(
+ const nsACString& aScope, nsIPrincipal* aPrincipal,
+ ServiceWorkerUpdateViaCache aUpdateViaCache,
+ IPCNavigationPreloadState&& aNavigationPreloadState);
+
+ void AddInstance(ServiceWorkerRegistrationListener* aInstance,
+ const ServiceWorkerRegistrationDescriptor& aDescriptor);
+
+ void RemoveInstance(ServiceWorkerRegistrationListener* aInstance);
+
+ const nsCString& Scope() const;
+
+ nsIPrincipal* Principal() const;
+
+ bool IsUnregistered() const;
+
+ void SetUnregistered();
+
+ already_AddRefed<ServiceWorkerInfo> Newest() const {
+ RefPtr<ServiceWorkerInfo> newest;
+ if (mInstallingWorker) {
+ newest = mInstallingWorker;
+ } else if (mWaitingWorker) {
+ newest = mWaitingWorker;
+ } else {
+ newest = mActiveWorker;
+ }
+
+ return newest.forget();
+ }
+
+ already_AddRefed<ServiceWorkerInfo> NewestIncludingEvaluating() const {
+ if (mEvaluatingWorker) {
+ RefPtr<ServiceWorkerInfo> newest = mEvaluatingWorker;
+ return newest.forget();
+ }
+ return Newest();
+ }
+
+ already_AddRefed<ServiceWorkerInfo> GetServiceWorkerInfoById(uint64_t aId);
+
+ void StartControllingClient() {
+ ++mControlledClientsCounter;
+ mDelayMultiplier = 0;
+ }
+
+ void StopControllingClient() {
+ MOZ_ASSERT(mControlledClientsCounter);
+ --mControlledClientsCounter;
+ }
+
+ bool IsControllingClients() const {
+ return mActiveWorker && mControlledClientsCounter;
+ }
+
+ // As a side effect, this nullifies
+ // `m{Evaluating,Installing,Waiting,Active}Worker`s.
+ void ShutdownWorkers();
+
+ void Clear();
+
+ void ClearAsCorrupt();
+
+ bool IsCorrupt() const;
+
+ void TryToActivateAsync(TryToActivateCallback&& aCallback = nullptr);
+
+ void TryToActivate(TryToActivateCallback&& aCallback);
+
+ void Activate();
+
+ void FinishActivate(bool aSuccess);
+
+ void RefreshLastUpdateCheckTime();
+
+ bool IsLastUpdateCheckTimeOverOneDay() const;
+
+ void MaybeScheduleTimeCheckAndUpdate();
+
+ void MaybeScheduleUpdate();
+
+ bool CheckAndClearIfUpdateNeeded();
+
+ ServiceWorkerInfo* GetEvaluating() const;
+
+ ServiceWorkerInfo* GetInstalling() const;
+
+ ServiceWorkerInfo* GetWaiting() const;
+
+ ServiceWorkerInfo* GetActive() const;
+
+ ServiceWorkerInfo* GetByDescriptor(
+ const ServiceWorkerDescriptor& aDescriptor) const;
+
+ // Set the given worker as the evaluating service worker. The worker
+ // state is not changed.
+ void SetEvaluating(ServiceWorkerInfo* aServiceWorker);
+
+ // Remove an existing evaluating worker, if present. The worker will
+ // be transitioned to the Redundant state.
+ void ClearEvaluating();
+
+ // Remove an existing installing worker, if present. The worker will
+ // be transitioned to the Redundant state.
+ void ClearInstalling();
+
+ // Transition the current evaluating worker to be the installing worker. The
+ // worker's state is update to Installing.
+ void TransitionEvaluatingToInstalling();
+
+ // Transition the current installing worker to be the waiting worker. The
+ // worker's state is updated to Installed.
+ void TransitionInstallingToWaiting();
+
+ // Override the current active worker. This is used during browser
+ // initialization to load persisted workers. Its also used to propagate
+ // active workers across child processes in e10s. This second use will
+ // go away once the ServiceWorkerManager moves to the parent process.
+ // The worker is transitioned to the Activated state.
+ void SetActive(ServiceWorkerInfo* aServiceWorker);
+
+ // Transition the current waiting worker to be the new active worker. The
+ // worker is updated to the Activating state.
+ void TransitionWaitingToActive();
+
+ // Determine if the registration is actively performing work.
+ bool IsIdle() const;
+
+ ServiceWorkerUpdateViaCache GetUpdateViaCache() const;
+
+ void SetUpdateViaCache(ServiceWorkerUpdateViaCache aUpdateViaCache);
+
+ int64_t GetLastUpdateTime() const;
+
+ void SetLastUpdateTime(const int64_t aTime);
+
+ const ServiceWorkerRegistrationDescriptor& Descriptor() const;
+
+ uint64_t Id() const;
+
+ uint64_t Version() const;
+
+ uint32_t GetUpdateDelay(const bool aWithMultiplier = true);
+
+ void FireUpdateFound();
+
+ void NotifyCleared();
+
+ void ClearWhenIdle();
+
+ const nsID& AgentClusterId() const;
+
+ void SetNavigationPreloadEnabled(const bool& aEnabled);
+
+ void SetNavigationPreloadHeader(const nsCString& aHeader);
+
+ IPCNavigationPreloadState GetNavigationPreloadState() const;
+
+ private:
+ // Roughly equivalent to [[Update Registration State algorithm]]. Make sure
+ // this is called *before* updating SW instances' state, otherwise they
+ // may get CC-ed.
+ void UpdateRegistrationState();
+
+ void UpdateRegistrationState(ServiceWorkerUpdateViaCache aUpdateViaCache);
+
+ // Used by devtools to track changes to the properties of
+ // *nsIServiceWorkerRegistrationInfo*. Note, this doesn't necessarily need to
+ // be in sync with the DOM registration objects, but it does need to be called
+ // in the same task that changed |mInstallingWorker|, |mWaitingWorker| or
+ // |mActiveWorker|.
+ void NotifyChromeRegistrationListeners();
+
+ static uint64_t GetNextId();
+
+ static uint64_t GetNextVersion();
+
+ // `aFunc`'s argument will be a reference to
+ // `m{Evaluating,Installing,Waiting,Active}Worker` (not to copy of them).
+ // Additionally, a null check will be performed for each worker before each
+ // call to `aFunc`, so `aFunc` will always get a reference to a non-null
+ // pointer.
+ void ForEachWorker(void (*aFunc)(RefPtr<ServiceWorkerInfo>&));
+
+ void CheckQuotaUsage();
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_serviceworkerregistrationinfo_h
diff --git a/dom/serviceworkers/ServiceWorkerRegistrationListener.h b/dom/serviceworkers/ServiceWorkerRegistrationListener.h
new file mode 100644
index 0000000000..2b90f049a8
--- /dev/null
+++ b/dom/serviceworkers/ServiceWorkerRegistrationListener.h
@@ -0,0 +1,35 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_ServiceWorkerRegistrationListener_h
+#define mozilla_dom_ServiceWorkerRegistrationListener_h
+
+namespace mozilla::dom {
+
+class ServiceWorkerRegistrationDescriptor;
+
+// Used by ServiceWorkerManager to notify ServiceWorkerRegistrations of
+// updatefound event and invalidating ServiceWorker instances.
+class ServiceWorkerRegistrationListener {
+ public:
+ NS_INLINE_DECL_PURE_VIRTUAL_REFCOUNTING
+
+ virtual void UpdateState(
+ const ServiceWorkerRegistrationDescriptor& aDescriptor) = 0;
+
+ virtual void FireUpdateFound() = 0;
+
+ virtual void RegistrationCleared() = 0;
+
+ virtual void GetScope(nsAString& aScope) const = 0;
+
+ virtual bool MatchesDescriptor(
+ const ServiceWorkerRegistrationDescriptor& aDescriptor) = 0;
+};
+
+} // namespace mozilla::dom
+
+#endif /* mozilla_dom_ServiceWorkerRegistrationListener_h */
diff --git a/dom/serviceworkers/ServiceWorkerRegistrationParent.cpp b/dom/serviceworkers/ServiceWorkerRegistrationParent.cpp
new file mode 100644
index 0000000000..d90bec3514
--- /dev/null
+++ b/dom/serviceworkers/ServiceWorkerRegistrationParent.cpp
@@ -0,0 +1,152 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "ServiceWorkerRegistrationParent.h"
+
+#include <utility>
+
+#include "ServiceWorkerRegistrationProxy.h"
+
+namespace mozilla::dom {
+
+using mozilla::ipc::IPCResult;
+
+void ServiceWorkerRegistrationParent::ActorDestroy(ActorDestroyReason aReason) {
+ if (mProxy) {
+ mProxy->RevokeActor(this);
+ mProxy = nullptr;
+ }
+}
+
+IPCResult ServiceWorkerRegistrationParent::RecvTeardown() {
+ MaybeSendDelete();
+ return IPC_OK();
+}
+
+namespace {
+
+void ResolveUnregister(
+ PServiceWorkerRegistrationParent::UnregisterResolver&& aResolver,
+ bool aSuccess, nsresult aRv) {
+ aResolver(Tuple<const bool&, const CopyableErrorResult&>(
+ aSuccess, CopyableErrorResult(aRv)));
+}
+
+} // anonymous namespace
+
+IPCResult ServiceWorkerRegistrationParent::RecvUnregister(
+ UnregisterResolver&& aResolver) {
+ if (!mProxy) {
+ ResolveUnregister(std::move(aResolver), false,
+ NS_ERROR_DOM_INVALID_STATE_ERR);
+ return IPC_OK();
+ }
+
+ mProxy->Unregister()->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [aResolver](bool aSuccess) mutable {
+ ResolveUnregister(std::move(aResolver), aSuccess, NS_OK);
+ },
+ [aResolver](nsresult aRv) mutable {
+ ResolveUnregister(std::move(aResolver), false, aRv);
+ });
+
+ return IPC_OK();
+}
+
+IPCResult ServiceWorkerRegistrationParent::RecvUpdate(
+ const nsACString& aNewestWorkerScriptUrl, UpdateResolver&& aResolver) {
+ if (!mProxy) {
+ aResolver(CopyableErrorResult(NS_ERROR_DOM_INVALID_STATE_ERR));
+ return IPC_OK();
+ }
+
+ mProxy->Update(aNewestWorkerScriptUrl)
+ ->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [aResolver](const ServiceWorkerRegistrationDescriptor& aDescriptor) {
+ aResolver(aDescriptor.ToIPC());
+ },
+ [aResolver](const CopyableErrorResult& aResult) {
+ aResolver(aResult);
+ });
+
+ return IPC_OK();
+}
+
+IPCResult ServiceWorkerRegistrationParent::RecvSetNavigationPreloadEnabled(
+ const bool& aEnabled, SetNavigationPreloadEnabledResolver&& aResolver) {
+ if (!mProxy) {
+ aResolver(false);
+ return IPC_OK();
+ }
+
+ mProxy->SetNavigationPreloadEnabled(aEnabled)->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [aResolver](bool) { aResolver(true); },
+ [aResolver](nsresult) { aResolver(false); });
+
+ return IPC_OK();
+}
+
+IPCResult ServiceWorkerRegistrationParent::RecvSetNavigationPreloadHeader(
+ const nsACString& aHeader, SetNavigationPreloadHeaderResolver&& aResolver) {
+ if (!mProxy) {
+ aResolver(false);
+ return IPC_OK();
+ }
+
+ mProxy->SetNavigationPreloadHeader(aHeader)->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [aResolver](bool) { aResolver(true); },
+ [aResolver](nsresult) { aResolver(false); });
+
+ return IPC_OK();
+}
+
+IPCResult ServiceWorkerRegistrationParent::RecvGetNavigationPreloadState(
+ GetNavigationPreloadStateResolver&& aResolver) {
+ if (!mProxy) {
+ aResolver(Nothing());
+ return IPC_OK();
+ }
+
+ mProxy->GetNavigationPreloadState()->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [aResolver](const IPCNavigationPreloadState& aState) {
+ aResolver(Some(aState));
+ },
+ [aResolver](const CopyableErrorResult& aResult) {
+ aResolver(Nothing());
+ });
+
+ return IPC_OK();
+}
+
+ServiceWorkerRegistrationParent::ServiceWorkerRegistrationParent()
+ : mDeleteSent(false) {}
+
+ServiceWorkerRegistrationParent::~ServiceWorkerRegistrationParent() {
+ MOZ_DIAGNOSTIC_ASSERT(!mProxy);
+}
+
+void ServiceWorkerRegistrationParent::Init(
+ const IPCServiceWorkerRegistrationDescriptor& aDescriptor) {
+ MOZ_DIAGNOSTIC_ASSERT(!mProxy);
+ mProxy = new ServiceWorkerRegistrationProxy(
+ ServiceWorkerRegistrationDescriptor(aDescriptor));
+ mProxy->Init(this);
+}
+
+void ServiceWorkerRegistrationParent::MaybeSendDelete() {
+ if (mDeleteSent) {
+ return;
+ }
+ mDeleteSent = true;
+ Unused << Send__delete__(this);
+}
+
+} // namespace mozilla::dom
diff --git a/dom/serviceworkers/ServiceWorkerRegistrationParent.h b/dom/serviceworkers/ServiceWorkerRegistrationParent.h
new file mode 100644
index 0000000000..5a6d751961
--- /dev/null
+++ b/dom/serviceworkers/ServiceWorkerRegistrationParent.h
@@ -0,0 +1,58 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_serviceworkerregistrationparent_h__
+#define mozilla_dom_serviceworkerregistrationparent_h__
+
+#include "mozilla/dom/PServiceWorkerRegistrationParent.h"
+
+namespace mozilla::dom {
+
+class IPCServiceWorkerRegistrationDescriptor;
+class ServiceWorkerRegistrationProxy;
+
+class ServiceWorkerRegistrationParent final
+ : public PServiceWorkerRegistrationParent {
+ RefPtr<ServiceWorkerRegistrationProxy> mProxy;
+ bool mDeleteSent;
+
+ ~ServiceWorkerRegistrationParent();
+
+ // PServiceWorkerRegistrationParent
+ void ActorDestroy(ActorDestroyReason aReason) override;
+
+ mozilla::ipc::IPCResult RecvTeardown() override;
+
+ mozilla::ipc::IPCResult RecvUnregister(
+ UnregisterResolver&& aResolver) override;
+
+ mozilla::ipc::IPCResult RecvUpdate(const nsACString& aNewestWorkerScriptUrl,
+ UpdateResolver&& aResolver) override;
+
+ mozilla::ipc::IPCResult RecvSetNavigationPreloadEnabled(
+ const bool& aEnabled,
+ SetNavigationPreloadEnabledResolver&& aResolver) override;
+
+ mozilla::ipc::IPCResult RecvSetNavigationPreloadHeader(
+ const nsACString& aHeader,
+ SetNavigationPreloadHeaderResolver&& aResolver) override;
+
+ mozilla::ipc::IPCResult RecvGetNavigationPreloadState(
+ GetNavigationPreloadStateResolver&& aResolver) override;
+
+ public:
+ NS_INLINE_DECL_REFCOUNTING(ServiceWorkerRegistrationParent, override);
+
+ ServiceWorkerRegistrationParent();
+
+ void Init(const IPCServiceWorkerRegistrationDescriptor& aDescriptor);
+
+ void MaybeSendDelete();
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_serviceworkerregistrationparent_h__
diff --git a/dom/serviceworkers/ServiceWorkerRegistrationProxy.cpp b/dom/serviceworkers/ServiceWorkerRegistrationProxy.cpp
new file mode 100644
index 0000000000..39f845bb56
--- /dev/null
+++ b/dom/serviceworkers/ServiceWorkerRegistrationProxy.cpp
@@ -0,0 +1,490 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "ServiceWorkerRegistrationProxy.h"
+
+#include "mozilla/SchedulerGroup.h"
+#include "mozilla/ScopeExit.h"
+#include "mozilla/ipc/BackgroundParent.h"
+#include "ServiceWorkerManager.h"
+#include "ServiceWorkerRegistrationParent.h"
+#include "ServiceWorkerUnregisterCallback.h"
+
+namespace mozilla::dom {
+
+using mozilla::ipc::AssertIsOnBackgroundThread;
+
+class ServiceWorkerRegistrationProxy::DelayedUpdate final
+ : public nsITimerCallback,
+ public nsINamed {
+ RefPtr<ServiceWorkerRegistrationProxy> mProxy;
+ RefPtr<ServiceWorkerRegistrationPromise::Private> mPromise;
+ nsCOMPtr<nsITimer> mTimer;
+ nsCString mNewestWorkerScriptUrl;
+
+ ~DelayedUpdate() = default;
+
+ public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSITIMERCALLBACK
+ NS_DECL_NSINAMED
+
+ DelayedUpdate(RefPtr<ServiceWorkerRegistrationProxy>&& aProxy,
+ RefPtr<ServiceWorkerRegistrationPromise::Private>&& aPromise,
+ nsCString&& aNewestWorkerScriptUrl, uint32_t delay);
+
+ void ChainTo(RefPtr<ServiceWorkerRegistrationPromise::Private> aPromise);
+
+ void Reject();
+
+ void SetNewestWorkerScriptUrl(nsCString&& aNewestWorkerScriptUrl);
+};
+
+ServiceWorkerRegistrationProxy::~ServiceWorkerRegistrationProxy() {
+ // Any thread
+ MOZ_DIAGNOSTIC_ASSERT(!mActor);
+ MOZ_DIAGNOSTIC_ASSERT(!mReg);
+}
+
+void ServiceWorkerRegistrationProxy::MaybeShutdownOnBGThread() {
+ AssertIsOnBackgroundThread();
+ if (!mActor) {
+ return;
+ }
+ mActor->MaybeSendDelete();
+}
+
+void ServiceWorkerRegistrationProxy::UpdateStateOnBGThread(
+ const ServiceWorkerRegistrationDescriptor& aDescriptor) {
+ AssertIsOnBackgroundThread();
+ if (!mActor) {
+ return;
+ }
+ Unused << mActor->SendUpdateState(aDescriptor.ToIPC());
+}
+
+void ServiceWorkerRegistrationProxy::FireUpdateFoundOnBGThread() {
+ AssertIsOnBackgroundThread();
+ if (!mActor) {
+ return;
+ }
+ Unused << mActor->SendFireUpdateFound();
+}
+
+void ServiceWorkerRegistrationProxy::InitOnMainThread() {
+ AssertIsOnMainThread();
+
+ auto scopeExit = MakeScopeExit([&] { MaybeShutdownOnMainThread(); });
+
+ RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance();
+ NS_ENSURE_TRUE_VOID(swm);
+
+ RefPtr<ServiceWorkerRegistrationInfo> reg =
+ swm->GetRegistration(mDescriptor.PrincipalInfo(), mDescriptor.Scope());
+ NS_ENSURE_TRUE_VOID(reg);
+
+ if (reg->Id() != mDescriptor.Id()) {
+ // This registration has already been replaced by another one.
+ return;
+ }
+
+ scopeExit.release();
+
+ mReg = new nsMainThreadPtrHolder<ServiceWorkerRegistrationInfo>(
+ "ServiceWorkerRegistrationProxy::mInfo", reg);
+
+ mReg->AddInstance(this, mDescriptor);
+}
+
+void ServiceWorkerRegistrationProxy::MaybeShutdownOnMainThread() {
+ AssertIsOnMainThread();
+
+ if (mDelayedUpdate) {
+ mDelayedUpdate->Reject();
+ mDelayedUpdate = nullptr;
+ }
+ nsCOMPtr<nsIRunnable> r = NewRunnableMethod(
+ __func__, this, &ServiceWorkerRegistrationProxy::MaybeShutdownOnBGThread);
+
+ MOZ_ALWAYS_SUCCEEDS(mEventTarget->Dispatch(r.forget(), NS_DISPATCH_NORMAL));
+}
+
+void ServiceWorkerRegistrationProxy::StopListeningOnMainThread() {
+ AssertIsOnMainThread();
+
+ if (!mReg) {
+ return;
+ }
+
+ mReg->RemoveInstance(this);
+ mReg = nullptr;
+}
+
+void ServiceWorkerRegistrationProxy::UpdateState(
+ const ServiceWorkerRegistrationDescriptor& aDescriptor) {
+ AssertIsOnMainThread();
+
+ if (mDescriptor == aDescriptor) {
+ return;
+ }
+ mDescriptor = aDescriptor;
+
+ nsCOMPtr<nsIRunnable> r =
+ NewRunnableMethod<ServiceWorkerRegistrationDescriptor>(
+ __func__, this,
+ &ServiceWorkerRegistrationProxy::UpdateStateOnBGThread, aDescriptor);
+
+ MOZ_ALWAYS_SUCCEEDS(mEventTarget->Dispatch(r.forget(), NS_DISPATCH_NORMAL));
+}
+
+void ServiceWorkerRegistrationProxy::FireUpdateFound() {
+ AssertIsOnMainThread();
+
+ nsCOMPtr<nsIRunnable> r = NewRunnableMethod(
+ __func__, this,
+ &ServiceWorkerRegistrationProxy::FireUpdateFoundOnBGThread);
+
+ MOZ_ALWAYS_SUCCEEDS(mEventTarget->Dispatch(r.forget(), NS_DISPATCH_NORMAL));
+}
+
+void ServiceWorkerRegistrationProxy::RegistrationCleared() {
+ MaybeShutdownOnMainThread();
+}
+
+void ServiceWorkerRegistrationProxy::GetScope(nsAString& aScope) const {
+ CopyUTF8toUTF16(mDescriptor.Scope(), aScope);
+}
+
+bool ServiceWorkerRegistrationProxy::MatchesDescriptor(
+ const ServiceWorkerRegistrationDescriptor& aDescriptor) {
+ AssertIsOnMainThread();
+ return aDescriptor.Id() == mDescriptor.Id() &&
+ aDescriptor.PrincipalInfo() == mDescriptor.PrincipalInfo() &&
+ aDescriptor.Scope() == mDescriptor.Scope();
+}
+
+ServiceWorkerRegistrationProxy::ServiceWorkerRegistrationProxy(
+ const ServiceWorkerRegistrationDescriptor& aDescriptor)
+ : mEventTarget(GetCurrentSerialEventTarget()), mDescriptor(aDescriptor) {}
+
+void ServiceWorkerRegistrationProxy::Init(
+ ServiceWorkerRegistrationParent* aActor) {
+ AssertIsOnBackgroundThread();
+ MOZ_DIAGNOSTIC_ASSERT(aActor);
+ MOZ_DIAGNOSTIC_ASSERT(!mActor);
+ MOZ_DIAGNOSTIC_ASSERT(mEventTarget);
+
+ mActor = aActor;
+
+ // Note, this must be done from a separate Init() method and not in
+ // the constructor. If done from the constructor the runnable can
+ // execute, complete, and release its reference before the constructor
+ // returns.
+ nsCOMPtr<nsIRunnable> r =
+ NewRunnableMethod("ServiceWorkerRegistrationProxy::Init", this,
+ &ServiceWorkerRegistrationProxy::InitOnMainThread);
+ MOZ_ALWAYS_SUCCEEDS(
+ SchedulerGroup::Dispatch(TaskCategory::Other, r.forget()));
+}
+
+void ServiceWorkerRegistrationProxy::RevokeActor(
+ ServiceWorkerRegistrationParent* aActor) {
+ AssertIsOnBackgroundThread();
+ MOZ_DIAGNOSTIC_ASSERT(mActor);
+ MOZ_DIAGNOSTIC_ASSERT(mActor == aActor);
+ mActor = nullptr;
+
+ nsCOMPtr<nsIRunnable> r = NewRunnableMethod(
+ __func__, this,
+ &ServiceWorkerRegistrationProxy::StopListeningOnMainThread);
+ MOZ_ALWAYS_SUCCEEDS(
+ SchedulerGroup::Dispatch(TaskCategory::Other, r.forget()));
+}
+
+RefPtr<GenericPromise> ServiceWorkerRegistrationProxy::Unregister() {
+ AssertIsOnBackgroundThread();
+
+ RefPtr<ServiceWorkerRegistrationProxy> self = this;
+ RefPtr<GenericPromise::Private> promise =
+ new GenericPromise::Private(__func__);
+
+ nsCOMPtr<nsIRunnable> r =
+ NS_NewRunnableFunction(__func__, [self, promise]() mutable {
+ nsresult rv = NS_ERROR_DOM_INVALID_STATE_ERR;
+ auto scopeExit = MakeScopeExit([&] { promise->Reject(rv, __func__); });
+
+ NS_ENSURE_TRUE_VOID(self->mReg);
+
+ RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance();
+ NS_ENSURE_TRUE_VOID(swm);
+
+ RefPtr<UnregisterCallback> cb = new UnregisterCallback(promise);
+
+ rv = swm->Unregister(self->mReg->Principal(), cb,
+ NS_ConvertUTF8toUTF16(self->mReg->Scope()));
+ NS_ENSURE_SUCCESS_VOID(rv);
+
+ scopeExit.release();
+ });
+
+ MOZ_ALWAYS_SUCCEEDS(
+ SchedulerGroup::Dispatch(TaskCategory::Other, r.forget()));
+
+ return promise;
+}
+
+namespace {
+
+class UpdateCallback final : public ServiceWorkerUpdateFinishCallback {
+ RefPtr<ServiceWorkerRegistrationPromise::Private> mPromise;
+
+ ~UpdateCallback() = default;
+
+ public:
+ explicit UpdateCallback(
+ RefPtr<ServiceWorkerRegistrationPromise::Private>&& aPromise)
+ : mPromise(std::move(aPromise)) {
+ MOZ_DIAGNOSTIC_ASSERT(mPromise);
+ }
+
+ void UpdateSucceeded(ServiceWorkerRegistrationInfo* aInfo) override {
+ mPromise->Resolve(aInfo->Descriptor(), __func__);
+ }
+
+ void UpdateFailed(ErrorResult& aResult) override {
+ mPromise->Reject(CopyableErrorResult(aResult), __func__);
+ }
+};
+
+} // anonymous namespace
+
+NS_IMPL_ISUPPORTS(ServiceWorkerRegistrationProxy::DelayedUpdate,
+ nsITimerCallback, nsINamed)
+
+ServiceWorkerRegistrationProxy::DelayedUpdate::DelayedUpdate(
+ RefPtr<ServiceWorkerRegistrationProxy>&& aProxy,
+ RefPtr<ServiceWorkerRegistrationPromise::Private>&& aPromise,
+ nsCString&& aNewestWorkerScriptUrl, uint32_t delay)
+ : mProxy(std::move(aProxy)),
+ mPromise(std::move(aPromise)),
+ mNewestWorkerScriptUrl(std::move(aNewestWorkerScriptUrl)) {
+ MOZ_DIAGNOSTIC_ASSERT(mProxy);
+ MOZ_DIAGNOSTIC_ASSERT(mPromise);
+ MOZ_ASSERT(!mNewestWorkerScriptUrl.IsEmpty());
+ mProxy->mDelayedUpdate = this;
+ Result<nsCOMPtr<nsITimer>, nsresult> result =
+ NS_NewTimerWithCallback(this, delay, nsITimer::TYPE_ONE_SHOT);
+ mTimer = result.unwrapOr(nullptr);
+ MOZ_DIAGNOSTIC_ASSERT(mTimer);
+}
+
+void ServiceWorkerRegistrationProxy::DelayedUpdate::ChainTo(
+ RefPtr<ServiceWorkerRegistrationPromise::Private> aPromise) {
+ AssertIsOnMainThread();
+ MOZ_ASSERT(mProxy->mDelayedUpdate == this);
+ MOZ_ASSERT(mPromise);
+
+ mPromise->ChainTo(aPromise.forget(), __func__);
+}
+
+void ServiceWorkerRegistrationProxy::DelayedUpdate::Reject() {
+ MOZ_DIAGNOSTIC_ASSERT(mPromise);
+ if (mTimer) {
+ mTimer->Cancel();
+ mTimer = nullptr;
+ }
+ mPromise->Reject(NS_ERROR_DOM_INVALID_STATE_ERR, __func__);
+}
+
+void ServiceWorkerRegistrationProxy::DelayedUpdate::SetNewestWorkerScriptUrl(
+ nsCString&& aNewestWorkerScriptUrl) {
+ MOZ_ASSERT(NS_IsMainThread());
+ mNewestWorkerScriptUrl = std::move(aNewestWorkerScriptUrl);
+}
+
+NS_IMETHODIMP
+ServiceWorkerRegistrationProxy::DelayedUpdate::Notify(nsITimer* aTimer) {
+ // Already shutting down.
+ if (mProxy->mDelayedUpdate != this) {
+ return NS_OK;
+ }
+
+ auto scopeExit = MakeScopeExit(
+ [&] { mPromise->Reject(NS_ERROR_DOM_INVALID_STATE_ERR, __func__); });
+
+ NS_ENSURE_TRUE(mProxy->mReg, NS_ERROR_FAILURE);
+
+ RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance();
+ NS_ENSURE_TRUE(swm, NS_ERROR_FAILURE);
+
+ RefPtr<UpdateCallback> cb = new UpdateCallback(std::move(mPromise));
+ swm->Update(mProxy->mReg->Principal(), mProxy->mReg->Scope(),
+ std::move(mNewestWorkerScriptUrl), cb);
+
+ mTimer = nullptr;
+ mProxy->mDelayedUpdate = nullptr;
+
+ scopeExit.release();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+ServiceWorkerRegistrationProxy::DelayedUpdate::GetName(nsACString& aName) {
+ aName.AssignLiteral("ServiceWorkerRegistrationProxy::DelayedUpdate");
+ return NS_OK;
+}
+
+RefPtr<ServiceWorkerRegistrationPromise> ServiceWorkerRegistrationProxy::Update(
+ const nsACString& aNewestWorkerScriptUrl) {
+ AssertIsOnBackgroundThread();
+
+ RefPtr<ServiceWorkerRegistrationProxy> self = this;
+ RefPtr<ServiceWorkerRegistrationPromise::Private> promise =
+ new ServiceWorkerRegistrationPromise::Private(__func__);
+
+ nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction(
+ __func__,
+ [self, promise,
+ newestWorkerScriptUrl = nsCString(aNewestWorkerScriptUrl)]() mutable {
+ auto scopeExit = MakeScopeExit(
+ [&] { promise->Reject(NS_ERROR_DOM_INVALID_STATE_ERR, __func__); });
+
+ // Get the delay value for the update
+ NS_ENSURE_TRUE_VOID(self->mReg);
+ uint32_t delay = self->mReg->GetUpdateDelay(false);
+
+ // If the delay value does not equal to 0, create a timer and a timer
+ // callback to perform the delayed update. Otherwise, update directly.
+ if (delay) {
+ if (self->mDelayedUpdate) {
+ // NOTE: if we `ChainTo(),` there will ultimately be a single
+ // update, and this update will resolve all promises that were
+ // issued while the update's timer was ticking down.
+ self->mDelayedUpdate->ChainTo(std::move(promise));
+
+ // Use the "newest newest worker"'s script URL.
+ self->mDelayedUpdate->SetNewestWorkerScriptUrl(
+ std::move(newestWorkerScriptUrl));
+ } else {
+ RefPtr<ServiceWorkerRegistrationProxy::DelayedUpdate> du =
+ new ServiceWorkerRegistrationProxy::DelayedUpdate(
+ std::move(self), std::move(promise),
+ std::move(newestWorkerScriptUrl), delay);
+ }
+ } else {
+ RefPtr<ServiceWorkerManager> swm =
+ ServiceWorkerManager::GetInstance();
+ NS_ENSURE_TRUE_VOID(swm);
+
+ RefPtr<UpdateCallback> cb = new UpdateCallback(std::move(promise));
+ swm->Update(self->mReg->Principal(), self->mReg->Scope(),
+ std::move(newestWorkerScriptUrl), cb);
+ }
+ scopeExit.release();
+ });
+
+ MOZ_ALWAYS_SUCCEEDS(
+ SchedulerGroup::Dispatch(TaskCategory::Other, r.forget()));
+
+ return promise;
+}
+
+RefPtr<GenericPromise>
+ServiceWorkerRegistrationProxy::SetNavigationPreloadEnabled(
+ const bool& aEnabled) {
+ AssertIsOnBackgroundThread();
+
+ RefPtr<ServiceWorkerRegistrationProxy> self = this;
+ RefPtr<GenericPromise::Private> promise =
+ new GenericPromise::Private(__func__);
+
+ nsCOMPtr<nsIRunnable> r =
+ NS_NewRunnableFunction(__func__, [aEnabled, self, promise]() mutable {
+ nsresult rv = NS_ERROR_DOM_INVALID_STATE_ERR;
+ auto scopeExit = MakeScopeExit([&] { promise->Reject(rv, __func__); });
+
+ NS_ENSURE_TRUE_VOID(self->mReg);
+ NS_ENSURE_TRUE_VOID(self->mReg->GetActive());
+
+ auto reg = self->mReg;
+ reg->SetNavigationPreloadEnabled(aEnabled);
+
+ RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance();
+ NS_ENSURE_TRUE_VOID(swm);
+ swm->StoreRegistration(reg->Principal(), reg);
+
+ scopeExit.release();
+
+ promise->Resolve(true, __func__);
+ });
+
+ MOZ_ALWAYS_SUCCEEDS(
+ SchedulerGroup::Dispatch(TaskCategory::Other, r.forget()));
+
+ return promise;
+}
+
+RefPtr<GenericPromise>
+ServiceWorkerRegistrationProxy::SetNavigationPreloadHeader(
+ const nsACString& aHeader) {
+ AssertIsOnBackgroundThread();
+
+ RefPtr<ServiceWorkerRegistrationProxy> self = this;
+ RefPtr<GenericPromise::Private> promise =
+ new GenericPromise::Private(__func__);
+
+ nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction(
+ __func__, [aHeader = nsCString(aHeader), self, promise]() mutable {
+ nsresult rv = NS_ERROR_DOM_INVALID_STATE_ERR;
+ auto scopeExit = MakeScopeExit([&] { promise->Reject(rv, __func__); });
+
+ NS_ENSURE_TRUE_VOID(self->mReg);
+ NS_ENSURE_TRUE_VOID(self->mReg->GetActive());
+
+ auto reg = self->mReg;
+ reg->SetNavigationPreloadHeader(aHeader);
+
+ RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance();
+ NS_ENSURE_TRUE_VOID(swm);
+ swm->StoreRegistration(reg->Principal(), reg);
+
+ scopeExit.release();
+
+ promise->Resolve(true, __func__);
+ });
+
+ MOZ_ALWAYS_SUCCEEDS(
+ SchedulerGroup::Dispatch(TaskCategory::Other, r.forget()));
+
+ return promise;
+}
+
+RefPtr<NavigationPreloadStatePromise>
+ServiceWorkerRegistrationProxy::GetNavigationPreloadState() {
+ AssertIsOnBackgroundThread();
+
+ RefPtr<ServiceWorkerRegistrationProxy> self = this;
+ RefPtr<NavigationPreloadStatePromise::Private> promise =
+ new NavigationPreloadStatePromise::Private(__func__);
+
+ nsCOMPtr<nsIRunnable> r =
+ NS_NewRunnableFunction(__func__, [self, promise]() mutable {
+ nsresult rv = NS_ERROR_DOM_INVALID_STATE_ERR;
+ auto scopeExit = MakeScopeExit([&] { promise->Reject(rv, __func__); });
+
+ NS_ENSURE_TRUE_VOID(self->mReg);
+ scopeExit.release();
+
+ promise->Resolve(self->mReg->GetNavigationPreloadState(), __func__);
+ });
+
+ MOZ_ALWAYS_SUCCEEDS(
+ SchedulerGroup::Dispatch(TaskCategory::Other, r.forget()));
+
+ return promise;
+}
+
+} // namespace mozilla::dom
diff --git a/dom/serviceworkers/ServiceWorkerRegistrationProxy.h b/dom/serviceworkers/ServiceWorkerRegistrationProxy.h
new file mode 100644
index 0000000000..4253d2f259
--- /dev/null
+++ b/dom/serviceworkers/ServiceWorkerRegistrationProxy.h
@@ -0,0 +1,92 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef moz_dom_ServiceWorkerRegistrationProxy_h
+#define moz_dom_ServiceWorkerRegistrationProxy_h
+
+#include "mozilla/dom/PServiceWorkerRegistrationParent.h"
+#include "nsProxyRelease.h"
+#include "ServiceWorkerRegistrationDescriptor.h"
+#include "ServiceWorkerRegistrationListener.h"
+#include "ServiceWorkerUtils.h"
+
+namespace mozilla::dom {
+
+class ServiceWorkerRegistrationInfo;
+class ServiceWorkerRegistrationParent;
+
+class ServiceWorkerRegistrationProxy final
+ : public ServiceWorkerRegistrationListener {
+ // Background thread only
+ RefPtr<ServiceWorkerRegistrationParent> mActor;
+
+ // Written on background thread and read on main thread
+ nsCOMPtr<nsISerialEventTarget> mEventTarget;
+
+ // Main thread only
+ ServiceWorkerRegistrationDescriptor mDescriptor;
+ nsMainThreadPtrHandle<ServiceWorkerRegistrationInfo> mReg;
+
+ ~ServiceWorkerRegistrationProxy();
+
+ // Background thread methods
+ void MaybeShutdownOnBGThread();
+
+ void UpdateStateOnBGThread(
+ const ServiceWorkerRegistrationDescriptor& aDescriptor);
+
+ void FireUpdateFoundOnBGThread();
+
+ // Main thread methods
+ void InitOnMainThread();
+
+ void MaybeShutdownOnMainThread();
+
+ void StopListeningOnMainThread();
+
+ // The timer callback to perform the delayed update
+ class DelayedUpdate;
+ RefPtr<DelayedUpdate> mDelayedUpdate;
+
+ // ServiceWorkerRegistrationListener interface
+ void UpdateState(
+ const ServiceWorkerRegistrationDescriptor& aDescriptor) override;
+
+ void FireUpdateFound() override;
+
+ void RegistrationCleared() override;
+
+ void GetScope(nsAString& aScope) const override;
+
+ bool MatchesDescriptor(
+ const ServiceWorkerRegistrationDescriptor& aDescriptor) override;
+
+ public:
+ explicit ServiceWorkerRegistrationProxy(
+ const ServiceWorkerRegistrationDescriptor& aDescriptor);
+
+ void Init(ServiceWorkerRegistrationParent* aActor);
+
+ void RevokeActor(ServiceWorkerRegistrationParent* aActor);
+
+ RefPtr<GenericPromise> Unregister();
+
+ RefPtr<ServiceWorkerRegistrationPromise> Update(
+ const nsACString& aNewestWorkerScriptUrl);
+
+ RefPtr<GenericPromise> SetNavigationPreloadEnabled(const bool& aEnabled);
+
+ RefPtr<GenericPromise> SetNavigationPreloadHeader(const nsACString& aHeader);
+
+ RefPtr<NavigationPreloadStatePromise> GetNavigationPreloadState();
+
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(ServiceWorkerRegistrationProxy,
+ override);
+};
+
+} // namespace mozilla::dom
+
+#endif // moz_dom_ServiceWorkerRegistrationProxy_h
diff --git a/dom/serviceworkers/ServiceWorkerScriptCache.cpp b/dom/serviceworkers/ServiceWorkerScriptCache.cpp
new file mode 100644
index 0000000000..77a51b2427
--- /dev/null
+++ b/dom/serviceworkers/ServiceWorkerScriptCache.cpp
@@ -0,0 +1,1503 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "ServiceWorkerScriptCache.h"
+
+#include "js/Array.h" // JS::GetArrayLength
+#include "js/PropertyAndElement.h" // JS_GetElement
+#include "mozilla/Unused.h"
+#include "mozilla/dom/CacheBinding.h"
+#include "mozilla/dom/cache/CacheStorage.h"
+#include "mozilla/dom/cache/Cache.h"
+#include "mozilla/dom/Promise.h"
+#include "mozilla/dom/PromiseWorkerProxy.h"
+#include "mozilla/dom/ScriptLoader.h"
+#include "mozilla/dom/WorkerCommon.h"
+#include "mozilla/ipc/BackgroundUtils.h"
+#include "mozilla/ipc/PBackgroundSharedTypes.h"
+#include "mozilla/net/CookieJarSettings.h"
+#include "mozilla/ScopeExit.h"
+#include "mozilla/StaticPrefs_extensions.h"
+#include "nsICacheInfoChannel.h"
+#include "nsIHttpChannel.h"
+#include "nsIStreamLoader.h"
+#include "nsIThreadRetargetableRequest.h"
+#include "nsIUUIDGenerator.h"
+#include "nsIXPConnect.h"
+
+#include "nsIInputStreamPump.h"
+#include "nsIPrincipal.h"
+#include "nsIScriptSecurityManager.h"
+#include "nsContentUtils.h"
+#include "nsNetUtil.h"
+#include "ServiceWorkerManager.h"
+#include "nsStringStream.h"
+
+using mozilla::dom::cache::Cache;
+using mozilla::dom::cache::CacheStorage;
+using mozilla::ipc::PrincipalInfo;
+
+namespace mozilla::dom::serviceWorkerScriptCache {
+
+namespace {
+
+already_AddRefed<CacheStorage> CreateCacheStorage(JSContext* aCx,
+ nsIPrincipal* aPrincipal,
+ ErrorResult& aRv) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aPrincipal);
+
+ nsIXPConnect* xpc = nsContentUtils::XPConnect();
+ MOZ_ASSERT(xpc, "This should never be null!");
+ JS::Rooted<JSObject*> sandbox(aCx);
+ aRv = xpc->CreateSandbox(aCx, aPrincipal, sandbox.address());
+ if (NS_WARN_IF(aRv.Failed())) {
+ return nullptr;
+ }
+
+ // This is called when the JSContext is not in a realm, so CreateSandbox
+ // returned an unwrapped global.
+ MOZ_ASSERT(JS_IsGlobalObject(sandbox));
+
+ nsCOMPtr<nsIGlobalObject> sandboxGlobalObject = xpc::NativeGlobal(sandbox);
+ if (!sandboxGlobalObject) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return nullptr;
+ }
+
+ // We assume private browsing is not enabled here. The ScriptLoader
+ // explicitly fails for private browsing so there should never be
+ // a service worker running in private browsing mode. Therefore if
+ // we are purging scripts or running a comparison algorithm we cannot
+ // be in private browsing.
+ //
+ // Also, bypass the CacheStorage trusted origin checks. The ServiceWorker
+ // has validated the origin prior to this point. All the information
+ // to revalidate is not available now.
+ return CacheStorage::CreateOnMainThread(cache::CHROME_ONLY_NAMESPACE,
+ sandboxGlobalObject, aPrincipal,
+ true /* force trusted origin */, aRv);
+}
+
+class CompareManager;
+class CompareCache;
+
+// This class downloads a URL from the network, compare the downloaded script
+// with an existing cache if provided, and report to CompareManager via calling
+// ComparisonFinished().
+class CompareNetwork final : public nsIStreamLoaderObserver,
+ public nsIRequestObserver {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSISTREAMLOADEROBSERVER
+ NS_DECL_NSIREQUESTOBSERVER
+
+ CompareNetwork(CompareManager* aManager,
+ ServiceWorkerRegistrationInfo* aRegistration,
+ bool aIsMainScript)
+ : mManager(aManager),
+ mRegistration(aRegistration),
+ mInternalHeaders(new InternalHeaders()),
+ mLoadFlags(nsIChannel::LOAD_BYPASS_SERVICE_WORKER),
+ mState(WaitingForInitialization),
+ mNetworkResult(NS_OK),
+ mCacheResult(NS_OK),
+ mIsMainScript(aIsMainScript),
+ mIsFromCache(false) {
+ MOZ_ASSERT(aManager);
+ MOZ_ASSERT(NS_IsMainThread());
+ }
+
+ nsresult Initialize(nsIPrincipal* aPrincipal, const nsAString& aURL,
+ Cache* const aCache);
+
+ void Abort();
+
+ void NetworkFinish(nsresult aRv);
+
+ void CacheFinish(nsresult aRv);
+
+ const nsString& URL() const {
+ MOZ_ASSERT(NS_IsMainThread());
+ return mURL;
+ }
+
+ const nsString& Buffer() const {
+ MOZ_ASSERT(NS_IsMainThread());
+ return mBuffer;
+ }
+
+ const ChannelInfo& GetChannelInfo() const { return mChannelInfo; }
+
+ already_AddRefed<InternalHeaders> GetInternalHeaders() const {
+ RefPtr<InternalHeaders> internalHeaders = mInternalHeaders;
+ return internalHeaders.forget();
+ }
+
+ UniquePtr<PrincipalInfo> TakePrincipalInfo() {
+ return std::move(mPrincipalInfo);
+ }
+
+ bool Succeeded() const { return NS_SUCCEEDED(mNetworkResult); }
+
+ const nsTArray<nsCString>& URLList() const { return mURLList; }
+
+ private:
+ ~CompareNetwork() {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(!mCC);
+ }
+
+ void Finish();
+
+ nsresult SetPrincipalInfo(nsIChannel* aChannel);
+
+ RefPtr<CompareManager> mManager;
+ RefPtr<CompareCache> mCC;
+ RefPtr<ServiceWorkerRegistrationInfo> mRegistration;
+
+ nsCOMPtr<nsIChannel> mChannel;
+ nsString mBuffer;
+ nsString mURL;
+ ChannelInfo mChannelInfo;
+ RefPtr<InternalHeaders> mInternalHeaders;
+ UniquePtr<PrincipalInfo> mPrincipalInfo;
+ nsTArray<nsCString> mURLList;
+
+ nsCString mMaxScope;
+ nsLoadFlags mLoadFlags;
+
+ enum {
+ WaitingForInitialization,
+ WaitingForBothFinished,
+ WaitingForNetworkFinished,
+ WaitingForCacheFinished,
+ Finished
+ } mState;
+
+ nsresult mNetworkResult;
+ nsresult mCacheResult;
+
+ const bool mIsMainScript;
+ bool mIsFromCache;
+};
+
+NS_IMPL_ISUPPORTS(CompareNetwork, nsIStreamLoaderObserver, nsIRequestObserver)
+
+// This class gets a cached Response from the CacheStorage and then it calls
+// CacheFinish() in the CompareNetwork.
+class CompareCache final : public PromiseNativeHandler,
+ public nsIStreamLoaderObserver {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSISTREAMLOADEROBSERVER
+
+ explicit CompareCache(CompareNetwork* aCN)
+ : mCN(aCN), mState(WaitingForInitialization), mInCache(false) {
+ MOZ_ASSERT(aCN);
+ MOZ_ASSERT(NS_IsMainThread());
+ }
+
+ nsresult Initialize(Cache* const aCache, const nsAString& aURL);
+
+ void Finish(nsresult aStatus, bool aInCache);
+
+ void Abort();
+
+ virtual void ResolvedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue,
+ ErrorResult& aRv) override;
+
+ virtual void RejectedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue,
+ ErrorResult& aRv) override;
+
+ const nsString& Buffer() const {
+ MOZ_ASSERT(NS_IsMainThread());
+ return mBuffer;
+ }
+
+ bool InCache() { return mInCache; }
+
+ private:
+ ~CompareCache() { MOZ_ASSERT(NS_IsMainThread()); }
+
+ void ManageValueResult(JSContext* aCx, JS::Handle<JS::Value> aValue);
+
+ RefPtr<CompareNetwork> mCN;
+ nsCOMPtr<nsIInputStreamPump> mPump;
+
+ nsString mURL;
+ nsString mBuffer;
+
+ enum {
+ WaitingForInitialization,
+ WaitingForScript,
+ Finished,
+ } mState;
+
+ bool mInCache;
+};
+
+NS_IMPL_ISUPPORTS(CompareCache, nsIStreamLoaderObserver)
+
+class CompareManager final : public PromiseNativeHandler {
+ public:
+ NS_DECL_ISUPPORTS
+
+ explicit CompareManager(ServiceWorkerRegistrationInfo* aRegistration,
+ CompareCallback* aCallback)
+ : mRegistration(aRegistration),
+ mCallback(aCallback),
+ mLoadFlags(nsIChannel::LOAD_BYPASS_SERVICE_WORKER),
+ mState(WaitingForInitialization),
+ mPendingCount(0),
+ mOnFailure(OnFailure::DoNothing),
+ mAreScriptsEqual(true) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aRegistration);
+ }
+
+ nsresult Initialize(nsIPrincipal* aPrincipal, const nsAString& aURL,
+ const nsAString& aCacheName);
+
+ void ResolvedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue,
+ ErrorResult& aRv) override;
+
+ void RejectedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue,
+ ErrorResult& aRv) override;
+
+ CacheStorage* CacheStorage_() {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(mCacheStorage);
+ return mCacheStorage;
+ }
+
+ void ComparisonFinished(nsresult aStatus, bool aIsMainScript, bool aIsEqual,
+ const nsACString& aMaxScope, nsLoadFlags aLoadFlags) {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (mState == Finished) {
+ return;
+ }
+
+ MOZ_DIAGNOSTIC_ASSERT(mState == WaitingForScriptOrComparisonResult);
+
+ if (NS_WARN_IF(NS_FAILED(aStatus))) {
+ Fail(aStatus);
+ return;
+ }
+
+ mAreScriptsEqual = mAreScriptsEqual && aIsEqual;
+
+ if (aIsMainScript) {
+ mMaxScope = aMaxScope;
+ mLoadFlags = aLoadFlags;
+ }
+
+ // Check whether all CompareNetworks finished their jobs.
+ MOZ_DIAGNOSTIC_ASSERT(mPendingCount > 0);
+ if (--mPendingCount) {
+ return;
+ }
+
+ if (mAreScriptsEqual) {
+ MOZ_ASSERT(mCallback);
+ mCallback->ComparisonResult(aStatus, true /* aSameScripts */, mOnFailure,
+ u""_ns, mMaxScope, mLoadFlags);
+ Cleanup();
+ return;
+ }
+
+ // Write to Cache so ScriptLoader reads succeed.
+ WriteNetworkBufferToNewCache();
+ }
+
+ private:
+ ~CompareManager() {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(mCNList.Length() == 0);
+ }
+
+ void Fail(nsresult aStatus);
+
+ void Cleanup();
+
+ nsresult FetchScript(const nsAString& aURL, bool aIsMainScript,
+ Cache* const aCache = nullptr) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ MOZ_DIAGNOSTIC_ASSERT(mState == WaitingForInitialization ||
+ mState == WaitingForScriptOrComparisonResult);
+
+ RefPtr<CompareNetwork> cn =
+ new CompareNetwork(this, mRegistration, aIsMainScript);
+ mCNList.AppendElement(cn);
+ mPendingCount += 1;
+
+ nsresult rv = cn->Initialize(mPrincipal, aURL, aCache);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ return NS_OK;
+ }
+
+ void ManageOldCache(JSContext* aCx, JS::Handle<JS::Value> aValue) {
+ MOZ_DIAGNOSTIC_ASSERT(mState == WaitingForExistingOpen);
+
+ // RAII Cleanup when fails.
+ nsresult rv = NS_ERROR_FAILURE;
+ auto guard = MakeScopeExit([&] { Fail(rv); });
+
+ if (NS_WARN_IF(!aValue.isObject())) {
+ return;
+ }
+
+ MOZ_ASSERT(!mOldCache);
+ JS::Rooted<JSObject*> obj(aCx, &aValue.toObject());
+ if (NS_WARN_IF(!obj) ||
+ NS_WARN_IF(NS_FAILED(UNWRAP_OBJECT(Cache, obj, mOldCache)))) {
+ return;
+ }
+
+ Optional<RequestOrUSVString> request;
+ CacheQueryOptions options;
+ ErrorResult error;
+ RefPtr<Promise> promise = mOldCache->Keys(aCx, request, options, error);
+ if (NS_WARN_IF(error.Failed())) {
+ // No exception here because there are no ReadableStreams involved here.
+ MOZ_ASSERT(!error.IsJSException());
+ rv = error.StealNSResult();
+ return;
+ }
+
+ mState = WaitingForExistingKeys;
+ promise->AppendNativeHandler(this);
+ guard.release();
+ }
+
+ void ManageOldKeys(JSContext* aCx, JS::Handle<JS::Value> aValue) {
+ MOZ_DIAGNOSTIC_ASSERT(mState == WaitingForExistingKeys);
+
+ // RAII Cleanup when fails.
+ nsresult rv = NS_ERROR_FAILURE;
+ auto guard = MakeScopeExit([&] { Fail(rv); });
+
+ if (NS_WARN_IF(!aValue.isObject())) {
+ return;
+ }
+
+ JS::Rooted<JSObject*> obj(aCx, &aValue.toObject());
+ if (NS_WARN_IF(!obj)) {
+ return;
+ }
+
+ uint32_t len = 0;
+ if (!JS::GetArrayLength(aCx, obj, &len)) {
+ return;
+ }
+
+ // Fetch and compare the source scripts.
+ MOZ_ASSERT(mPendingCount == 0);
+
+ mState = WaitingForScriptOrComparisonResult;
+
+ bool hasMainScript = false;
+ AutoTArray<nsString, 8> urlList;
+
+ // Extract the list of URLs in the old cache.
+ for (uint32_t i = 0; i < len; ++i) {
+ JS::Rooted<JS::Value> val(aCx);
+ if (NS_WARN_IF(!JS_GetElement(aCx, obj, i, &val)) ||
+ NS_WARN_IF(!val.isObject())) {
+ return;
+ }
+
+ Request* request;
+ JS::Rooted<JSObject*> requestObj(aCx, &val.toObject());
+ if (NS_WARN_IF(NS_FAILED(UNWRAP_OBJECT(Request, &requestObj, request)))) {
+ return;
+ };
+
+ nsString url;
+ request->GetUrl(url);
+
+ if (!hasMainScript && url == mURL) {
+ hasMainScript = true;
+ }
+
+ urlList.AppendElement(url);
+ }
+
+ // If the main script is missing, then something has gone wrong. We
+ // will try to continue with the update process to trigger a new
+ // installation. If that fails, however, then uninstall the registration
+ // because it is broken in a way that cannot be fixed.
+ if (!hasMainScript) {
+ mOnFailure = OnFailure::Uninstall;
+ }
+
+ // Always make sure to fetch the main script. If the old cache has
+ // no entries or the main script entry is missing, then the loop below
+ // may not trigger it. This should not really happen, but we handle it
+ // gracefully if it does occur. Its possible the bad cache state is due
+ // to a crash or shutdown during an update, etc.
+ rv = FetchScript(mURL, true /* aIsMainScript */, mOldCache);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+
+ for (const auto& url : urlList) {
+ // We explicitly start the fetch for the main script above.
+ if (mURL == url) {
+ continue;
+ }
+
+ rv = FetchScript(url, false /* aIsMainScript */, mOldCache);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+ }
+
+ guard.release();
+ }
+
+ void ManageNewCache(JSContext* aCx, JS::Handle<JS::Value> aValue) {
+ MOZ_DIAGNOSTIC_ASSERT(mState == WaitingForOpen);
+
+ // RAII Cleanup when fails.
+ nsresult rv = NS_ERROR_FAILURE;
+ auto guard = MakeScopeExit([&] { Fail(rv); });
+
+ if (NS_WARN_IF(!aValue.isObject())) {
+ return;
+ }
+
+ JS::Rooted<JSObject*> obj(aCx, &aValue.toObject());
+ if (NS_WARN_IF(!obj)) {
+ return;
+ }
+
+ Cache* cache = nullptr;
+ rv = UNWRAP_OBJECT(Cache, &obj, cache);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+
+ // Just to be safe.
+ RefPtr<Cache> kungfuDeathGrip = cache;
+
+ MOZ_ASSERT(mPendingCount == 0);
+ for (uint32_t i = 0; i < mCNList.Length(); ++i) {
+ // We bail out immediately when something goes wrong.
+ rv = WriteToCache(aCx, cache, mCNList[i]);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+ }
+
+ mState = WaitingForPut;
+ guard.release();
+ }
+
+ void WriteNetworkBufferToNewCache() {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(mCNList.Length() != 0);
+ MOZ_ASSERT(mCacheStorage);
+ MOZ_ASSERT(mNewCacheName.IsEmpty());
+
+ ErrorResult result;
+ result = serviceWorkerScriptCache::GenerateCacheName(mNewCacheName);
+ if (NS_WARN_IF(result.Failed())) {
+ MOZ_ASSERT(!result.IsErrorWithMessage());
+ Fail(result.StealNSResult());
+ return;
+ }
+
+ RefPtr<Promise> cacheOpenPromise =
+ mCacheStorage->Open(mNewCacheName, result);
+ if (NS_WARN_IF(result.Failed())) {
+ MOZ_ASSERT(!result.IsErrorWithMessage());
+ Fail(result.StealNSResult());
+ return;
+ }
+
+ mState = WaitingForOpen;
+ cacheOpenPromise->AppendNativeHandler(this);
+ }
+
+ nsresult WriteToCache(JSContext* aCx, Cache* aCache, CompareNetwork* aCN) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aCache);
+ MOZ_ASSERT(aCN);
+ MOZ_DIAGNOSTIC_ASSERT(mState == WaitingForOpen);
+
+ // We don't have to save any information from a failed CompareNetwork.
+ if (!aCN->Succeeded()) {
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIInputStream> body;
+ nsresult rv = NS_NewCStringInputStream(
+ getter_AddRefs(body), NS_ConvertUTF16toUTF8(aCN->Buffer()));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ SafeRefPtr<InternalResponse> ir =
+ MakeSafeRefPtr<InternalResponse>(200, "OK"_ns);
+ ir->SetBody(body, aCN->Buffer().Length());
+ ir->SetURLList(aCN->URLList());
+
+ ir->InitChannelInfo(aCN->GetChannelInfo());
+ UniquePtr<PrincipalInfo> principalInfo = aCN->TakePrincipalInfo();
+ if (principalInfo) {
+ ir->SetPrincipalInfo(std::move(principalInfo));
+ }
+
+ RefPtr<InternalHeaders> internalHeaders = aCN->GetInternalHeaders();
+ ir->Headers()->Fill(*(internalHeaders.get()), IgnoreErrors());
+
+ RefPtr<Response> response =
+ new Response(aCache->GetGlobalObject(), std::move(ir), nullptr);
+
+ RequestOrUSVString request;
+ request.SetAsUSVString().ShareOrDependUpon(aCN->URL());
+
+ // For now we have to wait until the Put Promise is fulfilled before we can
+ // continue since Cache does not yet support starting a read that is being
+ // written to.
+ ErrorResult result;
+ RefPtr<Promise> cachePromise = aCache->Put(aCx, request, *response, result);
+ result.WouldReportJSException();
+ if (NS_WARN_IF(result.Failed())) {
+ // No exception here because there are no ReadableStreams involved here.
+ MOZ_ASSERT(!result.IsJSException());
+ MOZ_ASSERT(!result.IsErrorWithMessage());
+ return result.StealNSResult();
+ }
+
+ mPendingCount += 1;
+ cachePromise->AppendNativeHandler(this);
+ return NS_OK;
+ }
+
+ RefPtr<ServiceWorkerRegistrationInfo> mRegistration;
+ RefPtr<CompareCallback> mCallback;
+ RefPtr<CacheStorage> mCacheStorage;
+
+ nsTArray<RefPtr<CompareNetwork>> mCNList;
+
+ nsString mURL;
+ RefPtr<nsIPrincipal> mPrincipal;
+
+ // Used for the old cache where saves the old source scripts.
+ RefPtr<Cache> mOldCache;
+
+ // Only used if the network script has changed and needs to be cached.
+ nsString mNewCacheName;
+
+ nsCString mMaxScope;
+ nsLoadFlags mLoadFlags;
+
+ enum {
+ WaitingForInitialization,
+ WaitingForExistingOpen,
+ WaitingForExistingKeys,
+ WaitingForScriptOrComparisonResult,
+ WaitingForOpen,
+ WaitingForPut,
+ Finished
+ } mState;
+
+ uint32_t mPendingCount;
+ OnFailure mOnFailure;
+ bool mAreScriptsEqual;
+};
+
+NS_IMPL_ISUPPORTS0(CompareManager)
+
+nsresult CompareNetwork::Initialize(nsIPrincipal* aPrincipal,
+ const nsAString& aURL,
+ Cache* const aCache) {
+ MOZ_ASSERT(aPrincipal);
+ MOZ_ASSERT(NS_IsMainThread());
+
+ nsCOMPtr<nsIURI> uri;
+ nsresult rv = NS_NewURI(getter_AddRefs(uri), aURL);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ mURL = aURL;
+ mURLList.AppendElement(NS_ConvertUTF16toUTF8(mURL));
+
+ nsCOMPtr<nsILoadGroup> loadGroup;
+ rv = NS_NewLoadGroup(getter_AddRefs(loadGroup), aPrincipal);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ // Update LoadFlags for propagating to ServiceWorkerInfo.
+ mLoadFlags = nsIChannel::LOAD_BYPASS_SERVICE_WORKER;
+
+ ServiceWorkerUpdateViaCache uvc = mRegistration->GetUpdateViaCache();
+ if (uvc == ServiceWorkerUpdateViaCache::None ||
+ (uvc == ServiceWorkerUpdateViaCache::Imports && mIsMainScript)) {
+ mLoadFlags |= nsIRequest::VALIDATE_ALWAYS;
+ }
+
+ if (mRegistration->IsLastUpdateCheckTimeOverOneDay()) {
+ mLoadFlags |= nsIRequest::LOAD_BYPASS_CACHE;
+ }
+
+ // Different settings are needed for fetching imported scripts, since they
+ // might be cross-origin scripts.
+ uint32_t secFlags =
+ mIsMainScript ? nsILoadInfo::SEC_REQUIRE_SAME_ORIGIN_DATA_IS_BLOCKED
+ : nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_INHERITS_SEC_CONTEXT;
+
+ nsContentPolicyType contentPolicyType =
+ mIsMainScript ? nsIContentPolicy::TYPE_INTERNAL_SERVICE_WORKER
+ : nsIContentPolicy::TYPE_INTERNAL_WORKER_IMPORT_SCRIPTS;
+
+ // Create a new cookieJarSettings.
+ nsCOMPtr<nsICookieJarSettings> cookieJarSettings =
+ mozilla::net::CookieJarSettings::Create(aPrincipal);
+
+ // Populate the partitionKey by using the given prinicpal. The ServiceWorkers
+ // are using the foreign partitioned principal, so we can get the partitionKey
+ // from it and the partitionKey will only exist if it's in the third-party
+ // context. In first-party context, we can still use the uri to set the
+ // partitionKey.
+ if (!aPrincipal->OriginAttributesRef().mPartitionKey.IsEmpty()) {
+ net::CookieJarSettings::Cast(cookieJarSettings)
+ ->SetPartitionKey(aPrincipal->OriginAttributesRef().mPartitionKey);
+ } else {
+ net::CookieJarSettings::Cast(cookieJarSettings)->SetPartitionKey(uri);
+ }
+
+ // Note that because there is no "serviceworker" RequestContext type, we can
+ // use the TYPE_INTERNAL_SCRIPT content policy types when loading a service
+ // worker.
+ rv = NS_NewChannel(getter_AddRefs(mChannel), uri, aPrincipal, secFlags,
+ contentPolicyType, cookieJarSettings,
+ nullptr /* aPerformanceStorage */, loadGroup,
+ nullptr /* aCallbacks */, mLoadFlags);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(mChannel);
+ if (httpChannel) {
+ // Spec says no redirects allowed for top-level SW scripts.
+ if (mIsMainScript) {
+ rv = httpChannel->SetRedirectionLimit(0);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+ }
+
+ rv = httpChannel->SetRequestHeader("Service-Worker"_ns, "script"_ns,
+ /* merge */ false);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+ }
+
+ nsCOMPtr<nsIStreamLoader> loader;
+ rv = NS_NewStreamLoader(getter_AddRefs(loader), this, this);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ rv = mChannel->AsyncOpen(loader);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ // If we do have an existing cache to compare with.
+ if (aCache) {
+ mCC = new CompareCache(this);
+ rv = mCC->Initialize(aCache, aURL);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ Abort();
+ return rv;
+ }
+
+ mState = WaitingForBothFinished;
+ return NS_OK;
+ }
+
+ mState = WaitingForNetworkFinished;
+ return NS_OK;
+}
+
+void CompareNetwork::Finish() {
+ if (mState == Finished) {
+ return;
+ }
+
+ bool same = true;
+ nsresult rv = NS_OK;
+
+ // mNetworkResult is prior to mCacheResult, since it's needed for reporting
+ // various errors to web content.
+ if (NS_FAILED(mNetworkResult)) {
+ // An imported script could become offline, since it might no longer be
+ // needed by the new importing script. In that case, the importing script
+ // must be different, and thus, it's okay to report same script found here.
+ rv = mIsMainScript ? mNetworkResult : NS_OK;
+ same = true;
+ } else if (mCC && NS_FAILED(mCacheResult)) {
+ rv = mCacheResult;
+ } else { // Both passed.
+ same = mCC && mCC->InCache() && mCC->Buffer().Equals(mBuffer);
+ }
+
+ mManager->ComparisonFinished(rv, mIsMainScript, same, mMaxScope, mLoadFlags);
+
+ // We have done with the CompareCache.
+ mCC = nullptr;
+}
+
+void CompareNetwork::NetworkFinish(nsresult aRv) {
+ MOZ_DIAGNOSTIC_ASSERT(mState == WaitingForBothFinished ||
+ mState == WaitingForNetworkFinished);
+
+ mNetworkResult = aRv;
+
+ if (mState == WaitingForBothFinished) {
+ mState = WaitingForCacheFinished;
+ return;
+ }
+
+ if (mState == WaitingForNetworkFinished) {
+ Finish();
+ return;
+ }
+}
+
+void CompareNetwork::CacheFinish(nsresult aRv) {
+ MOZ_DIAGNOSTIC_ASSERT(mState == WaitingForBothFinished ||
+ mState == WaitingForCacheFinished);
+
+ mCacheResult = aRv;
+
+ if (mState == WaitingForBothFinished) {
+ mState = WaitingForNetworkFinished;
+ return;
+ }
+
+ if (mState == WaitingForCacheFinished) {
+ Finish();
+ return;
+ }
+}
+
+void CompareNetwork::Abort() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (mState != Finished) {
+ mState = Finished;
+
+ MOZ_ASSERT(mChannel);
+ mChannel->CancelWithReason(NS_BINDING_ABORTED, "CompareNetwork::Abort"_ns);
+ mChannel = nullptr;
+
+ if (mCC) {
+ mCC->Abort();
+ mCC = nullptr;
+ }
+ }
+}
+
+NS_IMETHODIMP
+CompareNetwork::OnStartRequest(nsIRequest* aRequest) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (mState == Finished) {
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIChannel> channel = do_QueryInterface(aRequest);
+ MOZ_ASSERT_IF(mIsMainScript, channel == mChannel);
+ mChannel = channel;
+
+ MOZ_ASSERT(!mChannelInfo.IsInitialized());
+ mChannelInfo.InitFromChannel(mChannel);
+
+ nsresult rv = SetPrincipalInfo(mChannel);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ mInternalHeaders->FillResponseHeaders(mChannel);
+
+ nsCOMPtr<nsICacheInfoChannel> cacheChannel(do_QueryInterface(channel));
+ if (cacheChannel) {
+ cacheChannel->IsFromCache(&mIsFromCache);
+ }
+
+ return NS_OK;
+}
+
+nsresult CompareNetwork::SetPrincipalInfo(nsIChannel* aChannel) {
+ nsIScriptSecurityManager* ssm = nsContentUtils::GetSecurityManager();
+ if (!ssm) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsCOMPtr<nsIPrincipal> channelPrincipal;
+ nsresult rv = ssm->GetChannelResultPrincipal(
+ aChannel, getter_AddRefs(channelPrincipal));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ UniquePtr<PrincipalInfo> principalInfo = MakeUnique<PrincipalInfo>();
+ rv = PrincipalToPrincipalInfo(channelPrincipal, principalInfo.get());
+
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ mPrincipalInfo = std::move(principalInfo);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+CompareNetwork::OnStopRequest(nsIRequest* aRequest, nsresult aStatusCode) {
+ // Nothing to do here!
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+CompareNetwork::OnStreamComplete(nsIStreamLoader* aLoader,
+ nsISupports* aContext, nsresult aStatus,
+ uint32_t aLen, const uint8_t* aString) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (mState == Finished) {
+ return NS_OK;
+ }
+
+ nsresult rv = NS_ERROR_FAILURE;
+ auto guard = MakeScopeExit([&] { NetworkFinish(rv); });
+
+ if (NS_WARN_IF(NS_FAILED(aStatus))) {
+ rv = (aStatus == NS_ERROR_REDIRECT_LOOP) ? NS_ERROR_DOM_SECURITY_ERR
+ : aStatus;
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIRequest> request;
+ rv = aLoader->GetRequest(getter_AddRefs(request));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIChannel> channel = do_QueryInterface(request);
+ MOZ_ASSERT(channel, "How come we don't have any channel?");
+
+ nsCOMPtr<nsIURI> uri;
+ channel->GetOriginalURI(getter_AddRefs(uri));
+ bool isExtension = uri->SchemeIs("moz-extension");
+
+ if (isExtension &&
+ !StaticPrefs::extensions_backgroundServiceWorker_enabled_AtStartup()) {
+ // Return earlier with error is the worker script is a moz-extension url
+ // but the feature isn't enabled by prefs.
+ return NS_ERROR_FAILURE;
+ }
+
+ if (isExtension) {
+ // NOTE: trying to register any moz-extension use that doesn't ends
+ // with .js/.jsm/.mjs seems to be already completing with an error
+ // in aStatus and they never reach this point.
+
+ // TODO: look into avoid duplicated parts that could be shared with the HTTP
+ // channel scenario.
+ nsCOMPtr<nsIURI> channelURL;
+ rv = channel->GetURI(getter_AddRefs(channelURL));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ nsCString channelURLSpec;
+ MOZ_ALWAYS_SUCCEEDS(channelURL->GetSpec(channelURLSpec));
+
+ // Append the final URL (which for an extension worker script is going to
+ // be a file or jar url).
+ MOZ_DIAGNOSTIC_ASSERT(!mURLList.IsEmpty());
+ if (channelURLSpec != mURLList[0]) {
+ mURLList.AppendElement(channelURLSpec);
+ }
+
+ char16_t* buffer = nullptr;
+ size_t len = 0;
+
+ rv = ScriptLoader::ConvertToUTF16(channel, aString, aLen, u"UTF-8"_ns,
+ nullptr, buffer, len);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ mBuffer.Adopt(buffer, len);
+
+ rv = NS_OK;
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(request);
+
+ // Main scripts cannot be redirected successfully, however extensions
+ // may successfuly redirect imported scripts to a moz-extension url
+ // (if listed in the web_accessible_resources manifest property).
+ //
+ // When the service worker is initially registered the imported scripts
+ // will be loaded from the child process (see dom/workers/ScriptLoader.cpp)
+ // and in that case this method will only be called for the main script.
+ //
+ // When a registered worker is loaded again (e.g. when the webpage calls
+ // the ServiceWorkerRegistration's update method):
+ //
+ // - both the main and imported scripts are loaded by the
+ // CompareManager::FetchScript
+ // - the update requests for the imported scripts will also be calling this
+ // method and we should expect scripts redirected to an extension script
+ // to have a null httpChannel.
+ //
+ // The request that triggers this method is:
+ //
+ // - the one that is coming from the network (which may be intercepted by
+ // WebRequest listeners in extensions and redirected to a web_accessible
+ // moz-extension url)
+ // - it will then be compared with a previous response that we may have
+ // in the cache
+ //
+ // When the next service worker update occurs, if the request (for an imported
+ // script) is not redirected by an extension the cache entry is invalidated
+ // and a network request is triggered for the import.
+ if (!httpChannel) {
+ // Redirecting a service worker main script should fail before reaching this
+ // method.
+ // If a main script is somehow redirected, the diagnostic assert will crash
+ // in non-release builds. Release builds will return an explicit error.
+ MOZ_DIAGNOSTIC_ASSERT(!mIsMainScript,
+ "Unexpected ServiceWorker main script redirected");
+ if (mIsMainScript) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ nsCOMPtr<nsIPrincipal> channelPrincipal;
+
+ nsIScriptSecurityManager* ssm = nsContentUtils::GetSecurityManager();
+ if (!ssm) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ nsresult rv = ssm->GetChannelResultPrincipal(
+ channel, getter_AddRefs(channelPrincipal));
+
+ // An extension did redirect a non-MainScript request to a moz-extension url
+ // (in that case the originalURL is the resolved jar URI and so we have to
+ // look to the channel principal instead).
+ if (channelPrincipal->SchemeIs("moz-extension")) {
+ char16_t* buffer = nullptr;
+ size_t len = 0;
+
+ rv = ScriptLoader::ConvertToUTF16(channel, aString, aLen, u"UTF-8"_ns,
+ nullptr, buffer, len);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ mBuffer.Adopt(buffer, len);
+
+ return NS_OK;
+ }
+
+ // Make non-release and debug builds to crash if this happens and fail
+ // explicitly on release builds.
+ MOZ_DIAGNOSTIC_ASSERT(false,
+ "ServiceWorker imported script redirected to an url "
+ "with an unexpected scheme");
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ bool requestSucceeded;
+ rv = httpChannel->GetRequestSucceeded(&requestSucceeded);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return NS_OK;
+ }
+
+ if (NS_WARN_IF(!requestSucceeded)) {
+ // Get the stringified numeric status code, not statusText which could be
+ // something misleading like OK for a 404.
+ uint32_t status = 0;
+ Unused << httpChannel->GetResponseStatus(
+ &status); // don't care if this fails, use 0.
+ nsAutoString statusAsText;
+ statusAsText.AppendInt(status);
+
+ ServiceWorkerManager::LocalizeAndReportToAllClients(
+ mRegistration->Scope(), "ServiceWorkerRegisterNetworkError",
+ nsTArray<nsString>{NS_ConvertUTF8toUTF16(mRegistration->Scope()),
+ statusAsText, mURL});
+
+ rv = NS_ERROR_FAILURE;
+ return NS_OK;
+ }
+
+ // Note: we explicitly don't check for the return value here, because the
+ // absence of the header is not an error condition.
+ Unused << httpChannel->GetResponseHeader("Service-Worker-Allowed"_ns,
+ mMaxScope);
+
+ // [9.2 Update]4.13, If response's cache state is not "local",
+ // set registration's last update check time to the current time
+ if (!mIsFromCache) {
+ mRegistration->RefreshLastUpdateCheckTime();
+ }
+
+ nsAutoCString mimeType;
+ rv = httpChannel->GetContentType(mimeType);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ // We should only end up here if !mResponseHead in the channel. If headers
+ // were received but no content type was specified, we'll be given
+ // UNKNOWN_CONTENT_TYPE "application/x-unknown-content-type" and so fall
+ // into the next case with its better error message.
+ rv = NS_ERROR_DOM_SECURITY_ERR;
+ return rv;
+ }
+
+ if (mimeType.IsEmpty() ||
+ !nsContentUtils::IsJavascriptMIMEType(NS_ConvertUTF8toUTF16(mimeType))) {
+ ServiceWorkerManager::LocalizeAndReportToAllClients(
+ mRegistration->Scope(), "ServiceWorkerRegisterMimeTypeError2",
+ nsTArray<nsString>{NS_ConvertUTF8toUTF16(mRegistration->Scope()),
+ NS_ConvertUTF8toUTF16(mimeType), mURL});
+ rv = NS_ERROR_DOM_SECURITY_ERR;
+ return rv;
+ }
+
+ nsCOMPtr<nsIURI> channelURL;
+ rv = httpChannel->GetURI(getter_AddRefs(channelURL));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ nsCString channelURLSpec;
+ MOZ_ALWAYS_SUCCEEDS(channelURL->GetSpec(channelURLSpec));
+
+ // Append the final URL if its different from the original
+ // request URL. This lets us note that a redirect occurred
+ // even though we don't track every redirect URL here.
+ MOZ_DIAGNOSTIC_ASSERT(!mURLList.IsEmpty());
+ if (channelURLSpec != mURLList[0]) {
+ mURLList.AppendElement(channelURLSpec);
+ }
+
+ char16_t* buffer = nullptr;
+ size_t len = 0;
+
+ rv = ScriptLoader::ConvertToUTF16(httpChannel, aString, aLen, u"UTF-8"_ns,
+ nullptr, buffer, len);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ mBuffer.Adopt(buffer, len);
+
+ rv = NS_OK;
+ return NS_OK;
+}
+
+nsresult CompareCache::Initialize(Cache* const aCache, const nsAString& aURL) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aCache);
+ MOZ_DIAGNOSTIC_ASSERT(mState == WaitingForInitialization);
+
+ // This JSContext will not end up executing JS code because here there are
+ // no ReadableStreams involved.
+ AutoJSAPI jsapi;
+ jsapi.Init();
+
+ RequestOrUSVString request;
+ request.SetAsUSVString().ShareOrDependUpon(aURL);
+ ErrorResult error;
+ CacheQueryOptions params;
+ RefPtr<Promise> promise = aCache->Match(jsapi.cx(), request, params, error);
+ if (NS_WARN_IF(error.Failed())) {
+ // No exception here because there are no ReadableStreams involved here.
+ MOZ_ASSERT(!error.IsJSException());
+ mState = Finished;
+ return error.StealNSResult();
+ }
+
+ // Retrieve the script from aCache.
+ mState = WaitingForScript;
+ promise->AppendNativeHandler(this);
+ return NS_OK;
+}
+
+void CompareCache::Finish(nsresult aStatus, bool aInCache) {
+ if (mState != Finished) {
+ mState = Finished;
+ mInCache = aInCache;
+ mCN->CacheFinish(aStatus);
+ }
+}
+
+void CompareCache::Abort() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (mState != Finished) {
+ mState = Finished;
+
+ if (mPump) {
+ mPump->CancelWithReason(NS_BINDING_ABORTED, "CompareCache::Abort"_ns);
+ mPump = nullptr;
+ }
+ }
+}
+
+NS_IMETHODIMP
+CompareCache::OnStreamComplete(nsIStreamLoader* aLoader, nsISupports* aContext,
+ nsresult aStatus, uint32_t aLen,
+ const uint8_t* aString) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (mState == Finished) {
+ return aStatus;
+ }
+
+ if (NS_WARN_IF(NS_FAILED(aStatus))) {
+ Finish(aStatus, false);
+ return aStatus;
+ }
+
+ char16_t* buffer = nullptr;
+ size_t len = 0;
+
+ nsresult rv = ScriptLoader::ConvertToUTF16(nullptr, aString, aLen,
+ u"UTF-8"_ns, nullptr, buffer, len);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ Finish(rv, false);
+ return rv;
+ }
+
+ mBuffer.Adopt(buffer, len);
+
+ Finish(NS_OK, true);
+ return NS_OK;
+}
+
+void CompareCache::ResolvedCallback(JSContext* aCx,
+ JS::Handle<JS::Value> aValue,
+ ErrorResult& aRv) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ switch (mState) {
+ case Finished:
+ return;
+ case WaitingForScript:
+ ManageValueResult(aCx, aValue);
+ return;
+ default:
+ MOZ_CRASH("Unacceptable state.");
+ }
+}
+
+void CompareCache::RejectedCallback(JSContext* aCx,
+ JS::Handle<JS::Value> aValue,
+ ErrorResult& aRv) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (mState != Finished) {
+ Finish(NS_ERROR_FAILURE, false);
+ return;
+ }
+}
+
+void CompareCache::ManageValueResult(JSContext* aCx,
+ JS::Handle<JS::Value> aValue) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ // The cache returns undefined if the object is not stored.
+ if (aValue.isUndefined()) {
+ Finish(NS_OK, false);
+ return;
+ }
+
+ MOZ_ASSERT(aValue.isObject());
+
+ JS::Rooted<JSObject*> obj(aCx, &aValue.toObject());
+ if (NS_WARN_IF(!obj)) {
+ Finish(NS_ERROR_FAILURE, false);
+ return;
+ }
+
+ Response* response = nullptr;
+ nsresult rv = UNWRAP_OBJECT(Response, &obj, response);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ Finish(rv, false);
+ return;
+ }
+
+ MOZ_ASSERT(response->Ok());
+
+ nsCOMPtr<nsIInputStream> inputStream;
+ response->GetBody(getter_AddRefs(inputStream));
+ MOZ_ASSERT(inputStream);
+
+ MOZ_ASSERT(!mPump);
+ rv = NS_NewInputStreamPump(getter_AddRefs(mPump), inputStream.forget(),
+ 0, /* default segsize */
+ 0, /* default segcount */
+ false, /* default closeWhenDone */
+ GetMainThreadSerialEventTarget());
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ Finish(rv, false);
+ return;
+ }
+
+ nsCOMPtr<nsIStreamLoader> loader;
+ rv = NS_NewStreamLoader(getter_AddRefs(loader), this);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ Finish(rv, false);
+ return;
+ }
+
+ rv = mPump->AsyncRead(loader);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ mPump = nullptr;
+ Finish(rv, false);
+ return;
+ }
+
+ nsCOMPtr<nsIThreadRetargetableRequest> rr = do_QueryInterface(mPump);
+ if (rr) {
+ nsCOMPtr<nsIEventTarget> sts =
+ do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID);
+ rv = rr->RetargetDeliveryTo(sts);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ mPump = nullptr;
+ Finish(rv, false);
+ return;
+ }
+ }
+}
+
+nsresult CompareManager::Initialize(nsIPrincipal* aPrincipal,
+ const nsAString& aURL,
+ const nsAString& aCacheName) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aPrincipal);
+ MOZ_ASSERT(mPendingCount == 0);
+ MOZ_DIAGNOSTIC_ASSERT(mState == WaitingForInitialization);
+
+ // RAII Cleanup when fails.
+ auto guard = MakeScopeExit([&] { Cleanup(); });
+
+ mURL = aURL;
+ mPrincipal = aPrincipal;
+
+ // Always create a CacheStorage since we want to write the network entry to
+ // the cache even if there isn't an existing one.
+ AutoJSAPI jsapi;
+ jsapi.Init();
+ ErrorResult result;
+ mCacheStorage = CreateCacheStorage(jsapi.cx(), aPrincipal, result);
+ if (NS_WARN_IF(result.Failed())) {
+ MOZ_ASSERT(!result.IsErrorWithMessage());
+ return result.StealNSResult();
+ }
+
+ // If there is no existing cache, proceed to fetch the script directly.
+ if (aCacheName.IsEmpty()) {
+ mState = WaitingForScriptOrComparisonResult;
+ nsresult rv = FetchScript(aURL, true /* aIsMainScript */);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ guard.release();
+ return NS_OK;
+ }
+
+ // Open the cache saving the old source scripts.
+ RefPtr<Promise> promise = mCacheStorage->Open(aCacheName, result);
+ if (NS_WARN_IF(result.Failed())) {
+ MOZ_ASSERT(!result.IsErrorWithMessage());
+ return result.StealNSResult();
+ }
+
+ mState = WaitingForExistingOpen;
+ promise->AppendNativeHandler(this);
+
+ guard.release();
+ return NS_OK;
+}
+
+// This class manages 4 promises if needed:
+// 1. Retrieve the Cache object by a given CacheName of OldCache.
+// 2. Retrieve the URLs saved in OldCache.
+// 3. Retrieve the Cache object of the NewCache for the newly created SW.
+// 4. Put the value in the cache.
+// For this reason we have mState to know what callback we are handling.
+void CompareManager::ResolvedCallback(JSContext* aCx,
+ JS::Handle<JS::Value> aValue,
+ ErrorResult& aRv) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(mCallback);
+
+ switch (mState) {
+ case Finished:
+ return;
+ case WaitingForExistingOpen:
+ ManageOldCache(aCx, aValue);
+ return;
+ case WaitingForExistingKeys:
+ ManageOldKeys(aCx, aValue);
+ return;
+ case WaitingForOpen:
+ ManageNewCache(aCx, aValue);
+ return;
+ case WaitingForPut:
+ MOZ_DIAGNOSTIC_ASSERT(mPendingCount > 0);
+ if (--mPendingCount == 0) {
+ mCallback->ComparisonResult(NS_OK, false /* aIsEqual */, mOnFailure,
+ mNewCacheName, mMaxScope, mLoadFlags);
+ Cleanup();
+ }
+ return;
+ default:
+ MOZ_DIAGNOSTIC_ASSERT(false);
+ }
+}
+
+void CompareManager::RejectedCallback(JSContext* aCx,
+ JS::Handle<JS::Value> aValue,
+ ErrorResult& aRv) {
+ MOZ_ASSERT(NS_IsMainThread());
+ switch (mState) {
+ case Finished:
+ return;
+ case WaitingForExistingOpen:
+ NS_WARNING("Could not open the existing cache.");
+ break;
+ case WaitingForExistingKeys:
+ NS_WARNING("Could not get the existing URLs.");
+ break;
+ case WaitingForOpen:
+ NS_WARNING("Could not open cache.");
+ break;
+ case WaitingForPut:
+ NS_WARNING("Could not write to cache.");
+ break;
+ default:
+ MOZ_DIAGNOSTIC_ASSERT(false);
+ }
+
+ Fail(NS_ERROR_FAILURE);
+}
+
+void CompareManager::Fail(nsresult aStatus) {
+ MOZ_ASSERT(NS_IsMainThread());
+ mCallback->ComparisonResult(aStatus, false /* aIsEqual */, mOnFailure, u""_ns,
+ ""_ns, mLoadFlags);
+ Cleanup();
+}
+
+void CompareManager::Cleanup() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (mState != Finished) {
+ mState = Finished;
+
+ MOZ_ASSERT(mCallback);
+ mCallback = nullptr;
+
+ // Abort and release CompareNetworks.
+ for (uint32_t i = 0; i < mCNList.Length(); ++i) {
+ mCNList[i]->Abort();
+ }
+ mCNList.Clear();
+ }
+}
+
+} // namespace
+
+nsresult PurgeCache(nsIPrincipal* aPrincipal, const nsAString& aCacheName) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aPrincipal);
+
+ if (aCacheName.IsEmpty()) {
+ return NS_OK;
+ }
+
+ AutoJSAPI jsapi;
+ jsapi.Init();
+ ErrorResult rv;
+ RefPtr<CacheStorage> cacheStorage =
+ CreateCacheStorage(jsapi.cx(), aPrincipal, rv);
+ if (NS_WARN_IF(rv.Failed())) {
+ return rv.StealNSResult();
+ }
+
+ // We use the ServiceWorker scope as key for the cacheStorage.
+ RefPtr<Promise> promise = cacheStorage->Delete(aCacheName, rv);
+ if (NS_WARN_IF(rv.Failed())) {
+ return rv.StealNSResult();
+ }
+
+ // Set [[PromiseIsHandled]] to ensure that if this promise gets rejected,
+ // we don't end up reporting a rejected promise to the console.
+ MOZ_ALWAYS_TRUE(promise->SetAnyPromiseIsHandled());
+
+ // We don't actually care about the result of the delete operation.
+ return NS_OK;
+}
+
+nsresult GenerateCacheName(nsAString& aName) {
+ nsresult rv;
+ nsCOMPtr<nsIUUIDGenerator> uuidGenerator =
+ do_GetService("@mozilla.org/uuid-generator;1", &rv);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ nsID id;
+ rv = uuidGenerator->GenerateUUIDInPlace(&id);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ char chars[NSID_LENGTH];
+ id.ToProvidedString(chars);
+
+ // NSID_LENGTH counts the null terminator.
+ aName.AssignASCII(chars, NSID_LENGTH - 1);
+
+ return NS_OK;
+}
+
+nsresult Compare(ServiceWorkerRegistrationInfo* aRegistration,
+ nsIPrincipal* aPrincipal, const nsAString& aCacheName,
+ const nsAString& aURL, CompareCallback* aCallback) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aRegistration);
+ MOZ_ASSERT(aPrincipal);
+ MOZ_ASSERT(!aURL.IsEmpty());
+ MOZ_ASSERT(aCallback);
+
+ RefPtr<CompareManager> cm = new CompareManager(aRegistration, aCallback);
+
+ nsresult rv = cm->Initialize(aPrincipal, aURL, aCacheName);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ return NS_OK;
+}
+
+} // namespace mozilla::dom::serviceWorkerScriptCache
diff --git a/dom/serviceworkers/ServiceWorkerScriptCache.h b/dom/serviceworkers/ServiceWorkerScriptCache.h
new file mode 100644
index 0000000000..5d71840b46
--- /dev/null
+++ b/dom/serviceworkers/ServiceWorkerScriptCache.h
@@ -0,0 +1,54 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_ServiceWorkerScriptCache_h
+#define mozilla_dom_ServiceWorkerScriptCache_h
+
+#include "nsIRequest.h"
+#include "nsISupportsImpl.h"
+#include "nsString.h"
+
+class nsILoadGroup;
+class nsIPrincipal;
+
+namespace mozilla::dom {
+
+class ServiceWorkerRegistrationInfo;
+
+namespace serviceWorkerScriptCache {
+
+nsresult PurgeCache(nsIPrincipal* aPrincipal, const nsAString& aCacheName);
+
+nsresult GenerateCacheName(nsAString& aName);
+
+enum class OnFailure : uint8_t { DoNothing, Uninstall };
+
+class CompareCallback {
+ public:
+ /*
+ * If there is an error, ignore aInCacheAndEqual and aNewCacheName.
+ * On success, if the cached result and network result matched,
+ * aInCacheAndEqual will be true and no new cache name is passed, otherwise
+ * use the new cache name to load the ServiceWorker.
+ */
+ virtual void ComparisonResult(nsresult aStatus, bool aInCacheAndEqual,
+ OnFailure aOnFailure,
+ const nsAString& aNewCacheName,
+ const nsACString& aMaxScope,
+ nsLoadFlags aLoadFlags) = 0;
+
+ NS_INLINE_DECL_PURE_VIRTUAL_REFCOUNTING
+};
+
+nsresult Compare(ServiceWorkerRegistrationInfo* aRegistration,
+ nsIPrincipal* aPrincipal, const nsAString& aCacheName,
+ const nsAString& aURL, CompareCallback* aCallback);
+
+} // namespace serviceWorkerScriptCache
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_ServiceWorkerScriptCache_h
diff --git a/dom/serviceworkers/ServiceWorkerShutdownBlocker.cpp b/dom/serviceworkers/ServiceWorkerShutdownBlocker.cpp
new file mode 100644
index 0000000000..05a2eb5c31
--- /dev/null
+++ b/dom/serviceworkers/ServiceWorkerShutdownBlocker.cpp
@@ -0,0 +1,291 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "ServiceWorkerShutdownBlocker.h"
+
+#include <chrono>
+#include <utility>
+
+#include "MainThreadUtils.h"
+#include "nsComponentManagerUtils.h"
+#include "nsDebug.h"
+#include "nsError.h"
+#include "nsIWritablePropertyBag2.h"
+#include "nsThreadUtils.h"
+#include "ServiceWorkerManager.h"
+
+#include "mozilla/Assertions.h"
+#include "mozilla/RefPtr.h"
+
+namespace mozilla::dom {
+
+NS_IMPL_ISUPPORTS(ServiceWorkerShutdownBlocker, nsIAsyncShutdownBlocker,
+ nsITimerCallback, nsINamed)
+
+NS_IMETHODIMP ServiceWorkerShutdownBlocker::GetName(nsAString& aNameOut) {
+ aNameOut = nsLiteralString(
+ u"ServiceWorkerShutdownBlocker: shutting down Service Workers");
+ return NS_OK;
+}
+
+// nsINamed implementation
+NS_IMETHODIMP ServiceWorkerShutdownBlocker::GetName(nsACString& aNameOut) {
+ aNameOut.AssignLiteral("ServiceWorkerShutdownBlocker");
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+ServiceWorkerShutdownBlocker::BlockShutdown(nsIAsyncShutdownClient* aClient) {
+ AssertIsOnMainThread();
+ MOZ_ASSERT(!mShutdownClient);
+ MOZ_ASSERT(mServiceWorkerManager);
+
+ mShutdownClient = aClient;
+
+ (*mServiceWorkerManager)->MaybeStartShutdown();
+ mServiceWorkerManager.destroy();
+
+ MaybeUnblockShutdown();
+ MaybeInitUnblockShutdownTimer();
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP ServiceWorkerShutdownBlocker::GetState(nsIPropertyBag** aBagOut) {
+ AssertIsOnMainThread();
+ MOZ_ASSERT(aBagOut);
+
+ nsCOMPtr<nsIWritablePropertyBag2> propertyBag =
+ do_CreateInstance("@mozilla.org/hash-property-bag;1");
+
+ if (NS_WARN_IF(!propertyBag)) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ nsresult rv = propertyBag->SetPropertyAsBool(u"acceptingPromises"_ns,
+ IsAcceptingPromises());
+
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ rv = propertyBag->SetPropertyAsUint32(u"pendingPromises"_ns,
+ GetPendingPromises());
+
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ nsAutoCString shutdownStates;
+
+ for (auto iter = mShutdownStates.iter(); !iter.done(); iter.next()) {
+ shutdownStates.Append(iter.get().value().GetProgressString());
+ shutdownStates.Append(", ");
+ }
+
+ rv = propertyBag->SetPropertyAsACString(u"shutdownStates"_ns, shutdownStates);
+
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ propertyBag.forget(aBagOut);
+
+ return NS_OK;
+}
+
+/* static */ already_AddRefed<ServiceWorkerShutdownBlocker>
+ServiceWorkerShutdownBlocker::CreateAndRegisterOn(
+ nsIAsyncShutdownClient& aShutdownBarrier,
+ ServiceWorkerManager& aServiceWorkerManager) {
+ AssertIsOnMainThread();
+
+ RefPtr<ServiceWorkerShutdownBlocker> blocker =
+ new ServiceWorkerShutdownBlocker(aServiceWorkerManager);
+
+ nsresult rv = aShutdownBarrier.AddBlocker(
+ blocker.get(), NS_LITERAL_STRING_FROM_CSTRING(__FILE__), __LINE__,
+ u"Service Workers shutdown"_ns);
+
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return nullptr;
+ }
+
+ return blocker.forget();
+}
+
+void ServiceWorkerShutdownBlocker::WaitOnPromise(
+ GenericNonExclusivePromise* aPromise, uint32_t aShutdownStateId) {
+ AssertIsOnMainThread();
+ MOZ_DIAGNOSTIC_ASSERT(IsAcceptingPromises());
+ MOZ_ASSERT(aPromise);
+ MOZ_ASSERT(mShutdownStates.has(aShutdownStateId));
+
+ ++mState.as<AcceptingPromises>().mPendingPromises;
+
+ RefPtr<ServiceWorkerShutdownBlocker> self = this;
+
+ aPromise->Then(GetCurrentSerialEventTarget(), __func__,
+ [self = std::move(self), shutdownStateId = aShutdownStateId](
+ const GenericNonExclusivePromise::ResolveOrRejectValue&) {
+ // Progress reporting might race with aPromise settling.
+ self->mShutdownStates.remove(shutdownStateId);
+
+ if (!self->PromiseSettled()) {
+ self->MaybeUnblockShutdown();
+ }
+ });
+}
+
+void ServiceWorkerShutdownBlocker::StopAcceptingPromises() {
+ AssertIsOnMainThread();
+ MOZ_ASSERT(IsAcceptingPromises());
+
+ mState = AsVariant(NotAcceptingPromises(mState.as<AcceptingPromises>()));
+
+ MaybeUnblockShutdown();
+ MaybeInitUnblockShutdownTimer();
+}
+
+uint32_t ServiceWorkerShutdownBlocker::CreateShutdownState() {
+ AssertIsOnMainThread();
+
+ static uint32_t nextShutdownStateId = 1;
+
+ MOZ_ALWAYS_TRUE(mShutdownStates.putNew(nextShutdownStateId,
+ ServiceWorkerShutdownState()));
+
+ return nextShutdownStateId++;
+}
+
+void ServiceWorkerShutdownBlocker::ReportShutdownProgress(
+ uint32_t aShutdownStateId, Progress aProgress) {
+ AssertIsOnMainThread();
+ MOZ_RELEASE_ASSERT(aShutdownStateId != kInvalidShutdownStateId);
+
+ auto lookup = mShutdownStates.lookup(aShutdownStateId);
+
+ // Progress reporting might race with the promise that WaitOnPromise is called
+ // with settling.
+ if (!lookup) {
+ return;
+ }
+
+ // This will check for a valid progress transition with assertions.
+ lookup->value().SetProgress(aProgress);
+
+ if (aProgress == Progress::ShutdownCompleted) {
+ mShutdownStates.remove(lookup);
+ }
+}
+
+ServiceWorkerShutdownBlocker::ServiceWorkerShutdownBlocker(
+ ServiceWorkerManager& aServiceWorkerManager)
+ : mState(VariantType<AcceptingPromises>()),
+ mServiceWorkerManager(WrapNotNull(&aServiceWorkerManager)) {
+ AssertIsOnMainThread();
+}
+
+ServiceWorkerShutdownBlocker::~ServiceWorkerShutdownBlocker() {
+ MOZ_ASSERT(!IsAcceptingPromises());
+ MOZ_ASSERT(!GetPendingPromises());
+ MOZ_ASSERT(!mShutdownClient);
+ MOZ_ASSERT(!mServiceWorkerManager);
+}
+
+void ServiceWorkerShutdownBlocker::MaybeUnblockShutdown() {
+ AssertIsOnMainThread();
+
+ if (!mShutdownClient || IsAcceptingPromises() || GetPendingPromises()) {
+ return;
+ }
+
+ UnblockShutdown();
+}
+
+void ServiceWorkerShutdownBlocker::UnblockShutdown() {
+ MOZ_ASSERT(mShutdownClient);
+
+ mShutdownClient->RemoveBlocker(this);
+ mShutdownClient = nullptr;
+
+ if (mTimer) {
+ mTimer->Cancel();
+ }
+}
+
+uint32_t ServiceWorkerShutdownBlocker::PromiseSettled() {
+ AssertIsOnMainThread();
+ MOZ_ASSERT(GetPendingPromises());
+
+ if (IsAcceptingPromises()) {
+ return --mState.as<AcceptingPromises>().mPendingPromises;
+ }
+
+ return --mState.as<NotAcceptingPromises>().mPendingPromises;
+}
+
+bool ServiceWorkerShutdownBlocker::IsAcceptingPromises() const {
+ AssertIsOnMainThread();
+
+ return mState.is<AcceptingPromises>();
+}
+
+uint32_t ServiceWorkerShutdownBlocker::GetPendingPromises() const {
+ AssertIsOnMainThread();
+
+ if (IsAcceptingPromises()) {
+ return mState.as<AcceptingPromises>().mPendingPromises;
+ }
+
+ return mState.as<NotAcceptingPromises>().mPendingPromises;
+}
+
+ServiceWorkerShutdownBlocker::NotAcceptingPromises::NotAcceptingPromises(
+ AcceptingPromises aPreviousState)
+ : mPendingPromises(aPreviousState.mPendingPromises) {
+ AssertIsOnMainThread();
+}
+
+NS_IMETHODIMP ServiceWorkerShutdownBlocker::Notify(nsITimer*) {
+ // TODO: this method being called indicates that there are ServiceWorkers
+ // that did not complete shutdown before the timer expired - there should be
+ // a telemetry ping or some other way of recording the state of when this
+ // happens (e.g. what's returned by GetState()).
+ UnblockShutdown();
+ return NS_OK;
+}
+
+#ifdef RELEASE_OR_BETA
+# define SW_UNBLOCK_SHUTDOWN_TIMER_DURATION 10s
+#else
+// In Nightly, we do want a shutdown hang to be reported so we pick a value
+// notably longer than the 60s of the RunWatchDog timeout.
+# define SW_UNBLOCK_SHUTDOWN_TIMER_DURATION 200s
+#endif
+
+void ServiceWorkerShutdownBlocker::MaybeInitUnblockShutdownTimer() {
+ AssertIsOnMainThread();
+
+ if (mTimer || !mShutdownClient || IsAcceptingPromises()) {
+ return;
+ }
+
+ MOZ_ASSERT(GetPendingPromises(),
+ "Shouldn't be blocking shutdown with zero pending promises.");
+
+ using namespace std::chrono_literals;
+
+ static constexpr auto delay =
+ std::chrono::duration_cast<std::chrono::milliseconds>(
+ SW_UNBLOCK_SHUTDOWN_TIMER_DURATION);
+
+ mTimer = NS_NewTimer();
+
+ mTimer->InitWithCallback(this, delay.count(), nsITimer::TYPE_ONE_SHOT);
+}
+
+} // namespace mozilla::dom
diff --git a/dom/serviceworkers/ServiceWorkerShutdownBlocker.h b/dom/serviceworkers/ServiceWorkerShutdownBlocker.h
new file mode 100644
index 0000000000..a200325c5b
--- /dev/null
+++ b/dom/serviceworkers/ServiceWorkerShutdownBlocker.h
@@ -0,0 +1,157 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_serviceworkershutdownblocker_h__
+#define mozilla_dom_serviceworkershutdownblocker_h__
+
+#include "nsCOMPtr.h"
+#include "nsIAsyncShutdown.h"
+#include "nsISupportsImpl.h"
+#include "nsITimer.h"
+
+#include "ServiceWorkerShutdownState.h"
+#include "mozilla/InitializedOnce.h"
+#include "mozilla/MozPromise.h"
+#include "mozilla/NotNull.h"
+#include "mozilla/HashTable.h"
+
+namespace mozilla::dom {
+
+class ServiceWorkerManager;
+
+/**
+ * Main thread only.
+ *
+ * A ServiceWorkerShutdownBlocker will "accept promises", and each of these
+ * promises will be a "pending promise" while it hasn't settled. At some point,
+ * `StopAcceptingPromises()` should be called and the state will change to "not
+ * accepting promises" (this is a one way state transition). The shutdown phase
+ * of the shutdown client the blocker is created with will be blocked until
+ * there are no more pending promises.
+ *
+ * It doesn't matter whether the state changes to "not accepting promises"
+ * before or during the associated shutdown phase.
+ *
+ * In beta/release builds there will be an additional timer that starts ticking
+ * once both the shutdown phase has been reached and the state is "not accepting
+ * promises". If when the timer expire there are still pending promises,
+ * shutdown will be forcefully unblocked.
+ */
+class ServiceWorkerShutdownBlocker final : public nsIAsyncShutdownBlocker,
+ public nsITimerCallback,
+ public nsINamed {
+ public:
+ using Progress = ServiceWorkerShutdownState::Progress;
+ static const uint32_t kInvalidShutdownStateId = 0;
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIASYNCSHUTDOWNBLOCKER
+ NS_DECL_NSITIMERCALLBACK
+ NS_DECL_NSINAMED
+
+ /**
+ * Returns the registered shutdown blocker if registration succeeded and
+ * nullptr otherwise.
+ */
+ static already_AddRefed<ServiceWorkerShutdownBlocker> CreateAndRegisterOn(
+ nsIAsyncShutdownClient& aShutdownBarrier,
+ ServiceWorkerManager& aServiceWorkerManager);
+
+ /**
+ * Blocks shutdown until `aPromise` settles.
+ *
+ * Can be called multiple times, and shutdown will be blocked until all the
+ * calls' promises settle, but all of these calls must happen before
+ * `StopAcceptingPromises()` is called (assertions will enforce this).
+ *
+ * See `CreateShutdownState` for aShutdownStateId, which is needed to clear
+ * the shutdown state if the shutdown process aborts for some reason.
+ */
+ void WaitOnPromise(GenericNonExclusivePromise* aPromise,
+ uint32_t aShutdownStateId);
+
+ /**
+ * Once this is called, shutdown will be blocked until all promises
+ * passed to `WaitOnPromise()` settle, and there must be no more calls to
+ * `WaitOnPromise()` (assertions will enforce this).
+ */
+ void StopAcceptingPromises();
+
+ /**
+ * Start tracking the shutdown of an individual ServiceWorker for hang
+ * reporting purposes. Returns a "shutdown state ID" that should be used
+ * in subsequent calls to ReportShutdownProgress. The shutdown of an
+ * individual ServiceWorker is presumed to be completed when its `Progress`
+ * reaches `Progress::ShutdownCompleted`.
+ */
+ uint32_t CreateShutdownState();
+
+ void ReportShutdownProgress(uint32_t aShutdownStateId, Progress aProgress);
+
+ private:
+ explicit ServiceWorkerShutdownBlocker(
+ ServiceWorkerManager& aServiceWorkerManager);
+
+ ~ServiceWorkerShutdownBlocker();
+
+ /**
+ * No-op if any of the following are true:
+ * 1) `BlockShutdown()` hasn't been called yet, or
+ * 2) `StopAcceptingPromises()` hasn't been called yet, or
+ * 3) `StopAcceptingPromises()` HAS been called, but there are still pending
+ * promises.
+ */
+ void MaybeUnblockShutdown();
+
+ /**
+ * Requires `BlockShutdown()` to have been called.
+ */
+ void UnblockShutdown();
+
+ /**
+ * Returns the remaining pending promise count (i.e. excluding the promise
+ * that just settled).
+ */
+ uint32_t PromiseSettled();
+
+ bool IsAcceptingPromises() const;
+
+ uint32_t GetPendingPromises() const;
+
+ /**
+ * Initializes a timer that will unblock shutdown unconditionally once it's
+ * expired (even if there are still pending promises). No-op if:
+ * 1) not a beta or release build, or
+ * 2) shutdown is not being blocked or `StopAcceptingPromises()` has not been
+ * called.
+ */
+ void MaybeInitUnblockShutdownTimer();
+
+ struct AcceptingPromises {
+ uint32_t mPendingPromises = 0;
+ };
+
+ struct NotAcceptingPromises {
+ explicit NotAcceptingPromises(AcceptingPromises aPreviousState);
+
+ uint32_t mPendingPromises = 0;
+ };
+
+ Variant<AcceptingPromises, NotAcceptingPromises> mState;
+
+ nsCOMPtr<nsIAsyncShutdownClient> mShutdownClient;
+
+ HashMap<uint32_t, ServiceWorkerShutdownState> mShutdownStates;
+
+ nsCOMPtr<nsITimer> mTimer;
+ LazyInitializedOnceEarlyDestructible<
+ const NotNull<RefPtr<ServiceWorkerManager>>>
+ mServiceWorkerManager;
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_serviceworkershutdownblocker_h__
diff --git a/dom/serviceworkers/ServiceWorkerShutdownState.cpp b/dom/serviceworkers/ServiceWorkerShutdownState.cpp
new file mode 100644
index 0000000000..40f2e09f3f
--- /dev/null
+++ b/dom/serviceworkers/ServiceWorkerShutdownState.cpp
@@ -0,0 +1,165 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "ServiceWorkerShutdownState.h"
+
+#include <array>
+#include <type_traits>
+
+#include "MainThreadUtils.h"
+#include "ServiceWorkerUtils.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/SchedulerGroup.h"
+#include "mozilla/Unused.h"
+#include "mozilla/dom/ContentChild.h"
+#include "mozilla/dom/RemoteWorkerService.h"
+#include "mozilla/dom/ServiceWorkerManager.h"
+#include "mozilla/dom/WorkerCommon.h"
+#include "mozilla/ipc/BackgroundParent.h"
+#include "nsDebug.h"
+#include "nsThreadUtils.h"
+#include "nsXULAppAPI.h"
+
+namespace mozilla::dom {
+
+using Progress = ServiceWorkerShutdownState::Progress;
+
+namespace {
+
+constexpr inline auto UnderlyingProgressValue(Progress aProgress) {
+ return std::underlying_type_t<Progress>(aProgress);
+}
+
+constexpr std::array<const char*, UnderlyingProgressValue(Progress::EndGuard_)>
+ gProgressStrings = {{
+ // clang-format off
+ "parent process main thread",
+ "parent process IPDL background thread",
+ "content process worker launcher thread",
+ "content process main thread",
+ "shutdown completed"
+ // clang-format on
+ }};
+
+} // anonymous namespace
+
+ServiceWorkerShutdownState::ServiceWorkerShutdownState()
+ : mProgress(Progress::ParentProcessMainThread) {
+ MOZ_ASSERT(XRE_IsParentProcess());
+ MOZ_ASSERT(NS_IsMainThread());
+}
+
+ServiceWorkerShutdownState::~ServiceWorkerShutdownState() {
+ Unused << NS_WARN_IF(mProgress != Progress::ShutdownCompleted);
+}
+
+const char* ServiceWorkerShutdownState::GetProgressString() const {
+ return gProgressStrings[UnderlyingProgressValue(mProgress)];
+}
+
+void ServiceWorkerShutdownState::SetProgress(Progress aProgress) {
+ MOZ_ASSERT(aProgress != Progress::EndGuard_);
+ MOZ_RELEASE_ASSERT(UnderlyingProgressValue(mProgress) + 1 ==
+ UnderlyingProgressValue(aProgress));
+
+ mProgress = aProgress;
+}
+
+namespace {
+
+void ReportProgressToServiceWorkerManager(uint32_t aShutdownStateId,
+ Progress aProgress) {
+ MOZ_ASSERT(XRE_IsParentProcess());
+ MOZ_ASSERT(NS_IsMainThread());
+
+ RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance();
+ MOZ_RELEASE_ASSERT(swm, "ServiceWorkers should shutdown before SWM.");
+
+ swm->ReportServiceWorkerShutdownProgress(aShutdownStateId, aProgress);
+}
+
+void ReportProgressToParentProcess(uint32_t aShutdownStateId,
+ Progress aProgress) {
+ MOZ_ASSERT(XRE_IsContentProcess());
+ MOZ_ASSERT(NS_IsMainThread());
+
+ ContentChild* contentChild = ContentChild::GetSingleton();
+ MOZ_ASSERT(contentChild);
+
+ contentChild->SendReportServiceWorkerShutdownProgress(aShutdownStateId,
+ aProgress);
+}
+
+void ReportServiceWorkerShutdownProgress(uint32_t aShutdownStateId,
+ Progress aProgress) {
+ MOZ_ASSERT(UnderlyingProgressValue(Progress::ParentProcessMainThread) <
+ UnderlyingProgressValue(aProgress));
+ MOZ_ASSERT(UnderlyingProgressValue(aProgress) <
+ UnderlyingProgressValue(Progress::EndGuard_));
+
+ nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction(
+ __func__, [shutdownStateId = aShutdownStateId, progress = aProgress] {
+ if (XRE_IsParentProcess()) {
+ ReportProgressToServiceWorkerManager(shutdownStateId, progress);
+ } else {
+ ReportProgressToParentProcess(shutdownStateId, progress);
+ }
+ });
+
+ if (NS_IsMainThread()) {
+ MOZ_ALWAYS_SUCCEEDS(r->Run());
+ } else {
+ MOZ_ALWAYS_SUCCEEDS(
+ SchedulerGroup::Dispatch(TaskCategory::Other, r.forget()));
+ }
+}
+
+void ReportServiceWorkerShutdownProgress(uint32_t aShutdownStateId) {
+ Progress progress = Progress::EndGuard_;
+
+ if (XRE_IsParentProcess()) {
+ mozilla::ipc::AssertIsOnBackgroundThread();
+
+ progress = Progress::ParentProcessIpdlBackgroundThread;
+ } else {
+ if (NS_IsMainThread()) {
+ progress = Progress::ContentProcessMainThread;
+ } else {
+ MOZ_ASSERT(RemoteWorkerService::Thread()->IsOnCurrentThread());
+ progress = Progress::ContentProcessWorkerLauncherThread;
+ }
+ }
+
+ ReportServiceWorkerShutdownProgress(aShutdownStateId, progress);
+}
+
+} // anonymous namespace
+
+void MaybeReportServiceWorkerShutdownProgress(const ServiceWorkerOpArgs& aArgs,
+ bool aShutdownCompleted) {
+ if (XRE_IsParentProcess() && !XRE_IsE10sParentProcess()) {
+ return;
+ }
+
+ if (aShutdownCompleted) {
+ MOZ_ASSERT(aArgs.type() ==
+ ServiceWorkerOpArgs::TServiceWorkerTerminateWorkerOpArgs);
+
+ ReportServiceWorkerShutdownProgress(
+ aArgs.get_ServiceWorkerTerminateWorkerOpArgs().shutdownStateId(),
+ Progress::ShutdownCompleted);
+
+ return;
+ }
+
+ if (aArgs.type() ==
+ ServiceWorkerOpArgs::TServiceWorkerTerminateWorkerOpArgs) {
+ ReportServiceWorkerShutdownProgress(
+ aArgs.get_ServiceWorkerTerminateWorkerOpArgs().shutdownStateId());
+ }
+}
+
+} // namespace mozilla::dom
diff --git a/dom/serviceworkers/ServiceWorkerShutdownState.h b/dom/serviceworkers/ServiceWorkerShutdownState.h
new file mode 100644
index 0000000000..eec55fab8d
--- /dev/null
+++ b/dom/serviceworkers/ServiceWorkerShutdownState.h
@@ -0,0 +1,61 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_SERVICEWORKERS_SERVICEWORKERSHUTDOWNSTATE_H_
+#define DOM_SERVICEWORKERS_SERVICEWORKERSHUTDOWNSTATE_H_
+
+#include <cstdint>
+
+#include "ipc/EnumSerializer.h"
+#include "mozilla/dom/ServiceWorkerOpArgs.h"
+
+namespace mozilla::dom {
+
+class ServiceWorkerShutdownState {
+ public:
+ // Represents the "location" of the shutdown message or completion of
+ // shutdown.
+ enum class Progress {
+ ParentProcessMainThread,
+ ParentProcessIpdlBackgroundThread,
+ ContentProcessWorkerLauncherThread,
+ ContentProcessMainThread,
+ ShutdownCompleted,
+ EndGuard_,
+ };
+
+ ServiceWorkerShutdownState();
+
+ ~ServiceWorkerShutdownState();
+
+ const char* GetProgressString() const;
+
+ void SetProgress(Progress aProgress);
+
+ private:
+ Progress mProgress;
+};
+
+// Asynchronously reports that shutdown has progressed to the calling thread
+// if aArgs is for shutdown. If aShutdownCompleted is true, aArgs must be for
+// shutdown.
+void MaybeReportServiceWorkerShutdownProgress(const ServiceWorkerOpArgs& aArgs,
+ bool aShutdownCompleted = false);
+
+} // namespace mozilla::dom
+
+namespace IPC {
+
+using Progress = mozilla::dom::ServiceWorkerShutdownState::Progress;
+
+template <>
+struct ParamTraits<Progress>
+ : public ContiguousEnumSerializer<
+ Progress, Progress::ParentProcessMainThread, Progress::EndGuard_> {};
+
+} // namespace IPC
+
+#endif // DOM_SERVICEWORKERS_SERVICEWORKERSHUTDOWNSTATE_H_
diff --git a/dom/serviceworkers/ServiceWorkerUnregisterCallback.cpp b/dom/serviceworkers/ServiceWorkerUnregisterCallback.cpp
new file mode 100644
index 0000000000..36650c9b55
--- /dev/null
+++ b/dom/serviceworkers/ServiceWorkerUnregisterCallback.cpp
@@ -0,0 +1,35 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "ServiceWorkerUnregisterCallback.h"
+
+namespace mozilla::dom {
+
+NS_IMPL_ISUPPORTS(UnregisterCallback, nsIServiceWorkerUnregisterCallback)
+
+UnregisterCallback::UnregisterCallback()
+ : mPromise(new GenericPromise::Private(__func__)) {}
+
+UnregisterCallback::UnregisterCallback(GenericPromise::Private* aPromise)
+ : mPromise(aPromise) {
+ MOZ_DIAGNOSTIC_ASSERT(mPromise);
+}
+
+NS_IMETHODIMP
+UnregisterCallback::UnregisterSucceeded(bool aState) {
+ mPromise->Resolve(aState, __func__);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+UnregisterCallback::UnregisterFailed() {
+ mPromise->Reject(NS_ERROR_DOM_SECURITY_ERR, __func__);
+ return NS_OK;
+}
+
+RefPtr<GenericPromise> UnregisterCallback::Promise() const { return mPromise; }
+
+} // namespace mozilla::dom
diff --git a/dom/serviceworkers/ServiceWorkerUnregisterCallback.h b/dom/serviceworkers/ServiceWorkerUnregisterCallback.h
new file mode 100644
index 0000000000..dd1a53b37d
--- /dev/null
+++ b/dom/serviceworkers/ServiceWorkerUnregisterCallback.h
@@ -0,0 +1,41 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_ServiceWorkerUnregisterCallback_h
+#define mozilla_dom_ServiceWorkerUnregisterCallback_h
+
+#include "mozilla/MozPromise.h"
+#include "mozilla/RefPtr.h"
+#include "nsIServiceWorkerManager.h"
+
+namespace mozilla::dom {
+
+class UnregisterCallback final : public nsIServiceWorkerUnregisterCallback {
+ public:
+ NS_DECL_ISUPPORTS
+
+ UnregisterCallback();
+
+ explicit UnregisterCallback(GenericPromise::Private* aPromise);
+
+ // nsIServiceWorkerUnregisterCallback implementation
+ NS_IMETHOD
+ UnregisterSucceeded(bool aState) override;
+
+ NS_IMETHOD
+ UnregisterFailed() override;
+
+ RefPtr<GenericPromise> Promise() const;
+
+ private:
+ ~UnregisterCallback() = default;
+
+ RefPtr<GenericPromise::Private> mPromise;
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_ServiceWorkerUnregisterCallback_h
diff --git a/dom/serviceworkers/ServiceWorkerUnregisterJob.cpp b/dom/serviceworkers/ServiceWorkerUnregisterJob.cpp
new file mode 100644
index 0000000000..b40521bf52
--- /dev/null
+++ b/dom/serviceworkers/ServiceWorkerUnregisterJob.cpp
@@ -0,0 +1,139 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "ServiceWorkerUnregisterJob.h"
+
+#include "mozilla/Unused.h"
+#include "nsIPushService.h"
+#include "nsServiceManagerUtils.h"
+#include "nsThreadUtils.h"
+#include "ServiceWorkerManager.h"
+
+namespace mozilla::dom {
+
+class ServiceWorkerUnregisterJob::PushUnsubscribeCallback final
+ : public nsIUnsubscribeResultCallback {
+ public:
+ NS_DECL_ISUPPORTS
+
+ explicit PushUnsubscribeCallback(ServiceWorkerUnregisterJob* aJob)
+ : mJob(aJob) {
+ MOZ_ASSERT(NS_IsMainThread());
+ }
+
+ NS_IMETHOD
+ OnUnsubscribe(nsresult aStatus, bool) override {
+ // Warn if unsubscribing fails, but don't prevent the worker from
+ // unregistering.
+ Unused << NS_WARN_IF(NS_FAILED(aStatus));
+ mJob->Unregister();
+ return NS_OK;
+ }
+
+ private:
+ ~PushUnsubscribeCallback() = default;
+
+ RefPtr<ServiceWorkerUnregisterJob> mJob;
+};
+
+NS_IMPL_ISUPPORTS(ServiceWorkerUnregisterJob::PushUnsubscribeCallback,
+ nsIUnsubscribeResultCallback)
+
+ServiceWorkerUnregisterJob::ServiceWorkerUnregisterJob(nsIPrincipal* aPrincipal,
+ const nsACString& aScope,
+ bool aSendToParent)
+ : ServiceWorkerJob(Type::Unregister, aPrincipal, aScope, ""_ns),
+ mResult(false),
+ mSendToParent(aSendToParent) {}
+
+bool ServiceWorkerUnregisterJob::GetResult() const {
+ MOZ_ASSERT(NS_IsMainThread());
+ return mResult;
+}
+
+ServiceWorkerUnregisterJob::~ServiceWorkerUnregisterJob() = default;
+
+void ServiceWorkerUnregisterJob::AsyncExecute() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (Canceled()) {
+ Finish(NS_ERROR_DOM_ABORT_ERR);
+ return;
+ }
+
+ // Push API, section 5: "When a service worker registration is unregistered,
+ // any associated push subscription must be deactivated." To ensure the
+ // service worker registration isn't cleared as we're unregistering, we
+ // unsubscribe first.
+ nsCOMPtr<nsIPushService> pushService =
+ do_GetService("@mozilla.org/push/Service;1");
+ if (NS_WARN_IF(!pushService)) {
+ Unregister();
+ return;
+ }
+ nsCOMPtr<nsIUnsubscribeResultCallback> unsubscribeCallback =
+ new PushUnsubscribeCallback(this);
+ nsresult rv = pushService->Unsubscribe(NS_ConvertUTF8toUTF16(mScope),
+ mPrincipal, unsubscribeCallback);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ Unregister();
+ }
+}
+
+void ServiceWorkerUnregisterJob::Unregister() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance();
+ if (Canceled() || !swm) {
+ Finish(NS_ERROR_DOM_ABORT_ERR);
+ return;
+ }
+
+ // Step 1 of the Unregister algorithm requires checking that the
+ // client origin matches the scope's origin. We perform this in
+ // registration->update() method directly since we don't have that
+ // client information available here.
+
+ // "Let registration be the result of running [[Get Registration]]
+ // algorithm passing scope as the argument."
+ RefPtr<ServiceWorkerRegistrationInfo> registration =
+ swm->GetRegistration(mPrincipal, mScope);
+ if (!registration) {
+ // "If registration is null, then, resolve promise with false."
+ Finish(NS_OK);
+ return;
+ }
+
+ // Note, we send the message to remove the registration from disk now. This is
+ // necessary to ensure the registration is removed if the controlled
+ // clients are closed by shutting down the browser.
+ if (mSendToParent) {
+ swm->MaybeSendUnregister(mPrincipal, mScope);
+ }
+
+ swm->EvictFromBFCache(registration);
+
+ // "Remove scope to registration map[job's scope url]."
+ swm->RemoveRegistration(registration);
+ MOZ_ASSERT(registration->IsUnregistered());
+
+ // "Resolve promise with true"
+ mResult = true;
+ InvokeResultCallbacks(NS_OK);
+
+ // "Invoke Try Clear Registration with registration"
+ if (!registration->IsControllingClients()) {
+ if (registration->IsIdle()) {
+ registration->Clear();
+ } else {
+ registration->ClearWhenIdle();
+ }
+ }
+
+ Finish(NS_OK);
+}
+
+} // namespace mozilla::dom
diff --git a/dom/serviceworkers/ServiceWorkerUnregisterJob.h b/dom/serviceworkers/ServiceWorkerUnregisterJob.h
new file mode 100644
index 0000000000..839bbc659e
--- /dev/null
+++ b/dom/serviceworkers/ServiceWorkerUnregisterJob.h
@@ -0,0 +1,36 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_serviceworkerunregisterjob_h
+#define mozilla_dom_serviceworkerunregisterjob_h
+
+#include "ServiceWorkerJob.h"
+
+namespace mozilla::dom {
+
+class ServiceWorkerUnregisterJob final : public ServiceWorkerJob {
+ public:
+ ServiceWorkerUnregisterJob(nsIPrincipal* aPrincipal, const nsACString& aScope,
+ bool aSendToParent);
+
+ bool GetResult() const;
+
+ private:
+ class PushUnsubscribeCallback;
+
+ virtual ~ServiceWorkerUnregisterJob();
+
+ virtual void AsyncExecute() override;
+
+ void Unregister();
+
+ bool mResult;
+ bool mSendToParent;
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_serviceworkerunregisterjob_h
diff --git a/dom/serviceworkers/ServiceWorkerUpdateJob.cpp b/dom/serviceworkers/ServiceWorkerUpdateJob.cpp
new file mode 100644
index 0000000000..a6726fbf47
--- /dev/null
+++ b/dom/serviceworkers/ServiceWorkerUpdateJob.cpp
@@ -0,0 +1,541 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "ServiceWorkerUpdateJob.h"
+
+#include "mozilla/Telemetry.h"
+#include "nsIScriptError.h"
+#include "nsIURL.h"
+#include "nsNetUtil.h"
+#include "nsProxyRelease.h"
+#include "ServiceWorkerManager.h"
+#include "ServiceWorkerPrivate.h"
+#include "ServiceWorkerRegistrationInfo.h"
+#include "ServiceWorkerScriptCache.h"
+#include "mozilla/dom/WorkerCommon.h"
+
+namespace mozilla::dom {
+
+using serviceWorkerScriptCache::OnFailure;
+
+namespace {
+
+/**
+ * The spec mandates slightly different behaviors for computing the scope
+ * prefix string in case a Service-Worker-Allowed header is specified versus
+ * when it's not available.
+ *
+ * With the header:
+ * "Set maxScopeString to "/" concatenated with the strings in maxScope's
+ * path (including empty strings), separated from each other by "/"."
+ * Without the header:
+ * "Set maxScopeString to "/" concatenated with the strings, except the last
+ * string that denotes the script's file name, in registration's registering
+ * script url's path (including empty strings), separated from each other by
+ * "/"."
+ *
+ * In simpler terms, if the header is not present, we should only use the
+ * "directory" part of the pathname, and otherwise the entire pathname should be
+ * used. ScopeStringPrefixMode allows the caller to specify the desired
+ * behavior.
+ */
+enum ScopeStringPrefixMode { eUseDirectory, eUsePath };
+
+nsresult GetRequiredScopeStringPrefix(nsIURI* aScriptURI, nsACString& aPrefix,
+ ScopeStringPrefixMode aPrefixMode) {
+ nsresult rv;
+ if (aPrefixMode == eUseDirectory) {
+ nsCOMPtr<nsIURL> scriptURL(do_QueryInterface(aScriptURI));
+ if (NS_WARN_IF(!scriptURL)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ rv = scriptURL->GetDirectory(aPrefix);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ } else if (aPrefixMode == eUsePath) {
+ rv = aScriptURI->GetPathQueryRef(aPrefix);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ } else {
+ MOZ_ASSERT_UNREACHABLE("Invalid value for aPrefixMode");
+ }
+ return NS_OK;
+}
+
+} // anonymous namespace
+
+class ServiceWorkerUpdateJob::CompareCallback final
+ : public serviceWorkerScriptCache::CompareCallback {
+ RefPtr<ServiceWorkerUpdateJob> mJob;
+
+ ~CompareCallback() = default;
+
+ public:
+ explicit CompareCallback(ServiceWorkerUpdateJob* aJob) : mJob(aJob) {
+ MOZ_ASSERT(mJob);
+ }
+
+ virtual void ComparisonResult(nsresult aStatus, bool aInCacheAndEqual,
+ OnFailure aOnFailure,
+ const nsAString& aNewCacheName,
+ const nsACString& aMaxScope,
+ nsLoadFlags aLoadFlags) override {
+ mJob->ComparisonResult(aStatus, aInCacheAndEqual, aOnFailure, aNewCacheName,
+ aMaxScope, aLoadFlags);
+ }
+
+ NS_INLINE_DECL_REFCOUNTING(ServiceWorkerUpdateJob::CompareCallback, override)
+};
+
+class ServiceWorkerUpdateJob::ContinueUpdateRunnable final
+ : public LifeCycleEventCallback {
+ nsMainThreadPtrHandle<ServiceWorkerUpdateJob> mJob;
+ bool mSuccess;
+
+ public:
+ explicit ContinueUpdateRunnable(
+ const nsMainThreadPtrHandle<ServiceWorkerUpdateJob>& aJob)
+ : mJob(aJob), mSuccess(false) {
+ MOZ_ASSERT(NS_IsMainThread());
+ }
+
+ void SetResult(bool aResult) override { mSuccess = aResult; }
+
+ NS_IMETHOD
+ Run() override {
+ MOZ_ASSERT(NS_IsMainThread());
+ mJob->ContinueUpdateAfterScriptEval(mSuccess);
+ mJob = nullptr;
+ return NS_OK;
+ }
+};
+
+class ServiceWorkerUpdateJob::ContinueInstallRunnable final
+ : public LifeCycleEventCallback {
+ nsMainThreadPtrHandle<ServiceWorkerUpdateJob> mJob;
+ bool mSuccess;
+
+ public:
+ explicit ContinueInstallRunnable(
+ const nsMainThreadPtrHandle<ServiceWorkerUpdateJob>& aJob)
+ : mJob(aJob), mSuccess(false) {
+ MOZ_ASSERT(NS_IsMainThread());
+ }
+
+ void SetResult(bool aResult) override { mSuccess = aResult; }
+
+ NS_IMETHOD
+ Run() override {
+ MOZ_ASSERT(NS_IsMainThread());
+ mJob->ContinueAfterInstallEvent(mSuccess);
+ mJob = nullptr;
+ return NS_OK;
+ }
+};
+
+ServiceWorkerUpdateJob::ServiceWorkerUpdateJob(
+ nsIPrincipal* aPrincipal, const nsACString& aScope, nsCString aScriptSpec,
+ ServiceWorkerUpdateViaCache aUpdateViaCache)
+ : ServiceWorkerUpdateJob(Type::Update, aPrincipal, aScope,
+ std::move(aScriptSpec), aUpdateViaCache) {}
+
+already_AddRefed<ServiceWorkerRegistrationInfo>
+ServiceWorkerUpdateJob::GetRegistration() const {
+ MOZ_ASSERT(NS_IsMainThread());
+ RefPtr<ServiceWorkerRegistrationInfo> ref = mRegistration;
+ return ref.forget();
+}
+
+ServiceWorkerUpdateJob::ServiceWorkerUpdateJob(
+ Type aType, nsIPrincipal* aPrincipal, const nsACString& aScope,
+ nsCString aScriptSpec, ServiceWorkerUpdateViaCache aUpdateViaCache)
+ : ServiceWorkerJob(aType, aPrincipal, aScope, std::move(aScriptSpec)),
+ mUpdateViaCache(aUpdateViaCache),
+ mOnFailure(serviceWorkerScriptCache::OnFailure::DoNothing) {}
+
+ServiceWorkerUpdateJob::~ServiceWorkerUpdateJob() = default;
+
+void ServiceWorkerUpdateJob::FailUpdateJob(ErrorResult& aRv) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aRv.Failed());
+
+ // Cleanup after a failed installation. This essentially implements
+ // step 13 of the Install algorithm.
+ //
+ // https://w3c.github.io/ServiceWorker/#installation-algorithm
+ //
+ // The spec currently only runs this after an install event fails,
+ // but we must handle many more internal errors. So we check for
+ // cleanup on every non-successful exit.
+ if (mRegistration) {
+ // Some kinds of failures indicate there is something broken in the
+ // currently installed registration. In these cases we want to fully
+ // unregister.
+ if (mOnFailure == OnFailure::Uninstall) {
+ mRegistration->ClearAsCorrupt();
+ }
+
+ // Otherwise just clear the workers we may have created as part of the
+ // update process.
+ else {
+ mRegistration->ClearEvaluating();
+ mRegistration->ClearInstalling();
+ }
+
+ RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance();
+ if (swm) {
+ swm->MaybeRemoveRegistration(mRegistration);
+
+ // Also clear the registration on disk if we are forcing uninstall
+ // due to a particularly bad failure.
+ if (mOnFailure == OnFailure::Uninstall) {
+ swm->MaybeSendUnregister(mRegistration->Principal(),
+ mRegistration->Scope());
+ }
+ }
+ }
+
+ mRegistration = nullptr;
+
+ Finish(aRv);
+}
+
+void ServiceWorkerUpdateJob::FailUpdateJob(nsresult aRv) {
+ ErrorResult rv(aRv);
+ FailUpdateJob(rv);
+}
+
+void ServiceWorkerUpdateJob::AsyncExecute() {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(GetType() == Type::Update);
+
+ RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance();
+ if (Canceled() || !swm) {
+ FailUpdateJob(NS_ERROR_DOM_ABORT_ERR);
+ return;
+ }
+
+ // Invoke Update algorithm:
+ // https://w3c.github.io/ServiceWorker/#update-algorithm
+ //
+ // "Let registration be the result of running the Get Registration algorithm
+ // passing job’s scope url as the argument."
+ RefPtr<ServiceWorkerRegistrationInfo> registration =
+ swm->GetRegistration(mPrincipal, mScope);
+
+ if (!registration) {
+ ErrorResult rv;
+ rv.ThrowTypeError<MSG_SW_UPDATE_BAD_REGISTRATION>(mScope, "uninstalled");
+ FailUpdateJob(rv);
+ return;
+ }
+
+ // "Let newestWorker be the result of running Get Newest Worker algorithm
+ // passing registration as the argument."
+ RefPtr<ServiceWorkerInfo> newest = registration->Newest();
+
+ // "If job’s job type is update, and newestWorker is not null and its script
+ // url does not equal job’s script url, then:
+ // 1. Invoke Reject Job Promise with job and TypeError.
+ // 2. Invoke Finish Job with job and abort these steps."
+ if (newest && !newest->ScriptSpec().Equals(mScriptSpec)) {
+ ErrorResult rv;
+ rv.ThrowTypeError<MSG_SW_UPDATE_BAD_REGISTRATION>(mScope, "changed");
+ FailUpdateJob(rv);
+ return;
+ }
+
+ SetRegistration(registration);
+ Update();
+}
+
+void ServiceWorkerUpdateJob::SetRegistration(
+ ServiceWorkerRegistrationInfo* aRegistration) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ MOZ_ASSERT(!mRegistration);
+ MOZ_ASSERT(aRegistration);
+ mRegistration = aRegistration;
+}
+
+void ServiceWorkerUpdateJob::Update() {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(!Canceled());
+
+ // SetRegistration() must be called before Update().
+ MOZ_ASSERT(mRegistration);
+ MOZ_ASSERT(!mRegistration->GetInstalling());
+
+ // Begin the script download and comparison steps starting at step 5
+ // of the Update algorithm.
+
+ RefPtr<ServiceWorkerInfo> workerInfo = mRegistration->Newest();
+ nsAutoString cacheName;
+
+ // If the script has not changed, we need to perform a byte-for-byte
+ // comparison.
+ if (workerInfo && workerInfo->ScriptSpec().Equals(mScriptSpec)) {
+ cacheName = workerInfo->CacheName();
+ }
+
+ RefPtr<CompareCallback> callback = new CompareCallback(this);
+
+ nsresult rv = serviceWorkerScriptCache::Compare(
+ mRegistration, mPrincipal, cacheName, NS_ConvertUTF8toUTF16(mScriptSpec),
+ callback);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ FailUpdateJob(rv);
+ return;
+ }
+}
+
+ServiceWorkerUpdateViaCache ServiceWorkerUpdateJob::GetUpdateViaCache() const {
+ return mUpdateViaCache;
+}
+
+void ServiceWorkerUpdateJob::ComparisonResult(nsresult aStatus,
+ bool aInCacheAndEqual,
+ OnFailure aOnFailure,
+ const nsAString& aNewCacheName,
+ const nsACString& aMaxScope,
+ nsLoadFlags aLoadFlags) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ mOnFailure = aOnFailure;
+
+ RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance();
+ if (NS_WARN_IF(Canceled() || !swm)) {
+ FailUpdateJob(NS_ERROR_DOM_ABORT_ERR);
+ return;
+ }
+
+ // Handle failure of the download or comparison. This is part of Update
+ // step 5 as "If the algorithm asynchronously completes with null, then:".
+ if (NS_WARN_IF(NS_FAILED(aStatus))) {
+ FailUpdateJob(aStatus);
+ return;
+ }
+
+ // The spec validates the response before performing the byte-for-byte check.
+ // Here we perform the comparison in another module and then validate the
+ // script URL and scope. Make sure to do this validation before accepting
+ // an byte-for-byte match since the service-worker-allowed header might have
+ // changed since the last time it was installed.
+
+ // This is step 2 the "validate response" section of Update algorithm step 5.
+ // Step 1 is performed in the serviceWorkerScriptCache code.
+
+ nsCOMPtr<nsIURI> scriptURI;
+ nsresult rv = NS_NewURI(getter_AddRefs(scriptURI), mScriptSpec);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ FailUpdateJob(NS_ERROR_DOM_SECURITY_ERR);
+ return;
+ }
+
+ nsCOMPtr<nsIURI> maxScopeURI;
+ if (!aMaxScope.IsEmpty()) {
+ rv = NS_NewURI(getter_AddRefs(maxScopeURI), aMaxScope, nullptr, scriptURI);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ FailUpdateJob(NS_ERROR_DOM_SECURITY_ERR);
+ return;
+ }
+ }
+
+ nsAutoCString defaultAllowedPrefix;
+ rv = GetRequiredScopeStringPrefix(scriptURI, defaultAllowedPrefix,
+ eUseDirectory);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ FailUpdateJob(NS_ERROR_DOM_SECURITY_ERR);
+ return;
+ }
+
+ nsAutoCString maxPrefix(defaultAllowedPrefix);
+ if (maxScopeURI) {
+ rv = GetRequiredScopeStringPrefix(maxScopeURI, maxPrefix, eUsePath);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ FailUpdateJob(NS_ERROR_DOM_SECURITY_ERR);
+ return;
+ }
+ }
+
+ nsCOMPtr<nsIURI> scopeURI;
+ rv = NS_NewURI(getter_AddRefs(scopeURI), mRegistration->Scope(), nullptr,
+ scriptURI);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ FailUpdateJob(NS_ERROR_FAILURE);
+ return;
+ }
+
+ nsAutoCString scopeString;
+ rv = scopeURI->GetPathQueryRef(scopeString);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ FailUpdateJob(NS_ERROR_FAILURE);
+ return;
+ }
+
+ if (!StringBeginsWith(scopeString, maxPrefix)) {
+ nsAutoString message;
+ NS_ConvertUTF8toUTF16 reportScope(mRegistration->Scope());
+ NS_ConvertUTF8toUTF16 reportMaxPrefix(maxPrefix);
+
+ rv = nsContentUtils::FormatLocalizedString(
+ message, nsContentUtils::eDOM_PROPERTIES,
+ "ServiceWorkerScopePathMismatch", reportScope, reportMaxPrefix);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Failed to format localized string");
+ swm->ReportToAllClients(mScope, message, u""_ns, u""_ns, 0, 0,
+ nsIScriptError::errorFlag);
+ FailUpdateJob(NS_ERROR_DOM_SECURITY_ERR);
+ return;
+ }
+
+ // The response has been validated, so now we can consider if its a
+ // byte-for-byte match. This is step 6 of the Update algorithm.
+ if (aInCacheAndEqual) {
+ Finish(NS_OK);
+ return;
+ }
+
+ Telemetry::Accumulate(Telemetry::SERVICE_WORKER_UPDATED, 1);
+
+ // Begin step 7 of the Update algorithm to evaluate the new script.
+ nsLoadFlags flags = aLoadFlags;
+ if (GetUpdateViaCache() == ServiceWorkerUpdateViaCache::None) {
+ flags |= nsIRequest::VALIDATE_ALWAYS;
+ }
+
+ RefPtr<ServiceWorkerInfo> sw = new ServiceWorkerInfo(
+ mRegistration->Principal(), mRegistration->Scope(), mRegistration->Id(),
+ mRegistration->Version(), mScriptSpec, aNewCacheName, flags);
+
+ // If the registration is corrupt enough to force an uninstall if the
+ // upgrade fails, then we want to make sure the upgrade takes effect
+ // if it succeeds. Therefore force the skip-waiting flag on to replace
+ // the broken worker after install.
+ if (aOnFailure == OnFailure::Uninstall) {
+ sw->SetSkipWaitingFlag();
+ }
+
+ mRegistration->SetEvaluating(sw);
+
+ nsMainThreadPtrHandle<ServiceWorkerUpdateJob> handle(
+ new nsMainThreadPtrHolder<ServiceWorkerUpdateJob>(
+ "ServiceWorkerUpdateJob", this));
+ RefPtr<LifeCycleEventCallback> callback = new ContinueUpdateRunnable(handle);
+
+ ServiceWorkerPrivate* workerPrivate = sw->WorkerPrivate();
+ MOZ_ASSERT(workerPrivate);
+ rv = workerPrivate->CheckScriptEvaluation(callback);
+
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ FailUpdateJob(NS_ERROR_DOM_ABORT_ERR);
+ return;
+ }
+}
+
+void ServiceWorkerUpdateJob::ContinueUpdateAfterScriptEval(
+ bool aScriptEvaluationResult) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance();
+ if (Canceled() || !swm) {
+ FailUpdateJob(NS_ERROR_DOM_ABORT_ERR);
+ return;
+ }
+
+ // Step 7.5 of the Update algorithm verifying that the script evaluated
+ // successfully.
+
+ if (NS_WARN_IF(!aScriptEvaluationResult)) {
+ ErrorResult error;
+ error.ThrowTypeError<MSG_SW_SCRIPT_THREW>(mScriptSpec,
+ mRegistration->Scope());
+ FailUpdateJob(error);
+ return;
+ }
+
+ Install();
+}
+
+void ServiceWorkerUpdateJob::Install() {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_DIAGNOSTIC_ASSERT(!Canceled());
+
+ MOZ_ASSERT(!mRegistration->GetInstalling());
+
+ // Begin step 2 of the Install algorithm.
+ //
+ // https://slightlyoff.github.io/ServiceWorker/spec/service_worker/index.html#installation-algorithm
+
+ mRegistration->TransitionEvaluatingToInstalling();
+
+ // Step 6 of the Install algorithm resolving the job promise.
+ InvokeResultCallbacks(NS_OK);
+
+ // Queue a task to fire an event named updatefound at all the
+ // ServiceWorkerRegistration.
+ mRegistration->FireUpdateFound();
+
+ nsMainThreadPtrHandle<ServiceWorkerUpdateJob> handle(
+ new nsMainThreadPtrHolder<ServiceWorkerUpdateJob>(
+ "ServiceWorkerUpdateJob", this));
+ RefPtr<LifeCycleEventCallback> callback = new ContinueInstallRunnable(handle);
+
+ // Send the install event to the worker thread
+ ServiceWorkerPrivate* workerPrivate =
+ mRegistration->GetInstalling()->WorkerPrivate();
+ nsresult rv = workerPrivate->SendLifeCycleEvent(u"install"_ns, callback);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ ContinueAfterInstallEvent(false /* aSuccess */);
+ }
+}
+
+void ServiceWorkerUpdateJob::ContinueAfterInstallEvent(
+ bool aInstallEventSuccess) {
+ if (Canceled()) {
+ return FailUpdateJob(NS_ERROR_DOM_ABORT_ERR);
+ }
+
+ // If we haven't been canceled we should have a registration. There appears
+ // to be a path where it gets cleared before we call into here. Assert
+ // to try to catch this condition, but don't crash in release.
+ MOZ_DIAGNOSTIC_ASSERT(mRegistration);
+ if (!mRegistration) {
+ return FailUpdateJob(NS_ERROR_DOM_ABORT_ERR);
+ }
+
+ // Continue executing the Install algorithm at step 12.
+
+ // "If installFailed is true"
+ if (NS_WARN_IF(!aInstallEventSuccess)) {
+ // The installing worker is cleaned up by FailUpdateJob().
+ FailUpdateJob(NS_ERROR_DOM_ABORT_ERR);
+ return;
+ }
+
+ // Abort the update Job if the installWorker is null (e.g. when an extension
+ // is shutting down and all its workers have been terminated).
+ if (!mRegistration->GetInstalling()) {
+ return FailUpdateJob(NS_ERROR_DOM_ABORT_ERR);
+ }
+
+ mRegistration->TransitionInstallingToWaiting();
+
+ Finish(NS_OK);
+
+ // Step 20 calls for explicitly waiting for queued event tasks to fire.
+ // Instead, we simply queue a runnable to execute Activate. This ensures the
+ // events are flushed from the queue before proceeding.
+
+ // Step 22 of the Install algorithm. Activate is executed after the
+ // completion of this job. The controlling client and skipWaiting checks are
+ // performed in TryToActivate().
+ mRegistration->TryToActivateAsync();
+}
+
+} // namespace mozilla::dom
diff --git a/dom/serviceworkers/ServiceWorkerUpdateJob.h b/dom/serviceworkers/ServiceWorkerUpdateJob.h
new file mode 100644
index 0000000000..536aa72bfc
--- /dev/null
+++ b/dom/serviceworkers/ServiceWorkerUpdateJob.h
@@ -0,0 +1,97 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_serviceworkerupdatejob_h
+#define mozilla_dom_serviceworkerupdatejob_h
+
+#include "ServiceWorkerJob.h"
+#include "ServiceWorkerRegistration.h"
+
+#include "nsIRequest.h"
+
+namespace mozilla::dom {
+
+namespace serviceWorkerScriptCache {
+enum class OnFailure : uint8_t;
+} // namespace serviceWorkerScriptCache
+
+class ServiceWorkerManager;
+class ServiceWorkerRegistrationInfo;
+
+// A job class that performs the Update and Install algorithms from the
+// service worker spec. This class is designed to be inherited and customized
+// as a different job type. This is necessary because the register job
+// performs largely the same operations as the update job, but has a few
+// different starting steps.
+class ServiceWorkerUpdateJob : public ServiceWorkerJob {
+ public:
+ // Construct an update job to be used only for updates.
+ ServiceWorkerUpdateJob(nsIPrincipal* aPrincipal, const nsACString& aScope,
+ nsCString aScriptSpec,
+ ServiceWorkerUpdateViaCache aUpdateViaCache);
+
+ already_AddRefed<ServiceWorkerRegistrationInfo> GetRegistration() const;
+
+ protected:
+ // Construct an update job that is overriden as another job type.
+ ServiceWorkerUpdateJob(Type aType, nsIPrincipal* aPrincipal,
+ const nsACString& aScope, nsCString aScriptSpec,
+ ServiceWorkerUpdateViaCache aUpdateViaCache);
+
+ virtual ~ServiceWorkerUpdateJob();
+
+ // FailUpdateJob() must be called if an update job needs Finish() with
+ // an error.
+ void FailUpdateJob(ErrorResult& aRv);
+
+ void FailUpdateJob(nsresult aRv);
+
+ // The entry point when the update job is being used directly. Job
+ // types overriding this class should override this method to
+ // customize behavior.
+ virtual void AsyncExecute() override;
+
+ // Set the registration to be operated on by Update() or to be immediately
+ // returned as a result of the job. This must be called before Update().
+ void SetRegistration(ServiceWorkerRegistrationInfo* aRegistration);
+
+ // Execute the bulk of the update job logic using the registration defined
+ // by a previous SetRegistration() call. This can be called by the overriden
+ // AsyncExecute() to complete the job. The Update() method will always call
+ // Finish(). This method corresponds to the spec Update algorithm.
+ void Update();
+
+ ServiceWorkerUpdateViaCache GetUpdateViaCache() const;
+
+ private:
+ class CompareCallback;
+ class ContinueUpdateRunnable;
+ class ContinueInstallRunnable;
+
+ // Utility method called after a script is loaded and compared to
+ // our current cached script.
+ void ComparisonResult(nsresult aStatus, bool aInCacheAndEqual,
+ serviceWorkerScriptCache::OnFailure aOnFailure,
+ const nsAString& aNewCacheName,
+ const nsACString& aMaxScope, nsLoadFlags aLoadFlags);
+
+ // Utility method called after evaluating the worker script.
+ void ContinueUpdateAfterScriptEval(bool aScriptEvaluationResult);
+
+ // Utility method corresponding to the spec Install algorithm.
+ void Install();
+
+ // Utility method called after the install event is handled.
+ void ContinueAfterInstallEvent(bool aInstallEventSuccess);
+
+ RefPtr<ServiceWorkerRegistrationInfo> mRegistration;
+ ServiceWorkerUpdateViaCache mUpdateViaCache;
+ serviceWorkerScriptCache::OnFailure mOnFailure;
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_serviceworkerupdatejob_h
diff --git a/dom/serviceworkers/ServiceWorkerUtils.cpp b/dom/serviceworkers/ServiceWorkerUtils.cpp
new file mode 100644
index 0000000000..413b933be3
--- /dev/null
+++ b/dom/serviceworkers/ServiceWorkerUtils.cpp
@@ -0,0 +1,146 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "ServiceWorkerUtils.h"
+
+#include "mozilla/BasePrincipal.h"
+#include "mozilla/ErrorResult.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/dom/ClientInfo.h"
+#include "mozilla/dom/ServiceWorkerRegistrarTypes.h"
+#include "nsCOMPtr.h"
+#include "nsIPrincipal.h"
+#include "nsIURL.h"
+#include "nsPrintfCString.h"
+
+namespace mozilla::dom {
+
+bool ServiceWorkerRegistrationDataIsValid(
+ const ServiceWorkerRegistrationData& aData) {
+ return !aData.scope().IsEmpty() && !aData.currentWorkerURL().IsEmpty() &&
+ !aData.cacheName().IsEmpty();
+}
+
+namespace {
+
+void CheckForSlashEscapedCharsInPath(nsIURI* aURI, const char* aURLDescription,
+ ErrorResult& aRv) {
+ MOZ_ASSERT(aURI);
+
+ // A URL that can't be downcast to a standard URL is an invalid URL and should
+ // be treated as such and fail with SecurityError.
+ nsCOMPtr<nsIURL> url(do_QueryInterface(aURI));
+ if (NS_WARN_IF(!url)) {
+ // This really should not happen, since the caller checks that we
+ // have an http: or https: URL!
+ aRv.ThrowInvalidStateError("http: or https: URL without a concept of path");
+ return;
+ }
+
+ nsAutoCString path;
+ nsresult rv = url->GetFilePath(path);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ // Again, should not happen.
+ aRv.ThrowInvalidStateError("http: or https: URL without a concept of path");
+ return;
+ }
+
+ ToLowerCase(path);
+ if (path.Find("%2f") != kNotFound || path.Find("%5c") != kNotFound) {
+ nsPrintfCString err("%s contains %%2f or %%5c", aURLDescription);
+ aRv.ThrowTypeError(err);
+ }
+}
+
+} // anonymous namespace
+
+void ServiceWorkerScopeAndScriptAreValid(const ClientInfo& aClientInfo,
+ nsIURI* aScopeURI, nsIURI* aScriptURI,
+ ErrorResult& aRv) {
+ MOZ_DIAGNOSTIC_ASSERT(aScopeURI);
+ MOZ_DIAGNOSTIC_ASSERT(aScriptURI);
+
+ auto principalOrErr = aClientInfo.GetPrincipal();
+ if (NS_WARN_IF(principalOrErr.isErr())) {
+ aRv.ThrowInvalidStateError("Can't make security decisions about Client");
+ return;
+ }
+
+ auto hasHTTPScheme = [](nsIURI* aURI) -> bool {
+ return aURI->SchemeIs("http") || aURI->SchemeIs("https");
+ };
+ auto hasMozExtScheme = [](nsIURI* aURI) -> bool {
+ return aURI->SchemeIs("moz-extension");
+ };
+
+ nsCOMPtr<nsIPrincipal> principal = principalOrErr.unwrap();
+
+ auto isExtension = !!BasePrincipal::Cast(principal)->AddonPolicy();
+ auto hasValidURISchemes = !isExtension ? hasHTTPScheme : hasMozExtScheme;
+
+ // https://w3c.github.io/ServiceWorker/#start-register-algorithm step 3.
+ if (!hasValidURISchemes(aScriptURI)) {
+ auto message = !isExtension
+ ? "Script URL's scheme is not 'http' or 'https'"_ns
+ : "Script URL's scheme is not 'moz-extension'"_ns;
+ aRv.ThrowTypeError(message);
+ return;
+ }
+
+ // https://w3c.github.io/ServiceWorker/#start-register-algorithm step 4.
+ CheckForSlashEscapedCharsInPath(aScriptURI, "script URL", aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return;
+ }
+
+ // https://w3c.github.io/ServiceWorker/#start-register-algorithm step 8.
+ if (!hasValidURISchemes(aScopeURI)) {
+ auto message = !isExtension
+ ? "Scope URL's scheme is not 'http' or 'https'"_ns
+ : "Scope URL's scheme is not 'moz-extension'"_ns;
+ aRv.ThrowTypeError(message);
+ return;
+ }
+
+ // https://w3c.github.io/ServiceWorker/#start-register-algorithm step 9.
+ CheckForSlashEscapedCharsInPath(aScopeURI, "scope URL", aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return;
+ }
+
+ // The refs should really be empty coming in here, but if someone
+ // injects bad data into IPC, who knows. So let's revalidate that.
+ nsAutoCString ref;
+ Unused << aScopeURI->GetRef(ref);
+ if (NS_WARN_IF(!ref.IsEmpty())) {
+ aRv.ThrowSecurityError("Non-empty fragment on scope URL");
+ return;
+ }
+
+ Unused << aScriptURI->GetRef(ref);
+ if (NS_WARN_IF(!ref.IsEmpty())) {
+ aRv.ThrowSecurityError("Non-empty fragment on script URL");
+ return;
+ }
+
+ // Unfortunately we don't seem to have an obvious window id here; in
+ // particular ClientInfo does not have one.
+ nsresult rv = principal->CheckMayLoadWithReporting(
+ aScopeURI, false /* allowIfInheritsPrincipal */, 0 /* innerWindowID */);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ aRv.ThrowSecurityError("Scope URL is not same-origin with Client");
+ return;
+ }
+
+ rv = principal->CheckMayLoadWithReporting(
+ aScriptURI, false /* allowIfInheritsPrincipal */, 0 /* innerWindowID */);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ aRv.ThrowSecurityError("Script URL is not same-origin with Client");
+ return;
+ }
+}
+
+} // namespace mozilla::dom
diff --git a/dom/serviceworkers/ServiceWorkerUtils.h b/dom/serviceworkers/ServiceWorkerUtils.h
new file mode 100644
index 0000000000..ff4b9cf3d9
--- /dev/null
+++ b/dom/serviceworkers/ServiceWorkerUtils.h
@@ -0,0 +1,61 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#ifndef _mozilla_dom_ServiceWorkerUtils_h
+#define _mozilla_dom_ServiceWorkerUtils_h
+
+#include "mozilla/MozPromise.h"
+#include "mozilla/dom/IPCNavigationPreloadState.h"
+#include "mozilla/dom/ServiceWorkerRegistrationDescriptor.h"
+#include "nsTArray.h"
+
+class nsIURI;
+
+namespace mozilla {
+
+class CopyableErrorResult;
+class ErrorResult;
+
+namespace dom {
+
+class ClientInfo;
+class ServiceWorkerRegistrationData;
+class ServiceWorkerRegistrationDescriptor;
+struct NavigationPreloadState;
+
+using ServiceWorkerRegistrationPromise =
+ MozPromise<ServiceWorkerRegistrationDescriptor, CopyableErrorResult, false>;
+
+using ServiceWorkerRegistrationListPromise =
+ MozPromise<CopyableTArray<ServiceWorkerRegistrationDescriptor>,
+ CopyableErrorResult, false>;
+
+using NavigationPreloadStatePromise =
+ MozPromise<IPCNavigationPreloadState, CopyableErrorResult, false>;
+
+using ServiceWorkerRegistrationCallback =
+ std::function<void(const ServiceWorkerRegistrationDescriptor&)>;
+
+using ServiceWorkerRegistrationListCallback =
+ std::function<void(const nsTArray<ServiceWorkerRegistrationDescriptor>&)>;
+
+using ServiceWorkerBoolCallback = std::function<void(bool)>;
+
+using ServiceWorkerFailureCallback = std::function<void(ErrorResult&&)>;
+
+using NavigationPreloadGetStateCallback =
+ std::function<void(NavigationPreloadState&&)>;
+
+bool ServiceWorkerRegistrationDataIsValid(
+ const ServiceWorkerRegistrationData& aData);
+
+void ServiceWorkerScopeAndScriptAreValid(const ClientInfo& aClientInfo,
+ nsIURI* aScopeURI, nsIURI* aScriptURI,
+ ErrorResult& aRv);
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // _mozilla_dom_ServiceWorkerUtils_h
diff --git a/dom/serviceworkers/docs/telemetry.md b/dom/serviceworkers/docs/telemetry.md
new file mode 100644
index 0000000000..864b6bec25
--- /dev/null
+++ b/dom/serviceworkers/docs/telemetry.md
@@ -0,0 +1,42 @@
+1. ServiceWorkerRegistrar loading. The ability to determine whether to intercept
+ is based on this. (Although if not loaded, it's possible to just not intercept.)
+
+2. Process launching. ServiceWorkers need to be launched into a process, and
+ under fission this will almost certainly be a new process, and at startup we
+ might not be able to depend on preallocated processes.
+
+3. Permission transmission.
+
+4. Worker launching. The act of spawning the worker thread in the content process.
+
+5. Script loading.
+ Cache API opening for the given origin.
+ QuotaManager storage and temporary storage initialization, which has to
+ happen before the Cache API can start accessing its files.
+
+6. Fetch request serialization / deserialization to parent process.
+
+7. InterceptedHttpChannel creation for the fetch request.
+
+8. Creating FetchEvent related objects and propagting to the content process
+ worker thread.
+
+9. Handle FetchEvent by the ServiceWorker's script.
+
+10. Propagating the response from ServiceWorker to parent process.
+
+11. Synthesizing the response for the intercepted channel.
+
+12. Reset the interception by redirecting to a normal http channel or cancel the
+ interception.
+
+13. Push data into the intercepted channel.
+
+Telemetry probes cover:
+
+1: SERVICE_WORKER_REGISTRATION_LOADING
+2-4: SERVICE_WORKER_LAUNCH_TIME_2
+2-5, 7-13: SERVICE_WORKER_FETCH_INTERCEPTION_DURATION_MS_2
+7-9: SERVICE_WORKER_FETCH_EVENT_DISPATCH_MS_2
+11: SERVICE_WORKER_FETCH_EVENT_FINISH_SYNTHESIZED_RESPONSE_MS_2
+12: SERVICE_WORKER_FETCH_EVENT_CHANNEL_RESET_MS_2
diff --git a/dom/serviceworkers/moz.build b/dom/serviceworkers/moz.build
new file mode 100644
index 0000000000..c47bb8f74f
--- /dev/null
+++ b/dom/serviceworkers/moz.build
@@ -0,0 +1,130 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+with Files("**"):
+ BUG_COMPONENT = ("Core", "DOM: Service Workers")
+
+# Public stuff.
+EXPORTS.mozilla.dom += [
+ "FetchEventOpChild.h",
+ "FetchEventOpParent.h",
+ "FetchEventOpProxyChild.h",
+ "FetchEventOpProxyParent.h",
+ "NavigationPreloadManager.h",
+ "ServiceWorker.h",
+ "ServiceWorkerActors.h",
+ "ServiceWorkerChild.h",
+ "ServiceWorkerCloneData.h",
+ "ServiceWorkerContainer.h",
+ "ServiceWorkerContainerChild.h",
+ "ServiceWorkerContainerParent.h",
+ "ServiceWorkerDescriptor.h",
+ "ServiceWorkerEvents.h",
+ "ServiceWorkerInfo.h",
+ "ServiceWorkerInterceptController.h",
+ "ServiceWorkerIPCUtils.h",
+ "ServiceWorkerManager.h",
+ "ServiceWorkerManagerChild.h",
+ "ServiceWorkerManagerParent.h",
+ "ServiceWorkerOp.h",
+ "ServiceWorkerOpPromise.h",
+ "ServiceWorkerParent.h",
+ "ServiceWorkerRegistrar.h",
+ "ServiceWorkerRegistration.h",
+ "ServiceWorkerRegistrationChild.h",
+ "ServiceWorkerRegistrationDescriptor.h",
+ "ServiceWorkerRegistrationInfo.h",
+ "ServiceWorkerRegistrationParent.h",
+ "ServiceWorkerShutdownState.h",
+ "ServiceWorkerUtils.h",
+]
+
+UNIFIED_SOURCES += [
+ "FetchEventOpChild.cpp",
+ "FetchEventOpParent.cpp",
+ "FetchEventOpProxyChild.cpp",
+ "FetchEventOpProxyParent.cpp",
+ "NavigationPreloadManager.cpp",
+ "ServiceWorker.cpp",
+ "ServiceWorkerActors.cpp",
+ "ServiceWorkerChild.cpp",
+ "ServiceWorkerCloneData.cpp",
+ "ServiceWorkerContainer.cpp",
+ "ServiceWorkerContainerChild.cpp",
+ "ServiceWorkerContainerParent.cpp",
+ "ServiceWorkerContainerProxy.cpp",
+ "ServiceWorkerDescriptor.cpp",
+ "ServiceWorkerEvents.cpp",
+ "ServiceWorkerInfo.cpp",
+ "ServiceWorkerInterceptController.cpp",
+ "ServiceWorkerJob.cpp",
+ "ServiceWorkerJobQueue.cpp",
+ "ServiceWorkerManager.cpp",
+ "ServiceWorkerManagerParent.cpp",
+ "ServiceWorkerOp.cpp",
+ "ServiceWorkerParent.cpp",
+ "ServiceWorkerPrivate.cpp",
+ "ServiceWorkerProxy.cpp",
+ "ServiceWorkerQuotaUtils.cpp",
+ "ServiceWorkerRegisterJob.cpp",
+ "ServiceWorkerRegistrar.cpp",
+ "ServiceWorkerRegistration.cpp",
+ "ServiceWorkerRegistrationChild.cpp",
+ "ServiceWorkerRegistrationDescriptor.cpp",
+ "ServiceWorkerRegistrationInfo.cpp",
+ "ServiceWorkerRegistrationParent.cpp",
+ "ServiceWorkerRegistrationProxy.cpp",
+ "ServiceWorkerScriptCache.cpp",
+ "ServiceWorkerShutdownBlocker.cpp",
+ "ServiceWorkerShutdownState.cpp",
+ "ServiceWorkerUnregisterCallback.cpp",
+ "ServiceWorkerUnregisterJob.cpp",
+ "ServiceWorkerUpdateJob.cpp",
+ "ServiceWorkerUtils.cpp",
+]
+
+IPDL_SOURCES += [
+ "IPCNavigationPreloadState.ipdlh",
+ "IPCServiceWorkerDescriptor.ipdlh",
+ "IPCServiceWorkerRegistrationDescriptor.ipdlh",
+ "PFetchEventOp.ipdl",
+ "PFetchEventOpProxy.ipdl",
+ "PServiceWorker.ipdl",
+ "PServiceWorkerContainer.ipdl",
+ "PServiceWorkerManager.ipdl",
+ "PServiceWorkerRegistration.ipdl",
+ "ServiceWorkerOpArgs.ipdlh",
+ "ServiceWorkerRegistrarTypes.ipdlh",
+]
+
+LOCAL_INCLUDES += [
+ # For HttpBaseChannel.h dependencies
+ "/netwerk/base",
+ # For HttpBaseChannel.h
+ "/netwerk/protocol/http",
+]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+FINAL_LIBRARY = "xul"
+
+MOCHITEST_MANIFESTS += [
+ "test/mochitest-dFPI.ini",
+ "test/mochitest.ini",
+]
+
+MOCHITEST_CHROME_MANIFESTS += [
+ "test/chrome-dFPI.ini",
+ "test/chrome.ini",
+]
+
+BROWSER_CHROME_MANIFESTS += [
+ "test/browser-dFPI.ini",
+ "test/browser.ini",
+ "test/isolated/multi-e10s-update/browser.ini",
+]
+
+TEST_DIRS += ["test/gtest"]
diff --git a/dom/serviceworkers/test/ForceRefreshChild.sys.mjs b/dom/serviceworkers/test/ForceRefreshChild.sys.mjs
new file mode 100644
index 0000000000..b2b965be9e
--- /dev/null
+++ b/dom/serviceworkers/test/ForceRefreshChild.sys.mjs
@@ -0,0 +1,12 @@
+export class ForceRefreshChild extends JSWindowActorChild {
+ constructor() {
+ super();
+ }
+
+ handleEvent(evt) {
+ this.sendAsyncMessage("test:event", {
+ type: evt.type,
+ detail: evt.details,
+ });
+ }
+}
diff --git a/dom/serviceworkers/test/ForceRefreshParent.sys.mjs b/dom/serviceworkers/test/ForceRefreshParent.sys.mjs
new file mode 100644
index 0000000000..cb2d4809e9
--- /dev/null
+++ b/dom/serviceworkers/test/ForceRefreshParent.sys.mjs
@@ -0,0 +1,77 @@
+var maxCacheLoadCount = 3;
+var cachedLoadCount = 0;
+var baseLoadCount = 0;
+var done = false;
+
+export class ForceRefreshParent extends JSWindowActorParent {
+ constructor() {
+ super();
+ }
+
+ receiveMessage(msg) {
+ // if done is called, ignore the msg.
+ if (done) {
+ return;
+ }
+ if (msg.data.type === "base-load") {
+ baseLoadCount += 1;
+ if (cachedLoadCount === maxCacheLoadCount) {
+ ForceRefreshParent.SimpleTest.is(
+ baseLoadCount,
+ 2,
+ "cached load should occur before second base load"
+ );
+ done = true;
+ return ForceRefreshParent.done();
+ }
+ if (baseLoadCount !== 1) {
+ ForceRefreshParent.SimpleTest.ok(
+ false,
+ "base load without cached load should only occur once"
+ );
+ done = true;
+ return ForceRefreshParent.done();
+ }
+ } else if (msg.data.type === "base-register") {
+ ForceRefreshParent.SimpleTest.ok(
+ !cachedLoadCount,
+ "cached load should not occur before base register"
+ );
+ ForceRefreshParent.SimpleTest.is(
+ baseLoadCount,
+ 1,
+ "register should occur after first base load"
+ );
+ } else if (msg.data.type === "base-sw-ready") {
+ ForceRefreshParent.SimpleTest.ok(
+ !cachedLoadCount,
+ "cached load should not occur before base ready"
+ );
+ ForceRefreshParent.SimpleTest.is(
+ baseLoadCount,
+ 1,
+ "ready should occur after first base load"
+ );
+ ForceRefreshParent.refresh();
+ } else if (msg.data.type === "cached-load") {
+ ForceRefreshParent.SimpleTest.ok(
+ cachedLoadCount < maxCacheLoadCount,
+ "cached load should not occur too many times"
+ );
+ ForceRefreshParent.SimpleTest.is(
+ baseLoadCount,
+ 1,
+ "cache load occur after first base load"
+ );
+ cachedLoadCount += 1;
+ if (cachedLoadCount < maxCacheLoadCount) {
+ return ForceRefreshParent.refresh();
+ }
+ ForceRefreshParent.forceRefresh();
+ } else if (msg.data.type === "cached-failure") {
+ ForceRefreshParent.SimpleTest.ok(false, "failure: " + msg.data.detail);
+ done = true;
+ ForceRefreshParent.done();
+ }
+ }
+}
diff --git a/dom/serviceworkers/test/abrupt_completion_worker.js b/dom/serviceworkers/test/abrupt_completion_worker.js
new file mode 100644
index 0000000000..7afebc6d45
--- /dev/null
+++ b/dom/serviceworkers/test/abrupt_completion_worker.js
@@ -0,0 +1,18 @@
+function setMessageHandler(response) {
+ onmessage = e => {
+ e.source.postMessage(response);
+ };
+}
+
+setMessageHandler("handler-before-throw");
+
+// importScripts will throw when the ServiceWorker is past the "intalling" state.
+importScripts(`empty.js?${Date.now()}`);
+
+// When importScripts throws an uncaught exception, these calls should never be
+// made and the message handler should remain responding "handler-before-throw".
+setMessageHandler("handler-after-throw");
+
+// There needs to be a fetch handler to avoid the no-fetch optimizaiton,
+// which will skip starting up this worker.
+onfetch = e => e.respondWith(new Response("handler-after-throw"));
diff --git a/dom/serviceworkers/test/activate_event_error_worker.js b/dom/serviceworkers/test/activate_event_error_worker.js
new file mode 100644
index 0000000000..9f09cc5225
--- /dev/null
+++ b/dom/serviceworkers/test/activate_event_error_worker.js
@@ -0,0 +1,4 @@
+// Worker that errors on receiving an activate event.
+onactivate = function(e) {
+ undefined.doSomething;
+};
diff --git a/dom/serviceworkers/test/async_waituntil_worker.js b/dom/serviceworkers/test/async_waituntil_worker.js
new file mode 100644
index 0000000000..94219e4879
--- /dev/null
+++ b/dom/serviceworkers/test/async_waituntil_worker.js
@@ -0,0 +1,53 @@
+var keepAlivePromise;
+var resolvePromise;
+var result = "Failed";
+
+onactivate = function(event) {
+ event.waitUntil(clients.claim());
+};
+
+onmessage = function(event) {
+ if (event.data === "Start") {
+ event.waitUntil(Promise.reject());
+
+ keepAlivePromise = new Promise(function(resolve, reject) {
+ resolvePromise = resolve;
+ });
+
+ result = "Success";
+ event.waitUntil(keepAlivePromise);
+ event.source.postMessage("Started");
+ } else if (event.data === "Result") {
+ event.source.postMessage(result);
+ if (resolvePromise !== undefined) {
+ resolvePromise();
+ }
+ }
+};
+
+addEventListener("fetch", e => {
+ let respondWithPromise = new Promise(function(res, rej) {
+ setTimeout(() => {
+ res(new Response("ok"));
+ }, 0);
+ });
+ e.respondWith(respondWithPromise);
+ // Test that waitUntil can be called in the promise handler of the existing
+ // lifetime extension promise.
+ respondWithPromise.then(() => {
+ e.waitUntil(
+ clients.matchAll().then(cls => {
+ dump(`matchAll returned ${cls.length} client(s) with URLs:\n`);
+ cls.forEach(cl => {
+ dump(`${cl.url}\n`);
+ });
+
+ if (cls.length != 1) {
+ dump("ERROR: no controlled clients.\n");
+ }
+ client = cls[0];
+ client.postMessage("Done");
+ })
+ );
+ });
+});
diff --git a/dom/serviceworkers/test/blocking_install_event_worker.js b/dom/serviceworkers/test/blocking_install_event_worker.js
new file mode 100644
index 0000000000..5cb9270b04
--- /dev/null
+++ b/dom/serviceworkers/test/blocking_install_event_worker.js
@@ -0,0 +1,22 @@
+function postMessageToTest(msg) {
+ return clients.matchAll({ includeUncontrolled: true }).then(list => {
+ for (var client of list) {
+ if (client.url.endsWith("test_install_event_gc.html")) {
+ client.postMessage(msg);
+ break;
+ }
+ }
+ });
+}
+
+addEventListener("install", evt => {
+ // This must be a simple promise to trigger the CC failure.
+ evt.waitUntil(new Promise(function() {}));
+ postMessageToTest({ type: "INSTALL_EVENT" });
+});
+
+addEventListener("message", evt => {
+ if (evt.data.type === "ping") {
+ postMessageToTest({ type: "pong" });
+ }
+});
diff --git a/dom/serviceworkers/test/browser-common.ini b/dom/serviceworkers/test/browser-common.ini
new file mode 100644
index 0000000000..e1c8cfbc48
--- /dev/null
+++ b/dom/serviceworkers/test/browser-common.ini
@@ -0,0 +1,52 @@
+[DEFAULT]
+support-files =
+ browser_base_force_refresh.html
+ browser_cached_force_refresh.html
+ browser_head.js
+ download/window.html
+ download/worker.js
+ download_canceled/page_download_canceled.html
+ download_canceled/server-stream-download.sjs
+ download_canceled/sw_download_canceled.js
+ fetch.js
+ file_userContextId_openWindow.js
+ force_refresh_browser_worker.js
+ ForceRefreshChild.sys.mjs
+ ForceRefreshParent.sys.mjs
+ empty.html
+ empty_with_utils.html
+ empty.js
+ intercepted_channel_process_swap_worker.js
+ navigationPreload_page.html
+ network_with_utils.html
+ page_post_controlled.html
+ redirect.sjs
+ simple_fetch_worker.js
+ storage_recovery_worker.sjs
+ sw_respondwith_serviceworker.js
+ sw_with_navigationPreload.js
+ utils.js
+
+[browser_antitracking.js]
+[browser_antitracking_subiframes.js]
+[browser_devtools_serviceworker_interception.js]
+skip-if = serviceworker_e10s
+[browser_force_refresh.js]
+skip-if = verify # Bug 1603340
+[browser_download.js]
+[browser_download_canceled.js]
+skip-if = verify
+[browser_intercepted_channel_process_swap.js]
+skip-if = !fission
+[browser_intercepted_worker_script.js]
+[browser_navigation_fetch_fault_handling.js]
+skip-if =
+ os == "win" && os_version == "6.1" # Skip on Azure - frequent failure
+[browser_navigationPreload_read_after_respondWith.js]
+[browser_remote_type_process_swap.js]
+[browser_storage_permission.js]
+skip-if = true # Crashes: @ mozilla::dom::ServiceWorkerManagerService::PropagateUnregister(unsigned long, mozilla::ipc::PrincipalInfo const&, nsTSubstring<char16_t> const&), #Bug 1578337
+[browser_storage_recovery.js]
+[browser_unregister_with_containers.js]
+[browser_userContextId_openWindow.js]
+skip-if = true # See bug 1769437.
diff --git a/dom/serviceworkers/test/browser-dFPI.ini b/dom/serviceworkers/test/browser-dFPI.ini
new file mode 100644
index 0000000000..c327062f16
--- /dev/null
+++ b/dom/serviceworkers/test/browser-dFPI.ini
@@ -0,0 +1,7 @@
+[DEFAULT]
+# Enable dFPI(cookieBehavior 5) for service worker tests.
+prefs =
+ network.cookie.cookieBehavior=5
+dupe-manifest = true
+
+[include:browser-common.ini]
diff --git a/dom/serviceworkers/test/browser.ini b/dom/serviceworkers/test/browser.ini
new file mode 100644
index 0000000000..2a7162e9ed
--- /dev/null
+++ b/dom/serviceworkers/test/browser.ini
@@ -0,0 +1,4 @@
+[DEFAULT]
+dupe-manifest = true
+
+[include:browser-common.ini]
diff --git a/dom/serviceworkers/test/browser_antitracking.js b/dom/serviceworkers/test/browser_antitracking.js
new file mode 100644
index 0000000000..cb72f1c7e8
--- /dev/null
+++ b/dom/serviceworkers/test/browser_antitracking.js
@@ -0,0 +1,104 @@
+const BEHAVIOR_ACCEPT = Ci.nsICookieService.BEHAVIOR_ACCEPT;
+const BEHAVIOR_REJECT_TRACKER = Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER;
+
+let { UrlClassifierTestUtils } = ChromeUtils.import(
+ "resource://testing-common/UrlClassifierTestUtils.jsm"
+);
+
+const TOP_DOMAIN = "http://mochi.test:8888/";
+const SW_DOMAIN = "https://tracking.example.org/";
+
+const TOP_TEST_ROOT = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content/",
+ TOP_DOMAIN
+);
+const SW_TEST_ROOT = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content/",
+ SW_DOMAIN
+);
+
+const TOP_EMPTY_PAGE = `${TOP_TEST_ROOT}empty_with_utils.html`;
+const SW_REGISTER_PAGE = `${SW_TEST_ROOT}empty_with_utils.html`;
+const SW_IFRAME_PAGE = `${SW_TEST_ROOT}page_post_controlled.html`;
+// An empty script suffices for our SW needs; it's by definition no-fetch.
+const SW_REL_SW_SCRIPT = "empty.js";
+
+/**
+ * Set up a no-fetch-optimized ServiceWorker on a domain that will be covered by
+ * tracking protection (but is not yet). Once the SW is installed, activate TP
+ * and create a tab that embeds that tracking-site in an iframe.
+ */
+add_task(async function() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ ["network.cookie.cookieBehavior", BEHAVIOR_ACCEPT],
+ ],
+ });
+
+ // Open the top-level page.
+ info("Opening a new tab: " + SW_REGISTER_PAGE);
+ let topTab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ opening: SW_REGISTER_PAGE,
+ });
+
+ // ## Install SW
+ info("Installing SW");
+ await SpecialPowers.spawn(
+ topTab.linkedBrowser,
+ [{ sw: SW_REL_SW_SCRIPT }],
+ async function({ sw }) {
+ // Waive the xray to use the content utils.js script functions.
+ await content.wrappedJSObject.registerAndWaitForActive(sw);
+ }
+ );
+
+ // Enable Anti-tracking.
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["privacy.trackingprotection.enabled", false],
+ ["privacy.trackingprotection.pbmode.enabled", false],
+ ["privacy.trackingprotection.annotate_channels", true],
+ ["network.cookie.cookieBehavior", BEHAVIOR_REJECT_TRACKER],
+ ],
+ });
+ await UrlClassifierTestUtils.addTestTrackers();
+
+ // Open the top-level URL.
+ info("Loading a new top-level URL: " + TOP_EMPTY_PAGE);
+ let browserLoadedPromise = BrowserTestUtils.browserLoaded(
+ topTab.linkedBrowser
+ );
+ BrowserTestUtils.loadURI(topTab.linkedBrowser, TOP_EMPTY_PAGE);
+ await browserLoadedPromise;
+
+ // Create Iframe in the top-level page and verify its state.
+ let { controlled } = await SpecialPowers.spawn(
+ topTab.linkedBrowser,
+ [{ url: SW_IFRAME_PAGE }],
+ async function({ url }) {
+ const payload = await content.wrappedJSObject.createIframeAndWaitForMessage(
+ url
+ );
+ return payload;
+ }
+ );
+
+ ok(!controlled, "Should not be controlled!");
+
+ // ## Cleanup
+ info("Loading the SW unregister page: " + SW_REGISTER_PAGE);
+ browserLoadedPromise = BrowserTestUtils.browserLoaded(topTab.linkedBrowser);
+ BrowserTestUtils.loadURI(topTab.linkedBrowser, SW_REGISTER_PAGE);
+ await browserLoadedPromise;
+
+ await SpecialPowers.spawn(topTab.linkedBrowser, [], async function() {
+ await content.wrappedJSObject.unregisterAll();
+ });
+
+ // Close the testing tab.
+ BrowserTestUtils.removeTab(topTab);
+});
diff --git a/dom/serviceworkers/test/browser_antitracking_subiframes.js b/dom/serviceworkers/test/browser_antitracking_subiframes.js
new file mode 100644
index 0000000000..d2b7a97c96
--- /dev/null
+++ b/dom/serviceworkers/test/browser_antitracking_subiframes.js
@@ -0,0 +1,105 @@
+const BEHAVIOR_REJECT_TRACKER = Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER;
+
+const TOP_DOMAIN = "http://mochi.test:8888/";
+const SW_DOMAIN = "https://example.org/";
+
+const TOP_TEST_ROOT = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content/",
+ TOP_DOMAIN
+);
+const SW_TEST_ROOT = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content/",
+ SW_DOMAIN
+);
+
+const TOP_EMPTY_PAGE = `${TOP_TEST_ROOT}empty_with_utils.html`;
+const SW_REGISTER_PAGE = `${SW_TEST_ROOT}empty_with_utils.html`;
+const SW_IFRAME_PAGE = `${SW_TEST_ROOT}page_post_controlled.html`;
+const SW_REL_SW_SCRIPT = "empty.js";
+
+/**
+ * Set up a ServiceWorker on a domain that will be used as 3rd party iframe.
+ * That 3rd party frame should be controlled by the ServiceWorker.
+ * After that, we open a second iframe into the first one. That should not be
+ * controlled.
+ */
+add_task(async function() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ ["network.cookie.cookieBehavior", BEHAVIOR_REJECT_TRACKER],
+ ],
+ });
+
+ // Open the top-level page.
+ info("Opening a new tab: " + SW_REGISTER_PAGE);
+ let topTab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ opening: SW_REGISTER_PAGE,
+ });
+
+ // Install SW
+ info("Registering a SW: " + SW_REL_SW_SCRIPT);
+ await SpecialPowers.spawn(
+ topTab.linkedBrowser,
+ [{ sw: SW_REL_SW_SCRIPT }],
+ async function({ sw }) {
+ // Waive the xray to use the content utils.js script functions.
+ await content.wrappedJSObject.registerAndWaitForActive(sw);
+ // User interaction
+ content.document.userInteractionForTesting();
+ }
+ );
+
+ info("Loading a new top-level URL: " + TOP_EMPTY_PAGE);
+ let browserLoadedPromise = BrowserTestUtils.browserLoaded(
+ topTab.linkedBrowser
+ );
+ BrowserTestUtils.loadURI(topTab.linkedBrowser, TOP_EMPTY_PAGE);
+ await browserLoadedPromise;
+
+ // Create Iframe in the top-level page and verify its state.
+ info("Creating iframe and checking if controlled");
+ let { controlled } = await SpecialPowers.spawn(
+ topTab.linkedBrowser,
+ [{ url: SW_IFRAME_PAGE }],
+ async function({ url }) {
+ content.document.userInteractionForTesting();
+ const payload = await content.wrappedJSObject.createIframeAndWaitForMessage(
+ url
+ );
+ return payload;
+ }
+ );
+
+ ok(controlled, "Should be controlled!");
+
+ // Create a nested Iframe.
+ info("Creating nested-iframe and checking if controlled");
+ let { nested_controlled } = await SpecialPowers.spawn(
+ topTab.linkedBrowser,
+ [{ url: SW_IFRAME_PAGE }],
+ async function({ url }) {
+ const payload = await content.wrappedJSObject.createNestedIframeAndWaitForMessage(
+ url
+ );
+ return payload;
+ }
+ );
+
+ ok(!nested_controlled, "Should not be controlled!");
+
+ info("Loading the SW unregister page: " + SW_REGISTER_PAGE);
+ browserLoadedPromise = BrowserTestUtils.browserLoaded(topTab.linkedBrowser);
+ BrowserTestUtils.loadURI(topTab.linkedBrowser, SW_REGISTER_PAGE);
+ await browserLoadedPromise;
+
+ await SpecialPowers.spawn(topTab.linkedBrowser, [], async function() {
+ await content.wrappedJSObject.unregisterAll();
+ });
+
+ // Close the testing tab.
+ BrowserTestUtils.removeTab(topTab);
+});
diff --git a/dom/serviceworkers/test/browser_base_force_refresh.html b/dom/serviceworkers/test/browser_base_force_refresh.html
new file mode 100644
index 0000000000..1c3d02d42f
--- /dev/null
+++ b/dom/serviceworkers/test/browser_base_force_refresh.html
@@ -0,0 +1,27 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+</head>
+<body>
+<script type="text/javascript">
+addEventListener('load', function(event) {
+ navigator.serviceWorker.register('force_refresh_browser_worker.js').then(function(swr) {
+ if (!swr) {
+ return;
+ }
+ window.dispatchEvent(new Event("base-register", { bubbles: true }));
+ });
+
+ navigator.serviceWorker.ready.then(function() {
+ window.dispatchEvent(new Event("base-sw-ready", { bubbles: true }));
+ });
+
+ window.dispatchEvent(new Event("base-load", { bubbles: true }));
+});
+</script>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/browser_cached_force_refresh.html b/dom/serviceworkers/test/browser_cached_force_refresh.html
new file mode 100644
index 0000000000..faf2ee7a83
--- /dev/null
+++ b/dom/serviceworkers/test/browser_cached_force_refresh.html
@@ -0,0 +1,59 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+</head>
+<body>
+<script type="text/javascript">
+function ok(exp, msg) {
+ if (!exp) {
+ throw(msg);
+ }
+}
+
+function is(actual, expected, msg) {
+ if (actual !== expected) {
+ throw('got "' + actual + '", but expected "' + expected + '" - ' + msg);
+ }
+}
+
+function fail(err) {
+ window.dispatchEvent(new Event("cached-failure", { bubbles: true, detail: err }));
+}
+
+function getUncontrolledClients(sw) {
+ return new Promise(function(resolve, reject) {
+ navigator.serviceWorker.addEventListener('message', function onMsg(evt) {
+ if (evt.data.type === 'CLIENTS') {
+ navigator.serviceWorker.removeEventListener('message', onMsg);
+ resolve(evt.data.detail);
+ }
+ });
+ sw.postMessage({ type: 'GET_UNCONTROLLED_CLIENTS' })
+ });
+}
+
+addEventListener('load', function(event) {
+ if (!navigator.serviceWorker.controller) {
+ return fail(window.location.href + ' is not controlled!');
+ }
+
+ getUncontrolledClients(navigator.serviceWorker.controller)
+ .then(function(clientList) {
+ is(clientList.length, 1, 'should only have one client');
+ is(clientList[0].url, window.location.href,
+ 'client url should match current window');
+ is(clientList[0].frameType, 'top-level',
+ 'client should be a top-level window');
+ window.dispatchEvent(new Event('cached-load', { bubbles: true }));
+ })
+ .catch(function(err) {
+ fail(err);
+ });
+});
+</script>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/browser_devtools_serviceworker_interception.js b/dom/serviceworkers/test/browser_devtools_serviceworker_interception.js
new file mode 100644
index 0000000000..fc31159116
--- /dev/null
+++ b/dom/serviceworkers/test/browser_devtools_serviceworker_interception.js
@@ -0,0 +1,264 @@
+"use strict";
+
+const BASE_URI = "http://mochi.test:8888/browser/dom/serviceworkers/test/";
+const emptyDoc = BASE_URI + "empty.html";
+const fakeDoc = BASE_URI + "fake.html";
+const helloDoc = BASE_URI + "hello.html";
+
+const CROSS_URI = "http://example.com/browser/dom/serviceworkers/test/";
+const crossRedirect = CROSS_URI + "redirect";
+const crossHelloDoc = CROSS_URI + "hello.html";
+
+const sw = BASE_URI + "fetch.js";
+
+async function checkObserver(aInput) {
+ let interceptedChannel = null;
+
+ // We always get two channels which receive the "http-on-stop-request"
+ // notification if the service worker hijacks the request and respondWith an
+ // another fetch. One is for the "outer" window request when the other one is
+ // for the "inner" service worker request. Therefore, distinguish them by the
+ // order.
+ let waitForSecondOnStopRequest = aInput.intercepted;
+
+ let promiseResolve;
+
+ function observer(aSubject) {
+ let channel = aSubject.QueryInterface(Ci.nsIChannel);
+ // Since we cannot make sure that the network event triggered by the fetch()
+ // in this testcase is the very next event processed by ObserverService, we
+ // have to wait until we catch the one we want.
+ if (!channel.URI.spec.includes(aInput.expectedURL)) {
+ return;
+ }
+
+ if (waitForSecondOnStopRequest) {
+ waitForSecondOnStopRequest = false;
+ return;
+ }
+
+ // Wait for the service worker to intercept the request if it's expected to
+ // be intercepted
+ if (aInput.intercepted && interceptedChannel === null) {
+ return;
+ } else if (interceptedChannel) {
+ ok(
+ aInput.intercepted,
+ "Service worker intercepted the channel as expected"
+ );
+ } else {
+ ok(!aInput.intercepted, "The channel doesn't be intercepted");
+ }
+
+ var tc = interceptedChannel
+ ? interceptedChannel.QueryInterface(Ci.nsITimedChannel)
+ : aSubject.QueryInterface(Ci.nsITimedChannel);
+
+ // Check service worker related timings.
+ var serviceWorkerTimings = [
+ {
+ start: tc.launchServiceWorkerStartTime,
+ end: tc.launchServiceWorkerEndTime,
+ },
+ {
+ start: tc.dispatchFetchEventStartTime,
+ end: tc.dispatchFetchEventEndTime,
+ },
+ { start: tc.handleFetchEventStartTime, end: tc.handleFetchEventEndTime },
+ ];
+ if (!aInput.swPresent) {
+ serviceWorkerTimings.forEach(aTimings => {
+ is(aTimings.start, 0, "SW timings should be 0.");
+ is(aTimings.end, 0, "SW timings should be 0.");
+ });
+ }
+
+ // Check network related timings.
+ var networkTimings = [
+ tc.domainLookupStartTime,
+ tc.domainLookupEndTime,
+ tc.connectStartTime,
+ tc.connectEndTime,
+ tc.requestStartTime,
+ tc.responseStartTime,
+ tc.responseEndTime,
+ ];
+ if (aInput.fetch) {
+ networkTimings.reduce((aPreviousTiming, aCurrentTiming) => {
+ ok(aPreviousTiming <= aCurrentTiming, "Checking network timings");
+ return aCurrentTiming;
+ });
+ } else {
+ networkTimings.forEach(aTiming =>
+ is(aTiming, 0, "Network timings should be 0.")
+ );
+ }
+
+ interceptedChannel = null;
+ Services.obs.removeObserver(observer, topic);
+ promiseResolve();
+ }
+
+ function addInterceptedChannel(aSubject) {
+ let channel = aSubject.QueryInterface(Ci.nsIChannel);
+ if (!channel.URI.spec.includes(aInput.url)) {
+ return;
+ }
+
+ // Hold the interceptedChannel until checking timing information.
+ // Note: It's a interceptedChannel in the type of httpChannel
+ interceptedChannel = channel;
+ Services.obs.removeObserver(addInterceptedChannel, topic_SW);
+ }
+
+ const topic = "http-on-stop-request";
+ const topic_SW = "service-worker-synthesized-response";
+
+ Services.obs.addObserver(observer, topic);
+ if (aInput.intercepted) {
+ Services.obs.addObserver(addInterceptedChannel, topic_SW);
+ }
+
+ await new Promise(resolve => {
+ promiseResolve = resolve;
+ });
+}
+
+async function contentFetch(aURL) {
+ if (aURL.includes("redirect")) {
+ await content.window.fetch(aURL, { mode: "no-cors" });
+ return;
+ }
+ await content.window.fetch(aURL);
+}
+
+// The observer topics are fired in the parent process in parent-intercept
+// and the content process in child-intercept. This function will handle running
+// the check in the correct process. Note that it will block until the observers
+// are notified.
+async function fetchAndCheckObservers(
+ aFetchBrowser,
+ aObserverBrowser,
+ aTestCase
+) {
+ let promise = null;
+
+ promise = checkObserver(aTestCase);
+
+ await SpecialPowers.spawn(aFetchBrowser, [aTestCase.url], contentFetch);
+ await promise;
+}
+
+async function registerSWAndWaitForActive(aServiceWorker) {
+ let swr = await content.navigator.serviceWorker.register(aServiceWorker, {
+ scope: "empty.html",
+ });
+ await new Promise(resolve => {
+ let worker = swr.installing || swr.waiting || swr.active;
+ if (worker.state === "activated") {
+ return resolve();
+ }
+
+ worker.addEventListener("statechange", () => {
+ if (worker.state === "activated") {
+ return resolve();
+ }
+ });
+ });
+
+ await new Promise(resolve => {
+ if (content.navigator.serviceWorker.controller) {
+ return resolve();
+ }
+
+ content.navigator.serviceWorker.addEventListener(
+ "controllerchange",
+ resolve,
+ { once: true }
+ );
+ });
+}
+
+async function unregisterSW() {
+ let swr = await content.navigator.serviceWorker.getRegistration();
+ swr.unregister();
+}
+
+add_task(async function test_serivce_worker_interception() {
+ info("Setting the prefs to having e10s enabled");
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ // Make sure observer and testing function run in the same process
+ ["dom.ipc.processCount", 1],
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ ],
+ });
+
+ waitForExplicitFinish();
+
+ info("Open the tab");
+ let tab = BrowserTestUtils.addTab(gBrowser, emptyDoc);
+ let tabBrowser = gBrowser.getBrowserForTab(tab);
+ await BrowserTestUtils.browserLoaded(tabBrowser);
+
+ info("Open the tab for observing");
+ let tab_observer = BrowserTestUtils.addTab(gBrowser, emptyDoc);
+ let tabBrowser_observer = gBrowser.getBrowserForTab(tab_observer);
+ await BrowserTestUtils.browserLoaded(tabBrowser_observer);
+
+ let testcases = [
+ {
+ url: helloDoc,
+ expectedURL: helloDoc,
+ swPresent: false,
+ intercepted: false,
+ fetch: true,
+ },
+ {
+ url: fakeDoc,
+ expectedURL: helloDoc,
+ swPresent: true,
+ intercepted: true,
+ fetch: false, // should use HTTP cache
+ },
+ {
+ // Bypass http cache
+ url: helloDoc + "?ForBypassingHttpCache=" + Date.now(),
+ expectedURL: helloDoc,
+ swPresent: true,
+ intercepted: false,
+ fetch: true,
+ },
+ {
+ // no-cors mode redirect to no-cors mode (trigger internal redirect)
+ url: crossRedirect + "?url=" + crossHelloDoc + "&mode=no-cors",
+ expectedURL: crossHelloDoc,
+ swPresent: true,
+ redirect: "hello.html",
+ intercepted: true,
+ fetch: true,
+ },
+ ];
+
+ info("Test 1: Verify simple fetch");
+ await fetchAndCheckObservers(tabBrowser, tabBrowser_observer, testcases[0]);
+
+ info("Register a service worker");
+ await SpecialPowers.spawn(tabBrowser, [sw], registerSWAndWaitForActive);
+
+ info("Test 2: Verify simple hijack");
+ await fetchAndCheckObservers(tabBrowser, tabBrowser_observer, testcases[1]);
+
+ info("Test 3: Verify fetch without using http cache");
+ await fetchAndCheckObservers(tabBrowser, tabBrowser_observer, testcases[2]);
+
+ info("Test 4: make a internal redirect");
+ await fetchAndCheckObservers(tabBrowser, tabBrowser_observer, testcases[3]);
+
+ info("Clean up");
+ await SpecialPowers.spawn(tabBrowser, [undefined], unregisterSW);
+
+ gBrowser.removeTab(tab);
+ gBrowser.removeTab(tab_observer);
+});
diff --git a/dom/serviceworkers/test/browser_download.js b/dom/serviceworkers/test/browser_download.js
new file mode 100644
index 0000000000..271f217012
--- /dev/null
+++ b/dom/serviceworkers/test/browser_download.js
@@ -0,0 +1,97 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+var { Downloads } = ChromeUtils.importESModule(
+ "resource://gre/modules/Downloads.sys.mjs"
+);
+
+var gTestRoot = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content/",
+ "http://mochi.test:8888/"
+);
+
+function getFile(aFilename) {
+ if (aFilename.startsWith("file:")) {
+ var url = NetUtil.newURI(aFilename).QueryInterface(Ci.nsIFileURL);
+ return url.file.clone();
+ }
+
+ var file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+ file.initWithPath(aFilename);
+ return file;
+}
+
+function windowObserver(win, topic) {
+ if (topic !== "domwindowopened") {
+ return;
+ }
+
+ win.addEventListener(
+ "load",
+ function() {
+ if (
+ win.document.documentURI ===
+ "chrome://mozapps/content/downloads/unknownContentType.xhtml"
+ ) {
+ executeSoon(function() {
+ let dialog = win.document.getElementById("unknownContentType");
+ let button = dialog.getButton("accept");
+ button.disabled = false;
+ dialog.acceptDialog();
+ });
+ }
+ },
+ { once: true }
+ );
+}
+
+function test() {
+ waitForExplicitFinish();
+
+ Services.ww.registerNotification(windowObserver);
+
+ SpecialPowers.pushPrefEnv(
+ {
+ set: [
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ ],
+ },
+ function() {
+ var url = gTestRoot + "download/window.html";
+ var tab = BrowserTestUtils.addTab(gBrowser);
+ gBrowser.selectedTab = tab;
+
+ Downloads.getList(Downloads.ALL)
+ .then(function(downloadList) {
+ var downloadListener;
+
+ function downloadVerifier(aDownload) {
+ if (aDownload.succeeded) {
+ var file = getFile(aDownload.target.path);
+ ok(file.exists(), "download completed");
+ is(file.fileSize, 33, "downloaded file has correct size");
+ file.remove(false);
+ downloadList.remove(aDownload).catch(console.error);
+ downloadList.removeView(downloadListener).catch(console.error);
+ gBrowser.removeTab(tab);
+ Services.ww.unregisterNotification(windowObserver);
+
+ executeSoon(finish);
+ }
+ }
+
+ downloadListener = {
+ onDownloadAdded: downloadVerifier,
+ onDownloadChanged: downloadVerifier,
+ };
+
+ return downloadList.addView(downloadListener);
+ })
+ .then(function() {
+ BrowserTestUtils.loadURI(gBrowser, url);
+ });
+ }
+ );
+}
diff --git a/dom/serviceworkers/test/browser_download_canceled.js b/dom/serviceworkers/test/browser_download_canceled.js
new file mode 100644
index 0000000000..836a581acf
--- /dev/null
+++ b/dom/serviceworkers/test/browser_download_canceled.js
@@ -0,0 +1,176 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Test cancellation of a download in order to test edge-cases related to
+ * channel diversion. Channel diversion occurs in cases of file (and PSM cert)
+ * downloads where we realize in the child that we really want to consume the
+ * channel data in the parent. For data "sourced" by the parent, like network
+ * data, data streaming to the child is suspended and the parent waits for the
+ * child to send back the data it already received, then the channel is resumed.
+ * For data generated by the child, such as (the current, to be mooted by
+ * parent-intercept) child-side intercept, the data (currently) stream is
+ * continually pumped up to the parent.
+ *
+ * In particular, we want to reproduce the circumstances of Bug 1418795 where
+ * the child-side input-stream pump attempts to send data to the parent process
+ * but the parent has canceled the channel and so the IPC Actor has been torn
+ * down. Diversion begins once the nsURILoader receives the OnStartRequest
+ * notification with the headers, so there are two ways to produce
+ */
+
+const { Downloads } = ChromeUtils.importESModule(
+ "resource://gre/modules/Downloads.sys.mjs"
+);
+
+/**
+ * Clear the downloads list so other tests don't see our byproducts.
+ */
+async function clearDownloads() {
+ const downloads = await Downloads.getList(Downloads.ALL);
+ downloads.removeFinished();
+}
+
+/**
+ * Returns a Promise that will be resolved once the download dialog shows up and
+ * we have clicked the given button.
+ */
+function promiseClickDownloadDialogButton(buttonAction) {
+ const uri = "chrome://mozapps/content/downloads/unknownContentType.xhtml";
+ return BrowserTestUtils.promiseAlertDialogOpen(buttonAction, uri, {
+ async callback(win) {
+ // nsHelperAppDlg.js currently uses an eval-based setTimeout(0) to invoke
+ // its postShowCallback that results in a misleading error to the console
+ // if we close the dialog before it gets a chance to run. Just a
+ // setTimeout is not sufficient because it appears we get our "load"
+ // listener before the document's, so we use TestUtils.waitForTick() to
+ // defer until after its load handler runs, then use setTimeout(0) to end
+ // up after its eval.
+ await TestUtils.waitForTick();
+
+ await new Promise(resolve => setTimeout(resolve, 0));
+
+ const button = win.document
+ .getElementById("unknownContentType")
+ .getButton(buttonAction);
+ button.disabled = false;
+ info(`clicking ${buttonAction} button`);
+ button.click();
+ },
+ });
+}
+
+async function performCanceledDownload(tab, path) {
+ // If we're going to show a modal dialog for this download, then we should
+ // use it to cancel the download. If not, then we have to let the download
+ // start and then call into the downloads API ourselves to cancel it.
+ // We use this promise to signal the cancel being complete in either case.
+ let cancelledDownload;
+
+ if (
+ Services.prefs.getBoolPref(
+ "browser.download.always_ask_before_handling_new_types",
+ false
+ )
+ ) {
+ // Start waiting for the download dialog before triggering the download.
+ cancelledDownload = promiseClickDownloadDialogButton("cancel");
+ // Wait for the cancelation to have been triggered.
+ info("waiting for download popup");
+ } else {
+ let downloadView;
+ cancelledDownload = new Promise(resolve => {
+ downloadView = {
+ onDownloadAdded(aDownload) {
+ aDownload.cancel();
+ resolve();
+ },
+ };
+ });
+ const downloadList = await Downloads.getList(Downloads.ALL);
+ await downloadList.addView(downloadView);
+ }
+
+ // Trigger the download.
+ info(`triggering download of "${path}"`);
+ /* eslint-disable no-shadow */
+ await SpecialPowers.spawn(tab.linkedBrowser, [path], function(path) {
+ // Put a Promise in place that we can wait on for stream closure.
+ content.wrappedJSObject.trackStreamClosure(path);
+ // Create the link and trigger the download.
+ const link = content.document.createElement("a");
+ link.href = path;
+ link.download = path;
+ content.document.body.appendChild(link);
+ link.click();
+ });
+ /* eslint-enable no-shadow */
+
+ // Wait for the download to cancel.
+ await cancelledDownload;
+ info("cancelled download");
+
+ // Wait for confirmation that the stream stopped.
+ info(`wait for the ${path} stream to close.`);
+ /* eslint-disable no-shadow */
+ const why = await SpecialPowers.spawn(tab.linkedBrowser, [path], function(
+ path
+ ) {
+ return content.wrappedJSObject.streamClosed[path].promise;
+ });
+ /* eslint-enable no-shadow */
+ is(why.why, "canceled", "Ensure the stream canceled instead of timing out.");
+ // Note that for the "sw-stream-download" case, we end up with a bogus
+ // reason of "'close' may only be called on a stream in the 'readable' state."
+ // Since we aren't actually invoking close(), I'm assuming this is an
+ // implementation bug that will be corrected in the web platform tests.
+ info(`Cancellation reason: ${why.message} after ${why.ticks} ticks`);
+}
+
+const gTestRoot = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content/",
+ "http://mochi.test:8888/"
+);
+
+const PAGE_URL = `${gTestRoot}download_canceled/page_download_canceled.html`;
+
+add_task(async function interruptedDownloads() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ ],
+ });
+
+ // Open the tab
+ const tab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ opening: PAGE_URL,
+ });
+
+ // Wait for it to become controlled. Check that it was a promise that
+ // resolved as expected rather than undefined by checking the return value.
+ const controlled = await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [],
+ function() {
+ // This is a promise set up by the page during load, and we are post-load.
+ return content.wrappedJSObject.controlled;
+ }
+ );
+ is(controlled, "controlled", "page became controlled");
+
+ // Download a pass-through fetch stream.
+ await performCanceledDownload(tab, "sw-passthrough-download");
+
+ // Download a SW-generated stream
+ await performCanceledDownload(tab, "sw-stream-download");
+
+ // Cleanup
+ await SpecialPowers.spawn(tab.linkedBrowser, [], function() {
+ return content.wrappedJSObject.registration.unregister();
+ });
+ BrowserTestUtils.removeTab(tab);
+ await clearDownloads();
+});
diff --git a/dom/serviceworkers/test/browser_force_refresh.js b/dom/serviceworkers/test/browser_force_refresh.js
new file mode 100644
index 0000000000..11c3c3f1f7
--- /dev/null
+++ b/dom/serviceworkers/test/browser_force_refresh.js
@@ -0,0 +1,87 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+var gTestRoot = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content/",
+ "http://mochi.test:8888/"
+);
+
+async function refresh() {
+ EventUtils.synthesizeKey("R", { accelKey: true });
+}
+
+async function forceRefresh() {
+ EventUtils.synthesizeKey("R", { accelKey: true, shiftKey: true });
+}
+
+async function done() {
+ // unregister window actors
+ ChromeUtils.unregisterWindowActor("ForceRefresh");
+ let tab = gBrowser.selectedTab;
+ let tabBrowser = gBrowser.getBrowserForTab(tab);
+ await ContentTask.spawn(tabBrowser, null, async function() {
+ const swr = await content.navigator.serviceWorker.getRegistration();
+ await swr.unregister();
+ });
+
+ BrowserTestUtils.removeTab(tab);
+ executeSoon(finish);
+}
+
+function test() {
+ waitForExplicitFinish();
+ SpecialPowers.pushPrefEnv(
+ {
+ set: [
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ ["dom.caches.enabled", true],
+ ["browser.cache.disk.enable", false],
+ ["browser.cache.memory.enable", false],
+ ],
+ },
+ async function() {
+ // create ForceRefreseh window actor
+ const { ForceRefreshParent } = ChromeUtils.importESModule(
+ getRootDirectory(gTestPath) + "ForceRefreshParent.sys.mjs"
+ );
+
+ // setup helper functions for ForceRefreshParent
+ ForceRefreshParent.SimpleTest = SimpleTest;
+ ForceRefreshParent.refresh = refresh;
+ ForceRefreshParent.forceRefresh = forceRefresh;
+ ForceRefreshParent.done = done;
+
+ // setup window actor options
+ let windowActorOptions = {
+ parent: {
+ esModuleURI:
+ getRootDirectory(gTestPath) + "ForceRefreshParent.sys.mjs",
+ },
+ child: {
+ esModuleURI:
+ getRootDirectory(gTestPath) + "ForceRefreshChild.sys.mjs",
+ events: {
+ "base-register": { capture: true, wantUntrusted: true },
+ "base-sw-ready": { capture: true, wantUntrusted: true },
+ "base-load": { capture: true, wantUntrusted: true },
+ "cached-load": { capture: true, wantUntrusted: true },
+ "cached-failure": { capture: true, wantUntrusted: true },
+ },
+ },
+ allFrames: true,
+ };
+
+ // register ForceRefresh window actors
+ ChromeUtils.registerWindowActor("ForceRefresh", windowActorOptions);
+
+ // create a new tab and load test url
+ var url = gTestRoot + "browser_base_force_refresh.html";
+ var tab = BrowserTestUtils.addTab(gBrowser);
+ var tabBrowser = gBrowser.getBrowserForTab(tab);
+ gBrowser.selectedTab = tab;
+ BrowserTestUtils.loadURI(gBrowser, url);
+ }
+ );
+}
diff --git a/dom/serviceworkers/test/browser_head.js b/dom/serviceworkers/test/browser_head.js
new file mode 100644
index 0000000000..e1d8049308
--- /dev/null
+++ b/dom/serviceworkers/test/browser_head.js
@@ -0,0 +1,319 @@
+/**
+ * This file contains common functionality for ServiceWorker browser tests.
+ *
+ * Note that the normal auto-import mechanics for browser mochitests only
+ * handles "head.js", but we currently store all of our different varieties of
+ * mochitest in a single directory, which potentially results in a collision
+ * for similar heuristics for xpcshell.
+ *
+ * Many of the storage-related helpers in this file come from:
+ * https://searchfox.org/mozilla-central/source/dom/localstorage/test/unit/head.js
+ **/
+
+// To use this file, explicitly import it via (including the eslint comment):
+//
+// /* import-globals-from browser_head.js */
+// Services.scriptloader.loadSubScript("chrome://mochitests/content/browser/dom/serviceworkers/test/browser_head.js", this);
+
+// Find the current parent directory of the test context we're being loaded into
+// such that one can do `${originNoTrailingSlash}/${DIR_PATH}/file_in_dir.foo`.
+const DIR_PATH = getRootDirectory(gTestPath)
+ .replace("chrome://mochitests/content/", "")
+ .slice(0, -1);
+
+const SWM = Cc["@mozilla.org/serviceworkers/manager;1"].getService(
+ Ci.nsIServiceWorkerManager
+);
+
+// The expected minimum usage for an origin that has any Cache API storage in
+// use. Currently, the DB uses a page size of 4k and a minimum growth size of
+// 32k and has enough tables/indices for this to round up to 64k.
+const kMinimumOriginUsageBytes = 65536;
+
+function getPrincipal(url, attrs) {
+ const uri = Services.io.newURI(url);
+ if (!attrs) {
+ attrs = {};
+ }
+ return Services.scriptSecurityManager.createContentPrincipal(uri, attrs);
+}
+
+async function _qm_requestFinished(request) {
+ await new Promise(function(resolve) {
+ request.callback = function() {
+ resolve();
+ };
+ });
+
+ if (request.resultCode !== Cr.NS_OK) {
+ throw new RequestError(request.resultCode, request.resultName);
+ }
+
+ return request.result;
+}
+
+async function qm_reset_storage() {
+ return new Promise(resolve => {
+ let request = Services.qms.reset();
+ request.callback = resolve;
+ });
+}
+
+async function get_qm_origin_usage(origin) {
+ return new Promise(resolve => {
+ const principal = Services.scriptSecurityManager.createContentPrincipalFromOrigin(
+ origin
+ );
+ Services.qms.getUsageForPrincipal(principal, request =>
+ resolve(request.result.usage)
+ );
+ });
+}
+
+/**
+ * Clear the group associated with the given origin via nsIClearDataService. We
+ * are using nsIClearDataService here because nsIQuotaManagerService doesn't
+ * (directly) provide a means of clearing a group.
+ */
+async function clear_qm_origin_group_via_clearData(origin) {
+ const uri = Services.io.newURI(origin);
+ const baseDomain = Services.eTLD.getBaseDomain(uri);
+ info(`Clearing storage on domain ${baseDomain} (from origin ${origin})`);
+
+ // Initiate group clearing and wait for it.
+ await new Promise((resolve, reject) => {
+ Services.clearData.deleteDataFromBaseDomain(
+ baseDomain,
+ false,
+ Services.clearData.CLEAR_DOM_QUOTA,
+ failedFlags => {
+ if (failedFlags) {
+ reject(failedFlags);
+ } else {
+ resolve();
+ }
+ }
+ );
+ });
+}
+
+/**
+ * Look up the nsIServiceWorkerRegistrationInfo for a given SW descriptor.
+ */
+function swm_lookup_reg(swDesc) {
+ // Scopes always include the full origin.
+ const fullScope = `${swDesc.origin}/${DIR_PATH}/${swDesc.scope}`;
+ const principal = getPrincipal(fullScope);
+
+ const reg = SWM.getRegistrationByPrincipal(principal, fullScope);
+
+ return reg;
+}
+
+/**
+ * Install a ServiceWorker according to the provided descriptor by opening a
+ * fresh tab that will be closed when we are done. Returns the
+ * `nsIServiceWorkerRegistrationInfo` corresponding to the registration.
+ *
+ * The descriptor may have the following properties:
+ * - scope: Optional.
+ * - script: The script, which usually just wants to be a relative path.
+ * - origin: Requred, the origin (which should not include a trailing slash).
+ */
+async function install_sw(swDesc) {
+ info(
+ `Installing ServiceWorker ${swDesc.script} at ${swDesc.scope} on origin ${swDesc.origin}`
+ );
+ const pageUrlStr = `${swDesc.origin}/${DIR_PATH}/empty_with_utils.html`;
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: pageUrlStr,
+ },
+ async browser => {
+ await SpecialPowers.spawn(
+ browser,
+ [{ swScript: swDesc.script, swScope: swDesc.scope }],
+ async function({ swScript, swScope }) {
+ await content.wrappedJSObject.registerAndWaitForActive(
+ swScript,
+ swScope
+ );
+ }
+ );
+ }
+ );
+ info(`ServiceWorker installed`);
+
+ return swm_lookup_reg(swDesc);
+}
+
+/**
+ * Consume storage in the given origin by storing randomly generated Blobs into
+ * Cache API storage and IndexedDB storage. We use both APIs in order to
+ * ensure that data clearing wipes both QM clients.
+ *
+ * Randomly generated Blobs means Blobs with literally random content. This is
+ * done to compensate for the Cache API using snappy for compression.
+ */
+async function consume_storage(origin, storageDesc) {
+ info(`Consuming storage on origin ${origin}`);
+ const pageUrlStr = `${origin}/${DIR_PATH}/empty_with_utils.html`;
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: pageUrlStr,
+ },
+ async browser => {
+ await SpecialPowers.spawn(browser, [storageDesc], async function({
+ cacheBytes,
+ idbBytes,
+ }) {
+ await content.wrappedJSObject.fillStorage(cacheBytes, idbBytes);
+ });
+ }
+ );
+}
+
+// Check if the origin is effectively empty, but allowing for the minimum size
+// Cache API database to be present.
+function is_minimum_origin_usage(originUsageBytes) {
+ return originUsageBytes <= kMinimumOriginUsageBytes;
+}
+
+/**
+ * Perform a navigation, waiting until the navigation stops, then returning
+ * the `textContent` of the body node. The expectation is this will be used
+ * with ServiceWorkers that return a body that indicates the ServiceWorker
+ * provided the result (possibly derived from the request) versus if
+ * interception didn't happen.
+ */
+async function navigate_and_get_body(swDesc, debugTag) {
+ let pageUrlStr = `${swDesc.origin}/${DIR_PATH}/${swDesc.scope}`;
+ if (debugTag) {
+ pageUrlStr += "?" + debugTag;
+ }
+ info(`Navigating to ${pageUrlStr}`);
+
+ const tabResult = await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: pageUrlStr,
+ // In the event of an aborted navigation, the load event will never
+ // happen...
+ waitForLoad: false,
+ // ...but the stop will.
+ waitForStateStop: true,
+ },
+ async browser => {
+ info(` Tab opened, querying body content.`);
+ const spawnResult = await SpecialPowers.spawn(browser, [], function() {
+ const controlled = !!content.navigator.serviceWorker.controller;
+ // Special-case about: URL's.
+ let loc = content.document.documentURI;
+ if (loc.startsWith("about:")) {
+ // about:neterror is parameterized by query string, so truncate that
+ // off because our tests just care if we're seeing the neterror page.
+ const idxQuestion = loc.indexOf("?");
+ if (idxQuestion !== -1) {
+ loc = loc.substring(0, idxQuestion);
+ }
+ return { controlled, body: loc };
+ }
+ return {
+ controlled,
+ body: content.document?.body?.textContent?.trim(),
+ };
+ });
+
+ return spawnResult;
+ }
+ );
+
+ return tabResult;
+}
+
+function waitForIframeLoad(iframe) {
+ return new Promise(function(resolve) {
+ iframe.onload = resolve;
+ });
+}
+
+function waitForRegister(scope, callback) {
+ return new Promise(function(resolve) {
+ let listener = {
+ onRegister(registration) {
+ if (registration.scope !== scope) {
+ return;
+ }
+ SWM.removeListener(listener);
+ resolve(callback ? callback(registration) : registration);
+ },
+ };
+ SWM.addListener(listener);
+ });
+}
+
+function waitForUnregister(scope) {
+ return new Promise(function(resolve) {
+ let listener = {
+ onUnregister(registration) {
+ if (registration.scope !== scope) {
+ return;
+ }
+ SWM.removeListener(listener);
+ resolve(registration);
+ },
+ };
+ SWM.addListener(listener);
+ });
+}
+
+// Be careful using this helper function, please make sure QuotaUsageCheck must
+// happen, otherwise test would be stucked in this function.
+function waitForQuotaUsageCheckFinish(scope) {
+ return new Promise(function(resolve) {
+ let listener = {
+ onQuotaUsageCheckFinish(registration) {
+ if (registration.scope !== scope) {
+ return;
+ }
+ SWM.removeListener(listener);
+ resolve(registration);
+ },
+ };
+ SWM.addListener(listener);
+ });
+}
+
+function waitForServiceWorkerRegistrationChange(registration, callback) {
+ return new Promise(function(resolve) {
+ let listener = {
+ onChange() {
+ registration.removeListener(listener);
+ if (callback) {
+ callback();
+ }
+ resolve(callback ? callback() : undefined);
+ },
+ };
+ registration.addListener(listener);
+ });
+}
+
+function waitForServiceWorkerShutdown() {
+ return new Promise(function(resolve) {
+ let observer = {
+ observe(subject, topic, data) {
+ if (topic !== "service-worker-shutdown") {
+ return;
+ }
+ SpecialPowers.removeObserver(observer, "service-worker-shutdown");
+ resolve();
+ },
+ };
+ SpecialPowers.addObserver(observer, "service-worker-shutdown");
+ });
+}
diff --git a/dom/serviceworkers/test/browser_intercepted_channel_process_swap.js b/dom/serviceworkers/test/browser_intercepted_channel_process_swap.js
new file mode 100644
index 0000000000..f4722e91c0
--- /dev/null
+++ b/dom/serviceworkers/test/browser_intercepted_channel_process_swap.js
@@ -0,0 +1,110 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that navigation loads through intercepted channels result in the
+// appropriate process swaps. This appears to only be possible when navigating
+// to a cross-origin URL, where that navigation is controlled by a ServiceWorker.
+
+"use strict";
+
+const SAME_ORIGIN = "https://example.com";
+const CROSS_ORIGIN = "https://example.org";
+
+const SAME_ORIGIN_ROOT = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ SAME_ORIGIN
+);
+const CROSS_ORIGIN_ROOT = SAME_ORIGIN_ROOT.replace(SAME_ORIGIN, CROSS_ORIGIN);
+
+const SW_REGISTER_URL = `${CROSS_ORIGIN_ROOT}empty_with_utils.html`;
+const SW_SCRIPT_URL = `${CROSS_ORIGIN_ROOT}intercepted_channel_process_swap_worker.js`;
+const URL_BEFORE_NAVIGATION = `${SAME_ORIGIN_ROOT}empty.html`;
+const CROSS_ORIGIN_URL = `${CROSS_ORIGIN_ROOT}empty.html`;
+
+const TESTCASES = [
+ {
+ url: CROSS_ORIGIN_URL,
+ description:
+ "Controlled cross-origin navigation with network-provided response",
+ },
+ {
+ url: `${CROSS_ORIGIN_ROOT}this-path-does-not-exist?respondWith=${CROSS_ORIGIN_URL}`,
+ description:
+ "Controlled cross-origin navigation with ServiceWorker-provided response",
+ },
+];
+
+async function navigateTab(aTab, aUrl) {
+ BrowserTestUtils.loadURI(aTab.linkedBrowser, aUrl);
+
+ await BrowserTestUtils.waitForLocationChange(gBrowser, aUrl).then(() =>
+ BrowserTestUtils.browserStopped(aTab.linkedBrowser)
+ );
+}
+
+async function runTestcase(aTab, aTestcase) {
+ info(`Testing ${aTestcase.description}`);
+
+ await navigateTab(aTab, URL_BEFORE_NAVIGATION);
+
+ const [initialPid] = E10SUtils.getBrowserPids(aTab.linkedBrowser);
+
+ await navigateTab(aTab, aTestcase.url);
+
+ const [finalPid] = E10SUtils.getBrowserPids(aTab.linkedBrowser);
+
+ await SpecialPowers.spawn(aTab.linkedBrowser, [], () => {
+ Assert.ok(
+ content.navigator.serviceWorker.controller,
+ `${content.location} should be controlled.`
+ );
+ });
+
+ Assert.notEqual(
+ initialPid,
+ finalPid,
+ `Navigating from ${URL_BEFORE_NAVIGATION} to ${aTab.linkedBrowser.currentURI.spec} should have resulted in a different PID.`
+ );
+}
+
+add_task(async function setupPrefs() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ ],
+ });
+});
+
+add_task(async function setupBrowser() {
+ const tab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ opening: SW_REGISTER_URL,
+ });
+
+ await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [SW_SCRIPT_URL],
+ async scriptUrl => {
+ await content.wrappedJSObject.registerAndWaitForActive(scriptUrl);
+ }
+ );
+});
+
+add_task(async function runTestcases() {
+ for (const testcase of TESTCASES) {
+ await runTestcase(gBrowser.selectedTab, testcase);
+ }
+});
+
+add_task(async function cleanup() {
+ const tab = gBrowser.selectedTab;
+
+ await navigateTab(tab, SW_REGISTER_URL);
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async () => {
+ await content.wrappedJSObject.unregisterAll();
+ });
+
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/dom/serviceworkers/test/browser_intercepted_worker_script.js b/dom/serviceworkers/test/browser_intercepted_worker_script.js
new file mode 100644
index 0000000000..2e0542df07
--- /dev/null
+++ b/dom/serviceworkers/test/browser_intercepted_worker_script.js
@@ -0,0 +1,102 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * This test tests if the service worker is able to intercept the script loading
+ * channel of a dedicated worker.
+ *
+ * On success, the test will not crash.
+ */
+
+const SAME_ORIGIN = "https://example.com";
+
+const SAME_ORIGIN_ROOT = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ SAME_ORIGIN
+);
+
+const SW_REGISTER_URL = `${SAME_ORIGIN_ROOT}empty_with_utils.html`;
+const SW_SCRIPT_URL = `${SAME_ORIGIN_ROOT}simple_fetch_worker.js`;
+const SCRIPT_URL = `${SAME_ORIGIN_ROOT}empty.js`;
+
+async function navigateTab(aTab, aUrl) {
+ BrowserTestUtils.loadURI(aTab.linkedBrowser, aUrl);
+
+ await BrowserTestUtils.waitForLocationChange(gBrowser, aUrl).then(() =>
+ BrowserTestUtils.browserStopped(aTab.linkedBrowser)
+ );
+}
+
+async function runTest(aTestSharedWorker) {
+ const tab = gBrowser.selectedTab;
+
+ await navigateTab(tab, SW_REGISTER_URL);
+
+ await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [SCRIPT_URL, aTestSharedWorker],
+ async (scriptUrl, testSharedWorker) => {
+ await new Promise(resolve => {
+ content.navigator.serviceWorker.onmessage = e => {
+ if (e.data == scriptUrl) {
+ resolve();
+ }
+ };
+
+ if (testSharedWorker) {
+ let worker = new content.Worker(scriptUrl);
+ } else {
+ let worker = new content.SharedWorker(scriptUrl);
+ }
+ });
+ }
+ );
+
+ ok(true, "The service worker has intercepted the script loading.");
+}
+
+add_task(async function setupPrefs() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ [
+ "network.cookie.cookieBehavior",
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN,
+ ],
+ ],
+ });
+});
+
+add_task(async function setupBrowser() {
+ // The tab will be used by subsequent test steps via 'gBrowser.selectedTab'.
+ const tab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ opening: SW_REGISTER_URL,
+ });
+
+ registerCleanupFunction(async _ => {
+ await navigateTab(tab, SW_REGISTER_URL);
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async () => {
+ await content.wrappedJSObject.unregisterAll();
+ });
+
+ BrowserTestUtils.removeTab(tab);
+ });
+
+ await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [SW_SCRIPT_URL],
+ async scriptUrl => {
+ await content.wrappedJSObject.registerAndWaitForActive(scriptUrl);
+ }
+ );
+});
+
+add_task(async function runTests() {
+ await runTest(false);
+ await runTest(true);
+});
diff --git a/dom/serviceworkers/test/browser_navigationPreload_read_after_respondWith.js b/dom/serviceworkers/test/browser_navigationPreload_read_after_respondWith.js
new file mode 100644
index 0000000000..0e8a9eee20
--- /dev/null
+++ b/dom/serviceworkers/test/browser_navigationPreload_read_after_respondWith.js
@@ -0,0 +1,115 @@
+const TOP_DOMAIN = "http://mochi.test:8888/";
+const SW_DOMAIN = "https://example.org/";
+
+const TOP_TEST_ROOT = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content/",
+ TOP_DOMAIN
+);
+const SW_TEST_ROOT = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content/",
+ SW_DOMAIN
+);
+
+const TOP_EMPTY_PAGE = `${TOP_TEST_ROOT}empty_with_utils.html`;
+const SW_REGISTER_PAGE = `${SW_TEST_ROOT}empty_with_utils.html`;
+const SW_IFRAME_PAGE = `${SW_TEST_ROOT}navigationPreload_page.html`;
+// An empty script suffices for our SW needs; it's by definition no-fetch.
+const SW_REL_SW_SCRIPT = "sw_with_navigationPreload.js";
+
+/**
+ * Test the FetchEvent.preloadResponse can be read after FetchEvent.respondWith()
+ *
+ * Step 1. register a ServiceWorker which only handles FetchEvent when request
+ * url includes navigationPreload_page.html. Otherwise, it alwasy
+ * fallbacks the fetch to the network.
+ * If the request url includes navigationPreload_page.html, it call
+ * FetchEvent.respondWith() with a new Resposne, and then call
+ * FetchEvent.waitUtil() to wait FetchEvent.preloadResponse and post the
+ * preloadResponse's text to clients.
+ * Step 2. Open a controlled page and register message event handler to receive
+ * the postMessage from ServiceWorker.
+ * Step 3. Create a iframe which url is navigationPreload_page.html, such that
+ * ServiceWorker can fake the response and then send preloadResponse's
+ * result.
+ * Step 4. Unregister the ServiceWorker and cleanup the environment.
+ */
+add_task(async function() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ ],
+ });
+
+ // Step 1.
+ info("Opening a new tab: " + SW_REGISTER_PAGE);
+ let topTab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ opening: SW_REGISTER_PAGE,
+ });
+
+ // ## Install SW
+ await SpecialPowers.spawn(
+ topTab.linkedBrowser,
+ [{ sw: SW_REL_SW_SCRIPT }],
+ async function({ sw }) {
+ // Waive the xray to use the content utils.js script functions.
+ dump(`register serviceworker...\n`);
+ await content.wrappedJSObject.registerAndWaitForActive(sw);
+ }
+ );
+
+ // Step 2.
+ info("Loading a controlled page: " + SW_REGISTER_PAGE);
+ let browserLoadedPromise = BrowserTestUtils.browserLoaded(
+ topTab.linkedBrowser
+ );
+ BrowserTestUtils.loadURI(topTab.linkedBrowser, SW_REGISTER_PAGE);
+ await browserLoadedPromise;
+
+ info("Create a target iframe: " + SW_IFRAME_PAGE);
+ let result = await SpecialPowers.spawn(
+ topTab.linkedBrowser,
+ [{ url: SW_IFRAME_PAGE }],
+ async function({ url }) {
+ async function waitForNavigationPreload() {
+ return new Promise(resolve => {
+ content.wrappedJSObject.navigator.serviceWorker.addEventListener(
+ `message`,
+ event => {
+ resolve(event.data);
+ }
+ );
+ });
+ }
+
+ let promise = waitForNavigationPreload();
+
+ // Step 3.
+ const iframe = content.wrappedJSObject.document.createElement("iframe");
+ iframe.src = url;
+ content.wrappedJSObject.document.body.appendChild(iframe);
+ await new Promise(r => {
+ iframe.onload = r;
+ });
+
+ let result = await promise;
+ return result;
+ }
+ );
+
+ is(result, "NavigationPreload\n", "Should get NavigationPreload result");
+
+ // Step 4.
+ info("Loading the SW unregister page: " + SW_REGISTER_PAGE);
+ browserLoadedPromise = BrowserTestUtils.browserLoaded(topTab.linkedBrowser);
+ BrowserTestUtils.loadURI(topTab.linkedBrowser, SW_REGISTER_PAGE);
+ await browserLoadedPromise;
+
+ await SpecialPowers.spawn(topTab.linkedBrowser, [], async function() {
+ await content.wrappedJSObject.unregisterAll();
+ });
+
+ // Close the testing tab.
+ BrowserTestUtils.removeTab(topTab);
+});
diff --git a/dom/serviceworkers/test/browser_navigation_fetch_fault_handling.js b/dom/serviceworkers/test/browser_navigation_fetch_fault_handling.js
new file mode 100644
index 0000000000..6655c52a62
--- /dev/null
+++ b/dom/serviceworkers/test/browser_navigation_fetch_fault_handling.js
@@ -0,0 +1,272 @@
+/**
+ * This test file tests our automatic recovery and any related mitigating
+ * heuristics that occur during intercepted navigation fetch request.
+ * Specifically, we should be resetting interception so that we go to the
+ * network in these cases and then potentially taking actions like unregistering
+ * the ServiceWorker and/or clearing QuotaManager-managed storage for the
+ * origin.
+ *
+ * See specific test permutations for specific details inline in the test.
+ *
+ * NOTE THAT CURRENTLY THIS TEST IS DISCUSSING MITIGATIONS THAT ARE NOT YET
+ * IMPLEMENTED, JUST PLANNED. These will be iterated on and added to the rest
+ * of the stack of patches on Bug 1503072.
+ *
+ * ## Test Mechanics
+ *
+ * ### Fetch Fault Injection
+ *
+ * We expose:
+ * - On nsIServiceWorkerInfo, the per-ServiceWorker XPCOM interface:
+ * - A mechanism for creating synthetic faults by setting the
+ * `nsIServiceWorkerInfo::testingInjectCancellation` attribute to a failing
+ * nsresult. The fault is applied at the beginning of the steps to dispatch
+ * the fetch event on the global.
+ * - A count of the number of times we experienced these navigation faults
+ * that had to be reset as `nsIServiceWorkerInfo::navigationFaultCount`.
+ * (This would also include real faults, but we only expect to see synthetic
+ * faults in this test.)
+ * - On nsIServiceWorkerRegistrationInfo, the per-registration XPCOM interface:
+ * - A readonly attribute that indicates how many times an origin storage
+ * usage check has been initiated.
+ *
+ * We also use:
+ * - `nsIServiceWorkerManager::addListener(nsIServiceWorkerManagerListener)`
+ * allows our test to listen for the unregistration of registrations. This
+ * allows us to be notified when unregistering or origin-clearing actions have
+ * been taken as a mitigation.
+ *
+ * ### General Test Approach
+ *
+ * For each test we:
+ * - Ensure/confirm the testing origin has no QuotaManager storage in use.
+ * - Install the ServiceWorker.
+ * - If we are testing the situation where we want to simulate the origin being
+ * near its quota limit, we also generate Cache API and IDB storage usage
+ * sufficient to put our origin over the threshold.
+ * - We run a quota check on the origin after doing this in order to make sure
+ * that we did this correctly and that we properly constrained the limit for
+ * the origin. We fail the test for test implementation reasons if we
+ * didn't accomplish this.
+ * - Verify a fetch navigation to the SW works without any fault injection,
+ * producing a result produced by the ServiceWorker.
+ * - Begin fault permutations in a loop, where for each pass of the loop:
+ * - We trigger a navigation which will result in an intercepted fetch
+ * which will fault. We wait until the navigation completes.
+ * - We verify that we got the request from the network.
+ * - We verify that the ServiceWorker's navigationFaultCount increased.
+ * - If this the count at which we expect a mitigation to take place, we wait
+ * for the registration to become unregistered AND:
+ * - We check whether the storage for the origin was cleared or not, which
+ * indicates which mitigation of the following happened:
+ * - Unregister the registration directly.
+ * - Clear the origin's data which will also unregister the registration
+ * as a side effect.
+ * - We check whether the registration indicates an origin quota check
+ * happened or not.
+ *
+ * ### Disk Usage Limits
+ *
+ * In order to avoid gratuitous disk I/O and related overheads, we limit QM
+ * ("temporary") storage to 10 MiB which ends up limiting group usage to 10 MiB.
+ * This lets us set a threshold situation where we claim that a SW needs at
+ * least 4 MiB of storage for installation/operation, meaning that any usage
+ * beyond 6 MiB in the group will constitute a need to clear the group or
+ * origin. We fill with the storage with 8 MiB of artificial usage to this end,
+ * storing 4 MiB in Cache API and 4 MiB in IDB.
+ **/
+
+// Because of the amount of I/O involved in this test, pernosco reproductions
+// may experience timeouts without a timeout multiplier.
+requestLongerTimeout(2);
+
+/* import-globals-from browser_head.js */
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/dom/serviceworkers/test/browser_head.js",
+ this
+);
+
+// The origin we run the tests on.
+const TEST_ORIGIN = "https://test1.example.org";
+// An origin in the same group that impacts the usage of the TEST_ORIGIN. Used
+// to verify heuristics related to group-clearing (where clearing the
+// TEST_ORIGIN itself would not be sufficient for us to mitigate quota limits
+// being reached.)
+const SAME_GROUP_ORIGIN = "https://test2.example.org";
+
+const TEST_SW_SETUP = {
+ origin: TEST_ORIGIN,
+ // Page with a body textContent of "NETWORK" and has utils.js loaded.
+ scope: "network_with_utils.html",
+ // SW that serves a body with a textContent of "SERVICEWORKER" and
+ // has utils.js loaded.
+ script: "sw_respondwith_serviceworker.js",
+};
+
+const TEST_STORAGE_SETUP = {
+ cacheBytes: 4 * 1024 * 1024, // 4 MiB
+ idbBytes: 4 * 1024 * 1024, // 4 MiB
+};
+
+const FAULTS_BEFORE_MITIGATION = 3;
+
+/**
+ * Core test iteration logic.
+ *
+ * Parameters:
+ * - name: Human readable name of the fault we're injecting.
+ * - useError: The nsresult failure code to inject into fetch.
+ * - errorPage: The "about" page that we expect errors to leave us on.
+ * - consumeQuotaOrigin: If truthy, the origin to place the storage usage in.
+ * If falsey, we won't fill storage.
+ */
+async function do_fault_injection_test({
+ name,
+ useError,
+ errorPage,
+ consumeQuotaOrigin,
+}) {
+ info(
+ `### testing: error: ${name} (${useError}) consumeQuotaOrigin: ${consumeQuotaOrigin}`
+ );
+
+ // ## Ensure/confirm the testing origins have no QuotaManager storage in use.
+ await clear_qm_origin_group_via_clearData(TEST_ORIGIN);
+
+ // ## Install the ServiceWorker
+ const reg = await install_sw(TEST_SW_SETUP);
+ const sw = reg.activeWorker;
+
+ // ## Generate quota usage if appropriate
+ if (consumeQuotaOrigin) {
+ await consume_storage(consumeQuotaOrigin, TEST_STORAGE_SETUP);
+ }
+
+ // ## Verify normal navigation is served by the SW.
+ info(`## Checking normal operation.`);
+ {
+ const debugTag = `err=${name}&fault=0`;
+ const docInfo = await navigate_and_get_body(TEST_SW_SETUP, debugTag);
+ is(
+ docInfo.body,
+ "SERVICEWORKER",
+ "navigation without injected fault originates from ServiceWorker"
+ );
+
+ is(
+ docInfo.controlled,
+ true,
+ "successfully intercepted navigation should be controlled"
+ );
+ }
+
+ // Make sure the test is listening on the ServiceWorker unregistration, since
+ // we expect it happens after navigation fault threshold reached.
+ const unregisteredPromise = waitForUnregister(reg.scope);
+
+ // Make sure the test is listening on the finish of quota checking, since we
+ // expect it happens after navigation fault threshold reached.
+ const quotaUsageCheckFinishPromise = waitForQuotaUsageCheckFinish(reg.scope);
+
+ // ## Inject faults in a loop until expected mitigation.
+ sw.testingInjectCancellation = useError;
+ for (let iFault = 0; iFault < FAULTS_BEFORE_MITIGATION; iFault++) {
+ info(`## Testing with injected fault number ${iFault + 1}`);
+ // We should never have triggered an origin quota usage check before the
+ // final fault injection.
+ is(reg.quotaUsageCheckCount, 0, "No quota usage check yet");
+
+ // Make sure our loads encode the specific
+ const debugTag = `err=${name}&fault=${iFault + 1}`;
+
+ const docInfo = await navigate_and_get_body(TEST_SW_SETUP, debugTag);
+ // We should always be receiving network fallback.
+ is(
+ docInfo.body,
+ "NETWORK",
+ "navigation with injected fault originates from network"
+ );
+
+ is(docInfo.controlled, false, "bypassed pages shouldn't be controlled");
+
+ // The fault count should have increased
+ is(
+ sw.navigationFaultCount,
+ iFault + 1,
+ "navigation fault increased (to expected value)"
+ );
+ }
+
+ await unregisteredPromise;
+ is(reg.unregistered, true, "registration should be unregistered");
+
+ //is(reg.quotaUsageCheckCount, 1, "Quota usage check must be started");
+ await quotaUsageCheckFinishPromise;
+
+ if (consumeQuotaOrigin) {
+ // Check that there is no longer any storage usaged by the origin in this
+ // case.
+ const originUsage = await get_qm_origin_usage(TEST_ORIGIN);
+ ok(
+ is_minimum_origin_usage(originUsage),
+ "origin usage should be mitigated"
+ );
+
+ if (consumeQuotaOrigin === SAME_GROUP_ORIGIN) {
+ const sameGroupUsage = await get_qm_origin_usage(SAME_GROUP_ORIGIN);
+ ok(sameGroupUsage === 0, "same group usage should be mitigated");
+ }
+ }
+}
+
+add_task(async function test_navigation_fetch_fault_handling() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ ["dom.serviceWorkers.mitigations.bypass_on_fault", true],
+ ["dom.serviceWorkers.mitigations.group_usage_headroom_kb", 5 * 1024],
+ ["dom.quotaManager.testing", true],
+ // We want the temporary global limit to be 10 MiB (the pref is in KiB).
+ // This will result in the group limit also being 10 MiB because on small
+ // disks we provide a group limit value of min(10 MiB, global limit).
+ ["dom.quotaManager.temporaryStorage.fixedLimit", 10 * 1024],
+ ],
+ });
+
+ // Need to reset the storages to make dom.quotaManager.temporaryStorage.fixedLimit
+ // works.
+ await qm_reset_storage();
+
+ const quotaOriginVariations = [
+ // Don't put us near the storage limit.
+ undefined,
+ // Put us near the storage limit in the SW origin itself.
+ TEST_ORIGIN,
+ // Put us near the storage limit in the SW origin's group but not the origin
+ // itself.
+ SAME_GROUP_ORIGIN,
+ ];
+
+ for (const consumeQuotaOrigin of quotaOriginVariations) {
+ await do_fault_injection_test({
+ name: "NS_ERROR_DOM_ABORT_ERR",
+ useError: 0x80530014, // Not in `Cr`.
+ // Abort errors manifest as about:blank pages.
+ errorPage: "about:blank",
+ consumeQuotaOrigin,
+ });
+
+ await do_fault_injection_test({
+ name: "NS_ERROR_INTERCEPTION_FAILED",
+ useError: 0x804b0064, // Not in `Cr`.
+ // Interception failures manifest as corrupt content pages.
+ errorPage: "about:neterror",
+ consumeQuotaOrigin,
+ });
+ }
+
+ // Cleanup: wipe the origin and group so all the ServiceWorkers go away.
+ await clear_qm_origin_group_via_clearData(TEST_ORIGIN);
+});
diff --git a/dom/serviceworkers/test/browser_remote_type_process_swap.js b/dom/serviceworkers/test/browser_remote_type_process_swap.js
new file mode 100644
index 0000000000..60c8078e0d
--- /dev/null
+++ b/dom/serviceworkers/test/browser_remote_type_process_swap.js
@@ -0,0 +1,138 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * This test tests a navigation request to a Service Worker-controlled origin &
+ * scope that results in a cross-origin redirect to a
+ * non-Service Worker-controlled scope which additionally participates in
+ * cross-process redirect.
+ *
+ * On success, the test will not crash.
+ */
+
+const ORIGIN = "http://mochi.test:8888";
+const TEST_ROOT = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ ORIGIN
+);
+
+const SW_REGISTER_PAGE_URL = `${TEST_ROOT}empty_with_utils.html`;
+const SW_SCRIPT_URL = `${TEST_ROOT}empty.js`;
+
+const FILE_URL = (() => {
+ // Get the file as an nsIFile.
+ const file = getChromeDir(getResolvedURI(gTestPath));
+ file.append("empty.html");
+
+ // Convert the nsIFile to an nsIURI to access the path.
+ return Services.io.newFileURI(file).spec;
+})();
+
+const CROSS_ORIGIN = "https://example.com";
+const CROSS_ORIGIN_URL = SW_REGISTER_PAGE_URL.replace(ORIGIN, CROSS_ORIGIN);
+const CROSS_ORIGIN_REDIRECT_URL = `${TEST_ROOT}redirect.sjs?${CROSS_ORIGIN_URL}`;
+
+async function loadURI(aXULBrowser, aURI) {
+ const browserLoadedPromise = BrowserTestUtils.browserLoaded(aXULBrowser);
+ BrowserTestUtils.loadURI(aXULBrowser, aURI);
+
+ return browserLoadedPromise;
+}
+
+async function runTest() {
+ // Step 1: register a Service Worker under `ORIGIN` so that all subsequent
+ // requests to `ORIGIN` will be marked as controlled.
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ ["devtools.console.stdout.content", true],
+ ],
+ });
+
+ info(`Loading tab with page ${SW_REGISTER_PAGE_URL}`);
+ const tab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ opening: SW_REGISTER_PAGE_URL,
+ });
+ info(`Loaded page ${SW_REGISTER_PAGE_URL}`);
+
+ info(`Registering Service Worker ${SW_SCRIPT_URL}`);
+ await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [{ scriptURL: SW_SCRIPT_URL }],
+ async ({ scriptURL }) => {
+ await content.wrappedJSObject.registerAndWaitForActive(scriptURL);
+ }
+ );
+ info(`Registered and activated Service Worker ${SW_SCRIPT_URL}`);
+
+ // Step 2: open a page over file:// and navigate to trigger a process swap
+ // for the response.
+ info(`Loading ${FILE_URL}`);
+ await loadURI(tab.linkedBrowser, FILE_URL);
+
+ Assert.equal(
+ tab.linkedBrowser.remoteType,
+ E10SUtils.FILE_REMOTE_TYPE,
+ `${FILE_URL} should load in a file process`
+ );
+
+ info(`Dynamically creating ${FILE_URL}'s link`);
+ await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [{ href: CROSS_ORIGIN_REDIRECT_URL }],
+ ({ href }) => {
+ const { document } = content;
+ const link = document.createElement("a");
+ link.href = href;
+ link.id = "link";
+ link.appendChild(document.createTextNode(href));
+ document.body.appendChild(link);
+ }
+ );
+
+ const redirectPromise = BrowserTestUtils.waitForLocationChange(
+ gBrowser,
+ CROSS_ORIGIN_URL
+ );
+
+ info("Starting navigation");
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "#link",
+ {},
+ tab.linkedBrowser
+ );
+
+ info(`Waiting for location to change to ${CROSS_ORIGIN_URL}`);
+ await redirectPromise;
+
+ info("Waiting for the browser to stop");
+ await BrowserTestUtils.browserStopped(tab.linkedBrowser);
+
+ if (SpecialPowers.useRemoteSubframes) {
+ Assert.ok(
+ E10SUtils.isWebRemoteType(tab.linkedBrowser.remoteType),
+ `${CROSS_ORIGIN_URL} should load in a web-content process`
+ );
+ }
+
+ // Step 3: cleanup.
+ info("Loading initial page to unregister all Service Workers");
+ await loadURI(tab.linkedBrowser, SW_REGISTER_PAGE_URL);
+
+ info("Unregistering all Service Workers");
+ await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [],
+ async () => await content.wrappedJSObject.unregisterAll()
+ );
+
+ info("Closing tab");
+ BrowserTestUtils.removeTab(tab);
+}
+
+add_task(runTest);
diff --git a/dom/serviceworkers/test/browser_storage_permission.js b/dom/serviceworkers/test/browser_storage_permission.js
new file mode 100644
index 0000000000..68c2c18bad
--- /dev/null
+++ b/dom/serviceworkers/test/browser_storage_permission.js
@@ -0,0 +1,297 @@
+"use strict";
+
+const { PermissionTestUtils } = ChromeUtils.import(
+ "resource://testing-common/PermissionTestUtils.jsm"
+);
+
+const BASE_URI = "http://mochi.test:8888/browser/dom/serviceworkers/test/";
+const PAGE_URI = BASE_URI + "empty.html";
+const SCOPE = PAGE_URI + "?storage_permission";
+const SW_SCRIPT = BASE_URI + "empty.js";
+
+add_setup(async function() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ // Until the e10s refactor is complete, use a single process to avoid
+ // service worker propagation race.
+ ["dom.ipc.processCount", 1],
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ ],
+ });
+
+ let tab = BrowserTestUtils.addTab(gBrowser, PAGE_URI);
+ let browser = gBrowser.getBrowserForTab(tab);
+ await BrowserTestUtils.browserLoaded(browser);
+
+ await SpecialPowers.spawn(
+ browser,
+ [{ script: SW_SCRIPT, scope: SCOPE }],
+ async function(opts) {
+ let reg = await content.navigator.serviceWorker.register(opts.script, {
+ scope: opts.scope,
+ });
+ let worker = reg.installing || reg.waiting || reg.active;
+ await new Promise(resolve => {
+ if (worker.state === "activated") {
+ resolve();
+ return;
+ }
+ worker.addEventListener("statechange", function onStateChange() {
+ if (worker.state === "activated") {
+ worker.removeEventListener("statechange", onStateChange);
+ resolve();
+ }
+ });
+ });
+ }
+ );
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function test_allow_permission() {
+ PermissionTestUtils.add(
+ PAGE_URI,
+ "cookie",
+ Ci.nsICookiePermission.ACCESS_ALLOW
+ );
+
+ let tab = BrowserTestUtils.addTab(gBrowser, SCOPE);
+ let browser = gBrowser.getBrowserForTab(tab);
+ await BrowserTestUtils.browserLoaded(browser);
+
+ let controller = await SpecialPowers.spawn(browser, [], async function() {
+ return content.navigator.serviceWorker.controller;
+ });
+
+ ok(!!controller, "page should be controlled with storage allowed");
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function test_deny_permission() {
+ PermissionTestUtils.add(
+ PAGE_URI,
+ "cookie",
+ Ci.nsICookiePermission.ACCESS_DENY
+ );
+
+ let tab = BrowserTestUtils.addTab(gBrowser, SCOPE);
+ let browser = gBrowser.getBrowserForTab(tab);
+ await BrowserTestUtils.browserLoaded(browser);
+
+ let controller = await SpecialPowers.spawn(browser, [], async function() {
+ return content.navigator.serviceWorker.controller;
+ });
+
+ is(controller, null, "page should be not controlled with storage denied");
+
+ BrowserTestUtils.removeTab(tab);
+ PermissionTestUtils.remove(PAGE_URI, "cookie");
+});
+
+add_task(async function test_session_permission() {
+ PermissionTestUtils.add(
+ PAGE_URI,
+ "cookie",
+ Ci.nsICookiePermission.ACCESS_SESSION
+ );
+
+ let tab = BrowserTestUtils.addTab(gBrowser, SCOPE);
+ let browser = gBrowser.getBrowserForTab(tab);
+ await BrowserTestUtils.browserLoaded(browser);
+
+ let controller = await SpecialPowers.spawn(browser, [], async function() {
+ return content.navigator.serviceWorker.controller;
+ });
+
+ is(controller, null, "page should be not controlled with session storage");
+
+ BrowserTestUtils.removeTab(tab);
+ PermissionTestUtils.remove(PAGE_URI, "cookie");
+});
+
+// Test to verify an about:blank iframe successfully inherits the
+// parent's controller when storage is blocked between opening the
+// parent page and creating the iframe.
+add_task(async function test_block_storage_before_blank_iframe() {
+ PermissionTestUtils.remove(PAGE_URI, "cookie");
+
+ let tab = BrowserTestUtils.addTab(gBrowser, SCOPE);
+ let browser = gBrowser.getBrowserForTab(tab);
+ await BrowserTestUtils.browserLoaded(browser);
+
+ let controller = await SpecialPowers.spawn(browser, [], async function() {
+ return content.navigator.serviceWorker.controller;
+ });
+
+ ok(!!controller, "page should be controlled with storage allowed");
+
+ let controller2 = await SpecialPowers.spawn(browser, [], async function() {
+ let f = content.document.createElement("iframe");
+ content.document.body.appendChild(f);
+ await new Promise(resolve => (f.onload = resolve));
+ return !!f.contentWindow.navigator.serviceWorker.controller;
+ });
+
+ ok(!!controller2, "page should be controlled with storage allowed");
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["network.cookie.cookieBehavior", Ci.nsICookieService.BEHAVIOR_REJECT],
+ ],
+ });
+
+ let controller3 = await SpecialPowers.spawn(browser, [], async function() {
+ let f = content.document.createElement("iframe");
+ content.document.body.appendChild(f);
+ await new Promise(resolve => (f.onload = resolve));
+ return !!f.contentWindow.navigator.serviceWorker.controller;
+ });
+
+ ok(!!controller3, "page should be controlled with storage allowed");
+
+ await SpecialPowers.popPrefEnv();
+ BrowserTestUtils.removeTab(tab);
+});
+
+// Test to verify a blob URL iframe successfully inherits the
+// parent's controller when storage is blocked between opening the
+// parent page and creating the iframe.
+add_task(async function test_block_storage_before_blob_iframe() {
+ PermissionTestUtils.remove(PAGE_URI, "cookie");
+
+ let tab = BrowserTestUtils.addTab(gBrowser, SCOPE);
+ let browser = gBrowser.getBrowserForTab(tab);
+ await BrowserTestUtils.browserLoaded(browser);
+
+ let controller = await SpecialPowers.spawn(browser, [], async function() {
+ return content.navigator.serviceWorker.controller;
+ });
+
+ ok(!!controller, "page should be controlled with storage allowed");
+
+ let controller2 = await SpecialPowers.spawn(browser, [], async function() {
+ let b = new content.Blob(["<!DOCTYPE html><html></html>"], {
+ type: "text/html",
+ });
+ let f = content.document.createElement("iframe");
+ // No need to call revokeObjectURL() since the window will be closed shortly.
+ f.src = content.URL.createObjectURL(b);
+ content.document.body.appendChild(f);
+ await new Promise(resolve => (f.onload = resolve));
+ return !!f.contentWindow.navigator.serviceWorker.controller;
+ });
+
+ ok(!!controller2, "page should be controlled with storage allowed");
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["network.cookie.cookieBehavior", Ci.nsICookieService.BEHAVIOR_REJECT],
+ ],
+ });
+
+ let controller3 = await SpecialPowers.spawn(browser, [], async function() {
+ let b = new content.Blob(["<!DOCTYPE html><html></html>"], {
+ type: "text/html",
+ });
+ let f = content.document.createElement("iframe");
+ // No need to call revokeObjectURL() since the window will be closed shortly.
+ f.src = content.URL.createObjectURL(b);
+ content.document.body.appendChild(f);
+ await new Promise(resolve => (f.onload = resolve));
+ return !!f.contentWindow.navigator.serviceWorker.controller;
+ });
+
+ ok(!!controller3, "page should be controlled with storage allowed");
+
+ await SpecialPowers.popPrefEnv();
+ BrowserTestUtils.removeTab(tab);
+});
+
+// Test to verify a blob worker script does not hit our service
+// worker storage assertions when storage is blocked between opening
+// the parent page and creating the worker. Note, we cannot
+// explicitly check if the worker is controlled since we don't expose
+// WorkerNavigator.serviceWorkers.controller yet.
+add_task(async function test_block_storage_before_blob_worker() {
+ PermissionTestUtils.remove(PAGE_URI, "cookie");
+
+ let tab = BrowserTestUtils.addTab(gBrowser, SCOPE);
+ let browser = gBrowser.getBrowserForTab(tab);
+ await BrowserTestUtils.browserLoaded(browser);
+
+ let controller = await SpecialPowers.spawn(browser, [], async function() {
+ return content.navigator.serviceWorker.controller;
+ });
+
+ ok(!!controller, "page should be controlled with storage allowed");
+
+ let scriptURL = await SpecialPowers.spawn(browser, [], async function() {
+ let b = new content.Blob(
+ ["self.postMessage(self.location.href);self.close()"],
+ { type: "application/javascript" }
+ );
+ // No need to call revokeObjectURL() since the window will be closed shortly.
+ let u = content.URL.createObjectURL(b);
+ let w = new content.Worker(u);
+ return await new Promise(resolve => {
+ w.onmessage = e => resolve(e.data);
+ });
+ });
+
+ ok(scriptURL.startsWith("blob:"), "blob URL worker should run");
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["network.cookie.cookieBehavior", Ci.nsICookieService.BEHAVIOR_REJECT],
+ ],
+ });
+
+ let scriptURL2 = await SpecialPowers.spawn(browser, [], async function() {
+ let b = new content.Blob(
+ ["self.postMessage(self.location.href);self.close()"],
+ { type: "application/javascript" }
+ );
+ // No need to call revokeObjectURL() since the window will be closed shortly.
+ let u = content.URL.createObjectURL(b);
+ let w = new content.Worker(u);
+ return await new Promise(resolve => {
+ w.onmessage = e => resolve(e.data);
+ });
+ });
+
+ ok(scriptURL2.startsWith("blob:"), "blob URL worker should run");
+
+ await SpecialPowers.popPrefEnv();
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function cleanup() {
+ PermissionTestUtils.remove(PAGE_URI, "cookie");
+
+ let tab = BrowserTestUtils.addTab(gBrowser, PAGE_URI);
+ let browser = gBrowser.getBrowserForTab(tab);
+ await BrowserTestUtils.browserLoaded(browser);
+
+ await SpecialPowers.spawn(browser, [SCOPE], async function(uri) {
+ let reg = await content.navigator.serviceWorker.getRegistration(uri);
+ let worker = reg.active;
+ await reg.unregister();
+ await new Promise(resolve => {
+ if (worker.state === "redundant") {
+ resolve();
+ return;
+ }
+ worker.addEventListener("statechange", function onStateChange() {
+ if (worker.state === "redundant") {
+ worker.removeEventListener("statechange", onStateChange);
+ resolve();
+ }
+ });
+ });
+ });
+
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/dom/serviceworkers/test/browser_storage_recovery.js b/dom/serviceworkers/test/browser_storage_recovery.js
new file mode 100644
index 0000000000..5ecc135021
--- /dev/null
+++ b/dom/serviceworkers/test/browser_storage_recovery.js
@@ -0,0 +1,152 @@
+"use strict";
+
+// This test registers a SW for a scope that will never control a document
+// and therefore never trigger a "fetch" functional event that would
+// automatically attempt to update the registration. The overlap of the
+// PAGE_URI and SCOPE is incidental. checkForUpdate is the only thing that
+// will trigger an update of the registration and so there is no need to
+// worry about Schedule Job races to coalesce an update job.
+
+const BASE_URI = "http://mochi.test:8888/browser/dom/serviceworkers/test/";
+const PAGE_URI = BASE_URI + "empty.html";
+const SCOPE = PAGE_URI + "?storage_recovery";
+const SW_SCRIPT = BASE_URI + "storage_recovery_worker.sjs";
+
+async function checkForUpdate(browser) {
+ return SpecialPowers.spawn(browser, [SCOPE], async function(uri) {
+ let reg = await content.navigator.serviceWorker.getRegistration(uri);
+ await reg.update();
+ return !!reg.installing;
+ });
+}
+
+// Delete all of our chrome-namespace Caches for this origin, leaving any
+// content-owned caches in place. This is exclusively for simulating loss
+// of the origin's storage without loss of the registration and without
+// having to worry that future enhancements to QuotaClients/ServiceWorkerRegistrar
+// will break this test. If you want to wipe storage for an origin, use
+// QuotaManager APIs
+async function wipeStorage(u) {
+ let uri = Services.io.newURI(u);
+ let principal = Services.scriptSecurityManager.createContentPrincipal(
+ uri,
+ {}
+ );
+ let caches = new CacheStorage("chrome", principal);
+ let list = await caches.keys();
+ return Promise.all(list.map(c => caches.delete(c)));
+}
+
+add_setup(async function() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ ["dom.serviceWorkers.idle_timeout", 0],
+ ],
+ });
+
+ // Configure the server script to not redirect.
+ await fetch(SW_SCRIPT + "?clear-redirect");
+
+ let tab = BrowserTestUtils.addTab(gBrowser, PAGE_URI);
+ let browser = gBrowser.getBrowserForTab(tab);
+ await BrowserTestUtils.browserLoaded(browser);
+
+ await SpecialPowers.spawn(
+ browser,
+ [{ script: SW_SCRIPT, scope: SCOPE }],
+ async function(opts) {
+ let reg = await content.navigator.serviceWorker.register(opts.script, {
+ scope: opts.scope,
+ });
+ let worker = reg.installing || reg.waiting || reg.active;
+ await new Promise(resolve => {
+ if (worker.state === "activated") {
+ resolve();
+ return;
+ }
+ worker.addEventListener("statechange", function onStateChange() {
+ if (worker.state === "activated") {
+ worker.removeEventListener("statechange", onStateChange);
+ resolve();
+ }
+ });
+ });
+ }
+ );
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+// Verify that our service worker doesn't update normally.
+add_task(async function normal_update_check() {
+ let tab = BrowserTestUtils.addTab(gBrowser, PAGE_URI);
+ let browser = gBrowser.getBrowserForTab(tab);
+ await BrowserTestUtils.browserLoaded(browser);
+
+ let updated = await checkForUpdate(browser);
+ ok(!updated, "normal update check should not trigger an update");
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+// Test what happens when we wipe the service worker scripts
+// out from under the site before triggering the update. This
+// should cause an update to occur.
+add_task(async function wiped_update_check() {
+ // Wipe the backing cache storage, but leave the SW registered.
+ await wipeStorage(PAGE_URI);
+
+ let tab = BrowserTestUtils.addTab(gBrowser, PAGE_URI);
+ let browser = gBrowser.getBrowserForTab(tab);
+ await BrowserTestUtils.browserLoaded(browser);
+
+ let updated = await checkForUpdate(browser);
+ ok(updated, "wiping the service worker scripts should trigger an update");
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+// Test what happens when we wipe the service worker scripts
+// out from under the site before triggering the update. This
+// should cause an update to occur.
+add_task(async function wiped_and_failed_update_check() {
+ // Wipe the backing cache storage, but leave the SW registered.
+ await wipeStorage(PAGE_URI);
+
+ // Configure the service worker script to redirect. This will
+ // prevent the update from completing successfully.
+ await fetch(SW_SCRIPT + "?set-redirect");
+
+ let tab = BrowserTestUtils.addTab(gBrowser, PAGE_URI);
+ let browser = gBrowser.getBrowserForTab(tab);
+ await BrowserTestUtils.browserLoaded(browser);
+
+ // Attempt to update the service worker. This should throw
+ // an error because the script is now redirecting.
+ let updateFailed = false;
+ try {
+ await checkForUpdate(browser);
+ } catch (e) {
+ updateFailed = true;
+ }
+ ok(updateFailed, "redirecting service worker script should fail to update");
+
+ // Also, since the existing service worker's scripts are broken
+ // we should also remove the registration completely when the
+ // update fails.
+ let exists = await SpecialPowers.spawn(browser, [SCOPE], async function(uri) {
+ let reg = await content.navigator.serviceWorker.getRegistration(uri);
+ return !!reg;
+ });
+ ok(
+ !exists,
+ "registration should be removed after scripts are wiped and update fails"
+ );
+
+ // Note, we don't have to clean up the service worker registration
+ // since its effectively been force-removed here.
+
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/dom/serviceworkers/test/browser_unregister_with_containers.js b/dom/serviceworkers/test/browser_unregister_with_containers.js
new file mode 100644
index 0000000000..0304f70c1d
--- /dev/null
+++ b/dom/serviceworkers/test/browser_unregister_with_containers.js
@@ -0,0 +1,153 @@
+"use strict";
+
+const BASE_URI = "http://mochi.test:8888/browser/dom/serviceworkers/test/";
+const PAGE_URI = BASE_URI + "empty.html";
+const SCOPE = PAGE_URI + "?unregister_with_containers";
+const SW_SCRIPT = BASE_URI + "empty.js";
+
+function doRegister(browser) {
+ return SpecialPowers.spawn(
+ browser,
+ [{ script: SW_SCRIPT, scope: SCOPE }],
+ async function(opts) {
+ let reg = await content.navigator.serviceWorker.register(opts.script, {
+ scope: opts.scope,
+ });
+ let worker = reg.installing || reg.waiting || reg.active;
+ await new Promise(resolve => {
+ if (worker.state === "activated") {
+ resolve();
+ return;
+ }
+ worker.addEventListener("statechange", function onStateChange() {
+ if (worker.state === "activated") {
+ worker.removeEventListener("statechange", onStateChange);
+ resolve();
+ }
+ });
+ });
+ }
+ );
+}
+
+function doUnregister(browser) {
+ return SpecialPowers.spawn(browser, [SCOPE], async function(uri) {
+ let reg = await content.navigator.serviceWorker.getRegistration(uri);
+ let worker = reg.active;
+ await reg.unregister();
+ await new Promise(resolve => {
+ if (worker.state === "redundant") {
+ resolve();
+ return;
+ }
+ worker.addEventListener("statechange", function onStateChange() {
+ if (worker.state === "redundant") {
+ worker.removeEventListener("statechange", onStateChange);
+ resolve();
+ }
+ });
+ });
+ });
+}
+
+function isControlled(browser) {
+ return SpecialPowers.spawn(browser, [], function() {
+ return !!content.navigator.serviceWorker.controller;
+ });
+}
+
+async function checkControlled(browser) {
+ let controlled = await isControlled(browser);
+ ok(controlled, "window should be controlled");
+}
+
+async function checkUncontrolled(browser) {
+ let controlled = await isControlled(browser);
+ ok(!controlled, "window should not be controlled");
+}
+
+add_task(async function test() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ // Avoid service worker propagation races by disabling multi-e10s for now.
+ // This can be removed after the e10s refactor is complete.
+ ["dom.ipc.processCount", 1],
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ ],
+ });
+
+ // Setup service workers in two different contexts with the same scope.
+ let containerTab1 = BrowserTestUtils.addTab(gBrowser, PAGE_URI, {
+ userContextId: 1,
+ });
+ let containerBrowser1 = gBrowser.getBrowserForTab(containerTab1);
+ await BrowserTestUtils.browserLoaded(containerBrowser1);
+
+ let containerTab2 = BrowserTestUtils.addTab(gBrowser, PAGE_URI, {
+ userContextId: 2,
+ });
+ let containerBrowser2 = gBrowser.getBrowserForTab(containerTab2);
+ await BrowserTestUtils.browserLoaded(containerBrowser2);
+
+ await doRegister(containerBrowser1);
+ await doRegister(containerBrowser2);
+
+ await checkUncontrolled(containerBrowser1);
+ await checkUncontrolled(containerBrowser2);
+
+ // Close the tabs we used to register the service workers. These are not
+ // controlled.
+ BrowserTestUtils.removeTab(containerTab1);
+ BrowserTestUtils.removeTab(containerTab2);
+
+ // Open a controlled tab in each container.
+ containerTab1 = BrowserTestUtils.addTab(gBrowser, SCOPE, {
+ userContextId: 1,
+ });
+ containerBrowser1 = gBrowser.getBrowserForTab(containerTab1);
+ await BrowserTestUtils.browserLoaded(containerBrowser1);
+
+ containerTab2 = BrowserTestUtils.addTab(gBrowser, SCOPE, {
+ userContextId: 2,
+ });
+ containerBrowser2 = gBrowser.getBrowserForTab(containerTab2);
+ await BrowserTestUtils.browserLoaded(containerBrowser2);
+
+ await checkControlled(containerBrowser1);
+ await checkControlled(containerBrowser2);
+
+ // Remove the first container's controlled tab
+ BrowserTestUtils.removeTab(containerTab1);
+
+ // Create a new uncontrolled tab for the first container and use it to
+ // unregister the service worker.
+ containerTab1 = BrowserTestUtils.addTab(gBrowser, PAGE_URI, {
+ userContextId: 1,
+ });
+ containerBrowser1 = gBrowser.getBrowserForTab(containerTab1);
+ await BrowserTestUtils.browserLoaded(containerBrowser1);
+ await doUnregister(containerBrowser1);
+
+ await checkUncontrolled(containerBrowser1);
+ await checkControlled(containerBrowser2);
+
+ // Remove the second container's controlled tab
+ BrowserTestUtils.removeTab(containerTab2);
+
+ // Create a new uncontrolled tab for the second container and use it to
+ // unregister the service worker.
+ containerTab2 = BrowserTestUtils.addTab(gBrowser, PAGE_URI, {
+ userContextId: 2,
+ });
+ containerBrowser2 = gBrowser.getBrowserForTab(containerTab2);
+ await BrowserTestUtils.browserLoaded(containerBrowser2);
+ await doUnregister(containerBrowser2);
+
+ await checkUncontrolled(containerBrowser1);
+ await checkUncontrolled(containerBrowser2);
+
+ // Close the two tabs we used to unregister the service worker.
+ BrowserTestUtils.removeTab(containerTab1);
+ BrowserTestUtils.removeTab(containerTab2);
+});
diff --git a/dom/serviceworkers/test/browser_userContextId_openWindow.js b/dom/serviceworkers/test/browser_userContextId_openWindow.js
new file mode 100644
index 0000000000..8b07a532fb
--- /dev/null
+++ b/dom/serviceworkers/test/browser_userContextId_openWindow.js
@@ -0,0 +1,163 @@
+let Cm = Components.manager;
+
+let swm = Cc["@mozilla.org/serviceworkers/manager;1"].getService(
+ Ci.nsIServiceWorkerManager
+);
+
+const URI = "https://example.com/browser/dom/serviceworkers/test/empty.html";
+const MOCK_CID = Components.ID("{2a0f83c4-8818-4914-a184-f1172b4eaaa7}");
+const ALERTS_SERVICE_CONTRACT_ID = "@mozilla.org/alerts-service;1";
+const USER_CONTEXT_ID = 3;
+
+let mockAlertsService = {
+ showAlert(alert, alertListener) {
+ ok(true, "Showing alert");
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ setTimeout(function() {
+ alertListener.observe(null, "alertshow", alert.cookie);
+ }, 100);
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ setTimeout(function() {
+ alertListener.observe(null, "alertclickcallback", alert.cookie);
+ }, 100);
+ },
+
+ showAlertNotification(
+ imageUrl,
+ title,
+ text,
+ textClickable,
+ cookie,
+ alertListener,
+ name,
+ dir,
+ lang,
+ data
+ ) {
+ this.showAlert();
+ },
+
+ QueryInterface(aIID) {
+ if (aIID.equals(Ci.nsISupports) || aIID.equals(Ci.nsIAlertsService)) {
+ return this;
+ }
+ throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE);
+ },
+
+ createInstance(aIID) {
+ return this.QueryInterface(aIID);
+ },
+};
+
+registerCleanupFunction(() => {
+ Cm.QueryInterface(Ci.nsIComponentRegistrar).unregisterFactory(
+ MOCK_CID,
+ mockAlertsService
+ );
+});
+
+add_setup(async function() {
+ // make sure userContext, SW and notifications are enabled.
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["privacy.userContext.enabled", true],
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ ["dom.webnotifications.workers.enabled", true],
+ ["dom.webnotifications.serviceworker.enabled", true],
+ ["notification.prompt.testing", true],
+ ["dom.serviceWorkers.disable_open_click_delay", 1000],
+ ["dom.serviceWorkers.idle_timeout", 299999],
+ ["dom.serviceWorkers.idle_extended_timeout", 299999],
+ ["browser.link.open_newwindow", 3],
+ ],
+ });
+});
+
+add_task(async function test() {
+ Cm.QueryInterface(Ci.nsIComponentRegistrar).registerFactory(
+ MOCK_CID,
+ "alerts service",
+ ALERTS_SERVICE_CONTRACT_ID,
+ mockAlertsService
+ );
+
+ // open the tab in the correct userContextId
+ let tab = BrowserTestUtils.addTab(gBrowser, URI, {
+ userContextId: USER_CONTEXT_ID,
+ });
+ let browser = gBrowser.getBrowserForTab(tab);
+
+ // select tab and make sure its browser is focused
+ gBrowser.selectedTab = tab;
+ tab.ownerGlobal.focus();
+
+ // wait for tab load
+ await BrowserTestUtils.browserLoaded(gBrowser.getBrowserForTab(tab));
+
+ // Waiting for new tab.
+ let newTabPromise = BrowserTestUtils.waitForNewTab(gBrowser, null, true);
+
+ // here the test.
+ /* eslint-disable no-shadow */
+ let uci = await SpecialPowers.spawn(browser, [URI], uri => {
+ let uci = content.document.nodePrincipal.userContextId;
+
+ // Registration of the SW
+ return (
+ content.navigator.serviceWorker
+ .register("file_userContextId_openWindow.js")
+
+ // Activation
+ .then(swr => {
+ return new content.window.Promise(resolve => {
+ let worker = swr.installing;
+ worker.addEventListener("statechange", () => {
+ if (worker.state === "activated") {
+ resolve(swr);
+ }
+ });
+ });
+ })
+
+ // Ask for an openWindow.
+ .then(swr => {
+ swr.showNotification("testPopup");
+ return uci;
+ })
+ );
+ });
+ /* eslint-enable no-shadow */
+
+ is(uci, USER_CONTEXT_ID, "Tab runs with UCI " + USER_CONTEXT_ID);
+
+ let newTab = await newTabPromise;
+
+ is(
+ newTab.getAttribute("usercontextid"),
+ USER_CONTEXT_ID,
+ "New tab has UCI equal " + USER_CONTEXT_ID
+ );
+
+ // wait for SW unregistration
+ /* eslint-disable no-shadow */
+ uci = await SpecialPowers.spawn(browser, [], () => {
+ let uci = content.document.nodePrincipal.userContextId;
+
+ return content.navigator.serviceWorker
+ .getRegistration(".")
+ .then(registration => {
+ return registration.unregister();
+ })
+ .then(() => {
+ return uci;
+ });
+ });
+ /* eslint-enable no-shadow */
+
+ is(uci, USER_CONTEXT_ID, "Tab runs with UCI " + USER_CONTEXT_ID);
+
+ BrowserTestUtils.removeTab(newTab);
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/dom/serviceworkers/test/bug1151916_driver.html b/dom/serviceworkers/test/bug1151916_driver.html
new file mode 100644
index 0000000000..08e7d9414f
--- /dev/null
+++ b/dom/serviceworkers/test/bug1151916_driver.html
@@ -0,0 +1,53 @@
+<html>
+ <body>
+ <script language="javascript">
+ function fail(msg) {
+ window.parent.postMessage({ status: "failed", message: msg }, "*");
+ }
+
+ function success(msg) {
+ window.parent.postMessage({ status: "success", message: msg }, "*");
+ }
+
+ if (!window.parent) {
+ dump("This file must be embedded in an iframe!");
+ }
+
+ navigator.serviceWorker.getRegistration()
+ .then(function(reg) {
+ if (!reg) {
+ navigator.serviceWorker.ready.then(function(registration) {
+ if (registration.active.state == "activating") {
+ registration.active.onstatechange = function(e) {
+ registration.active.onstatechange = null;
+ if (registration.active.state == "activated") {
+ success("Registered and activated");
+ }
+ }
+ } else {
+ success("Registered and activated");
+ }
+ });
+ navigator.serviceWorker.register("bug1151916_worker.js",
+ { scope: "." });
+ } else {
+ // Simply force the sw to load a resource and touch self.caches.
+ if (!reg.active) {
+ fail("no-active-worker");
+ return;
+ }
+
+ fetch("madeup.txt").then(function(res) {
+ res.text().then(function(v) {
+ if (v == "Hi there") {
+ success("Loaded from cache");
+ } else {
+ fail("Response text did not match");
+ }
+ }, fail);
+ }, fail);
+ }
+ }, fail);
+ </script>
+ </body>
+</html>
diff --git a/dom/serviceworkers/test/bug1151916_worker.js b/dom/serviceworkers/test/bug1151916_worker.js
new file mode 100644
index 0000000000..2688b78668
--- /dev/null
+++ b/dom/serviceworkers/test/bug1151916_worker.js
@@ -0,0 +1,15 @@
+onactivate = function(e) {
+ e.waitUntil(
+ self.caches.open("default-cache").then(function(cache) {
+ var response = new Response("Hi there");
+ return cache.put("madeup.txt", response);
+ })
+ );
+};
+
+onfetch = function(e) {
+ if (e.request.url.match(/madeup.txt$/)) {
+ var p = self.caches.match("madeup.txt", { cacheName: "default-cache" });
+ e.respondWith(p);
+ }
+};
diff --git a/dom/serviceworkers/test/bug1240436_worker.js b/dom/serviceworkers/test/bug1240436_worker.js
new file mode 100644
index 0000000000..c21f60b60f
--- /dev/null
+++ b/dom/serviceworkers/test/bug1240436_worker.js
@@ -0,0 +1,2 @@
+// a contains a ZERO WIDTH JOINER (0x200D)
+var a = "‍";
diff --git a/dom/serviceworkers/test/chrome-common.ini b/dom/serviceworkers/test/chrome-common.ini
new file mode 100644
index 0000000000..b5cae318e1
--- /dev/null
+++ b/dom/serviceworkers/test/chrome-common.ini
@@ -0,0 +1,21 @@
+[DEFAULT]
+skip-if = os == 'android'
+support-files =
+ chrome_helpers.js
+ empty.js
+ fetch.js
+ hello.html
+ serviceworker.html
+ serviceworkerinfo_iframe.html
+ serviceworkermanager_iframe.html
+ serviceworkerregistrationinfo_iframe.html
+ utils.js
+ worker.js
+ worker2.js
+
+[test_devtools_track_serviceworker_time.html]
+[test_privateBrowsing.html]
+[test_serviceworkerinfo.xhtml]
+skip-if = serviceworker_e10s # nsIWorkerDebugger attribute not implemented
+[test_serviceworkermanager.xhtml]
+[test_serviceworkerregistrationinfo.xhtml]
diff --git a/dom/serviceworkers/test/chrome-dFPI.ini b/dom/serviceworkers/test/chrome-dFPI.ini
new file mode 100644
index 0000000000..b07337f753
--- /dev/null
+++ b/dom/serviceworkers/test/chrome-dFPI.ini
@@ -0,0 +1,7 @@
+[DEFAULT]
+# Enable dFPI(cookieBehavior 5) for service worker tests.
+prefs =
+ network.cookie.cookieBehavior=5
+dupe-manifest = true
+
+[include:chrome-common.ini]
diff --git a/dom/serviceworkers/test/chrome.ini b/dom/serviceworkers/test/chrome.ini
new file mode 100644
index 0000000000..e6f7e3206d
--- /dev/null
+++ b/dom/serviceworkers/test/chrome.ini
@@ -0,0 +1,4 @@
+[DEFAULT]
+dupe-manifest = true
+
+[include:chrome-common.ini]
diff --git a/dom/serviceworkers/test/chrome_helpers.js b/dom/serviceworkers/test/chrome_helpers.js
new file mode 100644
index 0000000000..566ba52eca
--- /dev/null
+++ b/dom/serviceworkers/test/chrome_helpers.js
@@ -0,0 +1,71 @@
+let swm = Cc["@mozilla.org/serviceworkers/manager;1"].getService(
+ Ci.nsIServiceWorkerManager
+);
+
+let EXAMPLE_URL = "https://example.com/chrome/dom/serviceworkers/test/";
+
+function waitForIframeLoad(iframe) {
+ return new Promise(function(resolve) {
+ iframe.onload = resolve;
+ });
+}
+
+function waitForRegister(scope, callback) {
+ return new Promise(function(resolve) {
+ let listener = {
+ onRegister(registration) {
+ if (registration.scope !== scope) {
+ return;
+ }
+ swm.removeListener(listener);
+ resolve(callback ? callback(registration) : registration);
+ },
+ };
+ swm.addListener(listener);
+ });
+}
+
+function waitForUnregister(scope) {
+ return new Promise(function(resolve) {
+ let listener = {
+ onUnregister(registration) {
+ if (registration.scope !== scope) {
+ return;
+ }
+ swm.removeListener(listener);
+ resolve(registration);
+ },
+ };
+ swm.addListener(listener);
+ });
+}
+
+function waitForServiceWorkerRegistrationChange(registration, callback) {
+ return new Promise(function(resolve) {
+ let listener = {
+ onChange() {
+ registration.removeListener(listener);
+ if (callback) {
+ callback();
+ }
+ resolve(callback ? callback() : undefined);
+ },
+ };
+ registration.addListener(listener);
+ });
+}
+
+function waitForServiceWorkerShutdown() {
+ return new Promise(function(resolve) {
+ let observer = {
+ observe(subject, topic, data) {
+ if (topic !== "service-worker-shutdown") {
+ return;
+ }
+ SpecialPowers.removeObserver(observer, "service-worker-shutdown");
+ resolve();
+ },
+ };
+ SpecialPowers.addObserver(observer, "service-worker-shutdown");
+ });
+}
diff --git a/dom/serviceworkers/test/claim_clients/client.html b/dom/serviceworkers/test/claim_clients/client.html
new file mode 100644
index 0000000000..969a6dbf10
--- /dev/null
+++ b/dom/serviceworkers/test/claim_clients/client.html
@@ -0,0 +1,43 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1130684 - claim client </title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+<script class="testbody" type="text/javascript">
+
+ if (!parent) {
+ info("This page shouldn't be launched directly!");
+ }
+
+ window.onload = function() {
+ parent.postMessage("READY", "*");
+ }
+
+ navigator.serviceWorker.oncontrollerchange = function() {
+ parent.postMessage({
+ event: "controllerchange",
+ controller: (navigator.serviceWorker.controller !== null)
+ }, "*");
+ }
+
+ navigator.serviceWorker.onmessage = function(e) {
+ parent.postMessage({
+ event: "message",
+ data: e.data
+ }, "*");
+ }
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/claim_oninstall_worker.js b/dom/serviceworkers/test/claim_oninstall_worker.js
new file mode 100644
index 0000000000..6d79c919b0
--- /dev/null
+++ b/dom/serviceworkers/test/claim_oninstall_worker.js
@@ -0,0 +1,7 @@
+oninstall = function(e) {
+ var claimFailedPromise = new Promise(function(resolve, reject) {
+ clients.claim().then(reject, () => resolve());
+ });
+
+ e.waitUntil(claimFailedPromise);
+};
diff --git a/dom/serviceworkers/test/claim_worker_1.js b/dom/serviceworkers/test/claim_worker_1.js
new file mode 100644
index 0000000000..493798a88b
--- /dev/null
+++ b/dom/serviceworkers/test/claim_worker_1.js
@@ -0,0 +1,32 @@
+onactivate = function(e) {
+ var result = {
+ resolve_value: false,
+ match_count_before: -1,
+ match_count_after: -1,
+ message: "claim_worker_1",
+ };
+
+ self.clients
+ .matchAll()
+ .then(function(matched) {
+ // should be 0
+ result.match_count_before = matched.length;
+ })
+ .then(function() {
+ return self.clients.claim();
+ })
+ .then(function(ret) {
+ result.resolve_value = ret;
+ return self.clients.matchAll();
+ })
+ .then(function(matched) {
+ // should be 2
+ result.match_count_after = matched.length;
+ for (i = 0; i < matched.length; i++) {
+ matched[i].postMessage(result);
+ }
+ if (result.match_count_after !== 2) {
+ dump("ERROR: claim_worker_1 failed to capture clients.\n");
+ }
+ });
+};
diff --git a/dom/serviceworkers/test/claim_worker_2.js b/dom/serviceworkers/test/claim_worker_2.js
new file mode 100644
index 0000000000..b244495ecd
--- /dev/null
+++ b/dom/serviceworkers/test/claim_worker_2.js
@@ -0,0 +1,34 @@
+onactivate = function(e) {
+ var result = {
+ resolve_value: false,
+ match_count_before: -1,
+ match_count_after: -1,
+ message: "claim_worker_2",
+ };
+
+ self.clients
+ .matchAll()
+ .then(function(matched) {
+ // should be 0
+ result.match_count_before = matched.length;
+ })
+ .then(function() {
+ return clients.claim();
+ })
+ .then(function(ret) {
+ result.resolve_value = ret;
+ return clients.matchAll();
+ })
+ .then(function(matched) {
+ // should be 1
+ result.match_count_after = matched.length;
+ if (result.match_count_after === 1) {
+ matched[0].postMessage(result);
+ } else {
+ dump("ERROR: claim_worker_2 failed to capture clients.\n");
+ for (let i = 0; i < matched.length; ++i) {
+ dump("### ### matched[" + i + "]: " + matched[i].url + "\n");
+ }
+ }
+ });
+};
diff --git a/dom/serviceworkers/test/close_test.js b/dom/serviceworkers/test/close_test.js
new file mode 100644
index 0000000000..cc0ec4d335
--- /dev/null
+++ b/dom/serviceworkers/test/close_test.js
@@ -0,0 +1,22 @@
+function ok(v, msg) {
+ client.postMessage({ status: "ok", result: !!v, message: msg });
+}
+
+var client;
+onmessage = function(e) {
+ if (e.data.message == "start") {
+ self.clients.matchAll().then(function(clients) {
+ client = clients[0];
+ try {
+ close();
+ ok(false, "close() should throw");
+ } catch (ex) {
+ ok(
+ ex.name === "InvalidAccessError",
+ "close() should throw InvalidAccessError"
+ );
+ }
+ client.postMessage({ status: "done" });
+ });
+ }
+};
diff --git a/dom/serviceworkers/test/console_monitor.js b/dom/serviceworkers/test/console_monitor.js
new file mode 100644
index 0000000000..e3c347b153
--- /dev/null
+++ b/dom/serviceworkers/test/console_monitor.js
@@ -0,0 +1,44 @@
+/* eslint-env mozilla/chrome-script */
+
+let consoleListener;
+
+function ConsoleListener() {
+ Services.console.registerListener(this);
+}
+
+ConsoleListener.prototype = {
+ callbacks: [],
+
+ observe: aMsg => {
+ if (!(aMsg instanceof Ci.nsIScriptError)) {
+ return;
+ }
+
+ let msg = {
+ cssSelectors: aMsg.cssSelectors,
+ errorMessage: aMsg.errorMessage,
+ sourceName: aMsg.sourceName,
+ sourceLine: aMsg.sourceLine,
+ lineNumber: aMsg.lineNumber,
+ columnNumber: aMsg.columnNumber,
+ category: aMsg.category,
+ windowID: aMsg.outerWindowID,
+ innerWindowID: aMsg.innerWindowID,
+ isScriptError: true,
+ isWarning: (aMsg.flags & Ci.nsIScriptError.warningFlag) === 1,
+ };
+
+ sendAsyncMessage("monitor", msg);
+ },
+};
+
+addMessageListener("load", function(e) {
+ consoleListener = new ConsoleListener();
+ sendAsyncMessage("ready", {});
+});
+
+addMessageListener("unload", function(e) {
+ Services.console.unregisterListener(consoleListener);
+ consoleListener = null;
+ sendAsyncMessage("unloaded", {});
+});
diff --git a/dom/serviceworkers/test/controller/index.html b/dom/serviceworkers/test/controller/index.html
new file mode 100644
index 0000000000..2a68e3f4bb
--- /dev/null
+++ b/dom/serviceworkers/test/controller/index.html
@@ -0,0 +1,72 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 94048 - test install event.</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+<script class="testbody" type="text/javascript">
+
+ // Make sure to use good, unique messages, since the actual expression will not show up in test results.
+ function my_ok(result, msg) {
+ parent.postMessage({status: "ok", result, message: msg}, "*");
+ }
+
+ function finish() {
+ parent.postMessage({status: "done"}, "*");
+ }
+
+ navigator.serviceWorker.ready.then(function(swr) {
+ my_ok(swr.scope.match(/serviceworkers\/test\/control$/),
+ "This page should be controlled by upper level registration");
+ my_ok(swr.installing == undefined,
+ "Upper level registration should not have a installing worker.");
+ if (navigator.serviceWorker.controller) {
+ // We are controlled.
+ // Register a new worker for this sub-scope. After that, controller should still be for upper level, but active should change to be this scope's.
+ navigator.serviceWorker.register("../worker2.js", { scope: "./" }).then(function(e) {
+ my_ok("installing" in e, "ServiceWorkerRegistration.installing exists.");
+ my_ok(e.installing instanceof ServiceWorker, "ServiceWorkerRegistration.installing is a ServiceWorker.");
+
+ my_ok("waiting" in e, "ServiceWorkerRegistration.waiting exists.");
+ my_ok("active" in e, "ServiceWorkerRegistration.active exists.");
+
+ my_ok(e.installing &&
+ e.installing.scriptURL.match(/worker2.js$/),
+ "Installing is serviceworker/controller");
+
+ my_ok("scope" in e, "ServiceWorkerRegistration.scope exists.");
+ my_ok(e.scope.match(/serviceworkers\/test\/controller\/$/), "Scope is serviceworker/test/controller " + e.scope);
+
+ my_ok("unregister" in e, "ServiceWorkerRegistration.unregister exists.");
+
+ my_ok(navigator.serviceWorker.controller.scriptURL.match(/worker\.js$/),
+ "Controller is still worker.js");
+
+ e.unregister().then(function(result) {
+ my_ok(result, "Unregistering the SW should succeed");
+ finish();
+ }, function(error) {
+ dump("Error unregistering the SW: " + error + "\n");
+ });
+ });
+ } else {
+ my_ok(false, "Should've been controlled!");
+ finish();
+ }
+ }).catch(function(e) {
+ my_ok(false, "Some test threw an error " + e);
+ finish();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/create_another_sharedWorker.html b/dom/serviceworkers/test/create_another_sharedWorker.html
new file mode 100644
index 0000000000..f49194fa50
--- /dev/null
+++ b/dom/serviceworkers/test/create_another_sharedWorker.html
@@ -0,0 +1,6 @@
+<!DOCTYPE HTML>
+<title>Shared workers: create antoehr sharedworekr client</title>
+<pre id=log>Hello World</pre>
+<script>
+ var worker = new SharedWorker('sharedWorker_fetch.js');
+</script>
diff --git a/dom/serviceworkers/test/download/window.html b/dom/serviceworkers/test/download/window.html
new file mode 100644
index 0000000000..7d7893e0e6
--- /dev/null
+++ b/dom/serviceworkers/test/download/window.html
@@ -0,0 +1,46 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+</head>
+<body>
+<script type="text/javascript">
+
+function wait_until_controlled() {
+ return new Promise(function(resolve) {
+ if (navigator.serviceWorker.controller) {
+ return resolve();
+ }
+ navigator.serviceWorker.addEventListener('controllerchange', function onController() {
+ if (navigator.serviceWorker.controller) {
+ navigator.serviceWorker.removeEventListener('controllerchange', onController);
+ return resolve();
+ }
+ });
+ });
+}
+addEventListener('load', function(event) {
+ var registration;
+ navigator.serviceWorker.register('worker.js').then(function(swr) {
+ registration = swr;
+
+ // While the iframe below is a navigation, we still wait until we are
+ // controlled here. We want an active client to hold the service worker
+ // alive since it calls unregister() on itself.
+ return wait_until_controlled();
+
+ }).then(function() {
+ var frame = document.createElement('iframe');
+ document.body.appendChild(frame);
+ frame.src = 'fake_download';
+
+ // The service worker is unregistered in the fetch event. The window and
+ // frame are cleaned up from the browser chrome script.
+ });
+});
+</script>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/download/worker.js b/dom/serviceworkers/test/download/worker.js
new file mode 100644
index 0000000000..71fb502f1c
--- /dev/null
+++ b/dom/serviceworkers/test/download/worker.js
@@ -0,0 +1,34 @@
+addEventListener("install", function(evt) {
+ evt.waitUntil(self.skipWaiting());
+});
+
+addEventListener("activate", function(evt) {
+ // We claim the current clients in order to ensure that we have an
+ // active client when we call unregister in the fetch handler. Otherwise
+ // the unregister() can kill the current worker before returning a
+ // response.
+ evt.waitUntil(clients.claim());
+});
+
+addEventListener("fetch", function(evt) {
+ // This worker may live long enough to receive a fetch event from the next
+ // test. Just pass such requests through to the network.
+ if (!evt.request.url.includes("fake_download")) {
+ return;
+ }
+
+ // We should only get a single download fetch event. Automatically unregister.
+ evt.respondWith(
+ registration.unregister().then(function() {
+ return new Response("service worker generated download", {
+ headers: {
+ "Content-Disposition": 'attachment; filename="fake_download.bin"',
+ // Prevent the default text editor from being launched
+ "Content-Type": "application/octet-stream",
+ // fake encoding header that should have no effect
+ "Content-Encoding": "gzip",
+ },
+ });
+ })
+ );
+});
diff --git a/dom/serviceworkers/test/download_canceled/page_download_canceled.html b/dom/serviceworkers/test/download_canceled/page_download_canceled.html
new file mode 100644
index 0000000000..e3904c4967
--- /dev/null
+++ b/dom/serviceworkers/test/download_canceled/page_download_canceled.html
@@ -0,0 +1,58 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+</head>
+<body>
+
+<script src="../utils.js"></script>
+<script type="text/javascript">
+function wait_until_controlled() {
+ return new Promise(function(resolve) {
+ if (navigator.serviceWorker.controller) {
+ return resolve('controlled');
+ }
+ navigator.serviceWorker.addEventListener('controllerchange', function onController() {
+ if (navigator.serviceWorker.controller) {
+ navigator.serviceWorker.removeEventListener('controllerchange', onController);
+ return resolve('controlled');
+ }
+ });
+ });
+}
+addEventListener('load', async function(event) {
+ window.controlled = wait_until_controlled();
+ window.registration =
+ await navigator.serviceWorker.register('sw_download_canceled.js');
+ let sw = registration.installing || registration.waiting ||
+ registration.active;
+ await waitForState(sw, 'activated');
+ sw.postMessage('claim');
+});
+
+// Place to hold promises for stream closures reported by the SW.
+window.streamClosed = {};
+
+// The ServiceWorker will postMessage to this BroadcastChannel when the streams
+// are closed. (Alternately, the SW could have used the clients API to post at
+// us, but the mechanism by which that operates would be different when this
+// test is uplifted, and it's desirable to avoid timing changes.)
+//
+// The browser test will use this promise to wait on stream shutdown.
+window.swStreamChannel = new BroadcastChannel("stream-closed");
+function trackStreamClosure(path) {
+ let resolve;
+ const promise = new Promise(r => { resolve = r });
+ window.streamClosed[path] = { promise, resolve };
+}
+window.swStreamChannel.onmessage = ({ data }) => {
+ window.streamClosed[data.what].resolve(data);
+};
+</script>
+
+</body>
+</html>
diff --git a/dom/serviceworkers/test/download_canceled/server-stream-download.sjs b/dom/serviceworkers/test/download_canceled/server-stream-download.sjs
new file mode 100644
index 0000000000..e6ebd68126
--- /dev/null
+++ b/dom/serviceworkers/test/download_canceled/server-stream-download.sjs
@@ -0,0 +1,133 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const { setInterval, clearInterval } = ChromeUtils.importESModule(
+ "resource://gre/modules/Timer.sys.mjs"
+);
+
+// stolen from file_blocked_script.sjs
+function setGlobalState(data, key) {
+ x = {
+ data,
+ QueryInterface(iid) {
+ return this;
+ },
+ };
+ x.wrappedJSObject = x;
+ setObjectState(key, x);
+}
+
+function getGlobalState(key) {
+ var data;
+ getObjectState(key, function(x) {
+ data = x && x.wrappedJSObject.data;
+ });
+ return data;
+}
+
+/*
+ * We want to let the sw_download_canceled.js service worker know when the
+ * stream was canceled. To this end, we let it issue a monitor request which we
+ * fulfill when the stream has been canceled. In order to coordinate between
+ * multiple requests, we use the getObjectState/setObjectState mechanism that
+ * httpd.js exposes to let data be shared and/or persist between requests. We
+ * handle both possible orderings of the requests because we currently don't
+ * try and impose an ordering between the two requests as issued by the SW, and
+ * file_blocked_script.sjs encourages us to do this, but we probably could order
+ * them.
+ */
+const MONITOR_KEY = "stream-monitor";
+function completeMonitorResponse(response, data) {
+ response.write(JSON.stringify(data));
+ response.finish();
+}
+function handleMonitorRequest(request, response) {
+ response.setHeader("Content-Type", "application/json");
+ response.setStatusLine(request.httpVersion, 200, "Found");
+
+ response.processAsync();
+ // Necessary to cause the headers to be flushed; that or touching the
+ // bodyOutputStream getter.
+ response.write("");
+ dump("server-stream-download.js: monitor headers issued\n");
+
+ const alreadyCompleted = getGlobalState(MONITOR_KEY);
+ if (alreadyCompleted) {
+ completeMonitorResponse(response, alreadyCompleted);
+ setGlobalState(null, MONITOR_KEY);
+ } else {
+ setGlobalState(response, MONITOR_KEY);
+ }
+}
+
+const MAX_TICK_COUNT = 3000;
+const TICK_INTERVAL = 2;
+function handleStreamRequest(request, response) {
+ const name = "server-stream-download";
+
+ // Create some payload to send.
+ let strChunk =
+ "Static routes are the future of ServiceWorkers! So say we all!\n";
+ while (strChunk.length < 1024) {
+ strChunk += strChunk;
+ }
+
+ response.setHeader("Content-Disposition", `attachment; filename="${name}"`);
+ response.setHeader(
+ "Content-Type",
+ `application/octet-stream; name="${name}"`
+ );
+ response.setHeader("Content-Length", `${strChunk.length * MAX_TICK_COUNT}`);
+ response.setStatusLine(request.httpVersion, 200, "Found");
+
+ response.processAsync();
+ response.write(strChunk);
+ dump("server-stream-download.js: stream headers + first payload issued\n");
+
+ let count = 0;
+ let intervalId;
+ function closeStream(why, message) {
+ dump("server-stream-download.js: closing stream: " + why + "\n");
+ clearInterval(intervalId);
+ response.finish();
+
+ const data = { why, message };
+ const monitorResponse = getGlobalState(MONITOR_KEY);
+ if (monitorResponse) {
+ completeMonitorResponse(monitorResponse, data);
+ setGlobalState(null, MONITOR_KEY);
+ } else {
+ setGlobalState(data, MONITOR_KEY);
+ }
+ }
+ function tick() {
+ try {
+ // bound worst-case behavior.
+ if (count++ > MAX_TICK_COUNT) {
+ closeStream("timeout", "timeout");
+ return;
+ }
+ response.write(strChunk);
+ } catch (e) {
+ closeStream("canceled", e.message);
+ }
+ }
+ intervalId = setInterval(tick, TICK_INTERVAL);
+}
+
+Components.utils.importGlobalProperties(["URLSearchParams"]);
+function handleRequest(request, response) {
+ dump(
+ "server-stream-download.js: processing request for " +
+ request.path +
+ "?" +
+ request.queryString +
+ "\n"
+ );
+ const query = new URLSearchParams(request.queryString);
+ if (query.has("monitor")) {
+ handleMonitorRequest(request, response);
+ } else {
+ handleStreamRequest(request, response);
+ }
+}
diff --git a/dom/serviceworkers/test/download_canceled/sw_download_canceled.js b/dom/serviceworkers/test/download_canceled/sw_download_canceled.js
new file mode 100644
index 0000000000..5d9d5f9bfd
--- /dev/null
+++ b/dom/serviceworkers/test/download_canceled/sw_download_canceled.js
@@ -0,0 +1,150 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// This file is derived from :bkelly's https://glitch.com/edit/#!/html-sw-stream
+
+addEventListener("install", evt => {
+ evt.waitUntil(self.skipWaiting());
+});
+
+// Create a BroadcastChannel to notify when we have closed our streams.
+const channel = new BroadcastChannel("stream-closed");
+
+const MAX_TICK_COUNT = 3000;
+const TICK_INTERVAL = 4;
+/**
+ * Generate a continuous stream of data at a sufficiently high frequency that a
+ * there"s a good chance of racing channel cancellation.
+ */
+function handleStream(evt, filename) {
+ // Create some payload to send.
+ const encoder = new TextEncoder();
+ let strChunk =
+ "Static routes are the future of ServiceWorkers! So say we all!\n";
+ while (strChunk.length < 1024) {
+ strChunk += strChunk;
+ }
+ const dataChunk = encoder.encode(strChunk);
+
+ evt.waitUntil(
+ new Promise(resolve => {
+ let body = new ReadableStream({
+ start: controller => {
+ const closeStream = why => {
+ console.log("closing stream: " + JSON.stringify(why) + "\n");
+ clearInterval(intervalId);
+ resolve();
+ // In event of error, the controller will automatically have closed.
+ if (why.why != "canceled") {
+ try {
+ controller.close();
+ } catch (ex) {
+ // If we thought we should cancel but experienced a problem,
+ // that's a different kind of failure and we need to report it.
+ // (If we didn't catch the exception here, we'd end up erroneously
+ // in the tick() method's canceled handler.)
+ channel.postMessage({
+ what: filename,
+ why: "close-failure",
+ message: ex.message,
+ ticks: why.ticks,
+ });
+ return;
+ }
+ }
+ // Post prior to performing any attempt to close...
+ channel.postMessage(why);
+ };
+
+ controller.enqueue(dataChunk);
+ let count = 0;
+ let intervalId;
+ function tick() {
+ try {
+ // bound worst-case behavior.
+ if (count++ > MAX_TICK_COUNT) {
+ closeStream({
+ what: filename,
+ why: "timeout",
+ message: "timeout",
+ ticks: count,
+ });
+ return;
+ }
+ controller.enqueue(dataChunk);
+ } catch (e) {
+ closeStream({
+ what: filename,
+ why: "canceled",
+ message: e.message,
+ ticks: count,
+ });
+ }
+ }
+ // Alternately, streams' pull mechanism could be used here, but this
+ // test doesn't so much want to saturate the stream as to make sure the
+ // data is at least flowing a little bit. (Also, the author had some
+ // concern about slowing down the test by overwhelming the event loop
+ // and concern that we might not have sufficent back-pressure plumbed
+ // through and an infinite pipe might make bad things happen.)
+ intervalId = setInterval(tick, TICK_INTERVAL);
+ tick();
+ },
+ });
+ evt.respondWith(
+ new Response(body, {
+ headers: {
+ "Content-Disposition": `attachment; filename="${filename}"`,
+ "Content-Type": "application/octet-stream",
+ },
+ })
+ );
+ })
+ );
+}
+
+/**
+ * Use an .sjs to generate a similar stream of data to the above, passing the
+ * response through directly. Because we're handing off the response but also
+ * want to be able to report when cancellation occurs, we create a second,
+ * overlapping long-poll style fetch that will not finish resolving until the
+ * .sjs experiences closure of its socket and terminates the payload stream.
+ */
+function handlePassThrough(evt, filename) {
+ evt.waitUntil(
+ (async () => {
+ console.log("issuing monitor fetch request");
+ const response = await fetch("server-stream-download.sjs?monitor");
+ console.log("monitor headers received, awaiting body");
+ const data = await response.json();
+ console.log("passthrough monitor fetch completed, notifying.");
+ channel.postMessage({
+ what: filename,
+ why: data.why,
+ message: data.message,
+ });
+ })()
+ );
+ evt.respondWith(
+ fetch("server-stream-download.sjs").then(response => {
+ console.log("server-stream-download.sjs Response received, propagating");
+ return response;
+ })
+ );
+}
+
+addEventListener("fetch", evt => {
+ console.log(`SW processing fetch of ${evt.request.url}`);
+ if (evt.request.url.includes("sw-stream-download")) {
+ return handleStream(evt, "sw-stream-download");
+ }
+ if (evt.request.url.includes("sw-passthrough-download")) {
+ return handlePassThrough(evt, "sw-passthrough-download");
+ }
+});
+
+addEventListener("message", evt => {
+ if (evt.data === "claim") {
+ evt.waitUntil(clients.claim());
+ }
+});
diff --git a/dom/serviceworkers/test/empty.html b/dom/serviceworkers/test/empty.html
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/dom/serviceworkers/test/empty.html
diff --git a/dom/serviceworkers/test/empty.js b/dom/serviceworkers/test/empty.js
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/dom/serviceworkers/test/empty.js
diff --git a/dom/serviceworkers/test/empty_with_utils.html b/dom/serviceworkers/test/empty_with_utils.html
new file mode 100644
index 0000000000..75f0aa8872
--- /dev/null
+++ b/dom/serviceworkers/test/empty_with_utils.html
@@ -0,0 +1,13 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <script src="utils.js" type="text/javascript"></script>
+</head>
+<body>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/error_reporting_helpers.js b/dom/serviceworkers/test/error_reporting_helpers.js
new file mode 100644
index 0000000000..42ddbe42a2
--- /dev/null
+++ b/dom/serviceworkers/test/error_reporting_helpers.js
@@ -0,0 +1,73 @@
+"use strict";
+
+/**
+ * Helpers for use in tests that want to verify that localized error messages
+ * are logged during the test. Because most of our errors (ex:
+ * ServiceWorkerManager) generate nsIScriptError instances with flattened
+ * strings (the interpolated arguments aren't kept around), we load the string
+ * bundle and use it to derive the exact string message we expect for the given
+ * payload.
+ **/
+
+let stringBundleService = SpecialPowers.Cc[
+ "@mozilla.org/intl/stringbundle;1"
+].getService(SpecialPowers.Ci.nsIStringBundleService);
+let localizer = stringBundleService.createBundle(
+ "chrome://global/locale/dom/dom.properties"
+);
+
+/**
+ * Start monitoring the console for the given localized error message string(s)
+ * with the given arguments to be logged. Call before running code that will
+ * generate the console message. Pair with a call to
+ * `wait_for_expected_message` invoked after the message should have been
+ * generated.
+ *
+ * Multiple error messages can be expected, just repeat the msgId and args
+ * argument pair as needed.
+ *
+ * @param {String} msgId
+ * The localization message identifier used in the properties file.
+ * @param {String[]} args
+ * The list of formatting arguments we expect the error to be generated with.
+ * @return {Object} Promise/handle to pass to wait_for_expected_message.
+ */
+function expect_console_message(/* msgId, args, ... */) {
+ let expectations = [];
+ // process repeated paired arguments of: msgId, args
+ for (let i = 0; i < arguments.length; i += 2) {
+ let msgId = arguments[i];
+ let args = arguments[i + 1];
+ if (args.length === 0) {
+ expectations.push({ errorMessage: localizer.GetStringFromName(msgId) });
+ } else {
+ expectations.push({
+ errorMessage: localizer.formatStringFromName(msgId, args),
+ });
+ }
+ }
+ return new Promise(resolve => {
+ SimpleTest.monitorConsole(resolve, expectations);
+ });
+}
+let expect_console_messages = expect_console_message;
+
+/**
+ * Stop monitoring the console, returning a Promise that will be resolved when
+ * the sentinel console message sent through the async data path has been
+ * received. The Promise will not reject on failure; instead a mochitest
+ * failure will have been generated by ok(false)/equivalent by the time it is
+ * resolved.
+ */
+function wait_for_expected_message(expectedPromise) {
+ SimpleTest.endMonitorConsole();
+ return expectedPromise;
+}
+
+/**
+ * Derive an absolute URL string from a relative URL to simplify error message
+ * argument generation.
+ */
+function make_absolute_url(relUrl) {
+ return new URL(relUrl, window.location).href;
+}
diff --git a/dom/serviceworkers/test/eval_worker.js b/dom/serviceworkers/test/eval_worker.js
new file mode 100644
index 0000000000..79b4808e66
--- /dev/null
+++ b/dom/serviceworkers/test/eval_worker.js
@@ -0,0 +1 @@
+eval("1+1");
diff --git a/dom/serviceworkers/test/eventsource/eventsource.resource b/dom/serviceworkers/test/eventsource/eventsource.resource
new file mode 100644
index 0000000000..eb62cbd4c5
--- /dev/null
+++ b/dom/serviceworkers/test/eventsource/eventsource.resource
@@ -0,0 +1,22 @@
+:this file must be enconded in utf8
+:and its Content-Type must be equal to text/event-stream
+
+retry:500
+data: 2
+unknow: unknow
+
+event: other_event_name
+retry:500
+data: 2
+unknow: unknow
+
+event: click
+retry:500
+
+event: blur
+retry:500
+
+event:keypress
+retry:500
+
+
diff --git a/dom/serviceworkers/test/eventsource/eventsource.resource^headers^ b/dom/serviceworkers/test/eventsource/eventsource.resource^headers^
new file mode 100644
index 0000000000..5b88be7c32
--- /dev/null
+++ b/dom/serviceworkers/test/eventsource/eventsource.resource^headers^
@@ -0,0 +1,3 @@
+Content-Type: text/event-stream
+Cache-Control: no-cache, must-revalidate
+Access-Control-Allow-Origin: *
diff --git a/dom/serviceworkers/test/eventsource/eventsource_cors_response.html b/dom/serviceworkers/test/eventsource/eventsource_cors_response.html
new file mode 100644
index 0000000000..115a0f5c65
--- /dev/null
+++ b/dom/serviceworkers/test/eventsource/eventsource_cors_response.html
@@ -0,0 +1,75 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1182103 - Test EventSource scenarios with fetch interception</title>
+ <script type="text/javascript">
+
+ var prefix = "http://mochi.test:8888/tests/dom/serviceworkers/test/eventsource/";
+
+ function ok(aCondition, aMessage) {
+ parent.postMessage({status: "callback", data: "ok", condition: aCondition, message: aMessage}, "*");
+ }
+
+ function doUnregister() {
+ navigator.serviceWorker.getRegistration().then(swr => {
+ swr.unregister().then(function(result) {
+ ok(result, "Unregister should return true.");
+ parent.postMessage({status: "callback", data: "done"}, "*");
+ }, function(e) {
+ ok(false, "Unregistering the SW failed with " + e);
+ });
+ });
+ }
+
+ function doEventSource() {
+ var source = new EventSource(prefix + "eventsource.resource");
+ source.onmessage = function(e) {
+ source.onmessage = null;
+ source.close();
+ ok(true, "EventSource should work with cors responses");
+ doUnregister();
+ };
+ source.onerror = function(error) {
+ source.onerror = null;
+ source.close();
+ ok(false, "Something went wrong");
+ };
+ }
+
+ function onLoad() {
+ if (!parent) {
+ dump("eventsource/eventsource_cors_response.html shouldn't be launched directly!");
+ }
+
+ window.addEventListener("message", function onMessage(e) {
+ if (e.data.status === "callback") {
+ switch(e.data.data) {
+ case "eventsource":
+ doEventSource();
+ window.removeEventListener("message", onMessage);
+ break;
+ default:
+ ok(false, "Something went wrong")
+ break
+ }
+ }
+ });
+
+ navigator.serviceWorker.ready.then(function() {
+ parent.postMessage({status: "callback", data: "ready"}, "*");
+ });
+
+ navigator.serviceWorker.addEventListener("message", function(event) {
+ parent.postMessage(event.data, "*");
+ });
+ }
+
+ </script>
+</head>
+<body onload="onLoad()">
+</body>
+</html>
diff --git a/dom/serviceworkers/test/eventsource/eventsource_cors_response_intercept_worker.js b/dom/serviceworkers/test/eventsource/eventsource_cors_response_intercept_worker.js
new file mode 100644
index 0000000000..0b9535d08e
--- /dev/null
+++ b/dom/serviceworkers/test/eventsource/eventsource_cors_response_intercept_worker.js
@@ -0,0 +1,30 @@
+// Cross origin request
+var prefix = "http://example.com/tests/dom/serviceworkers/test/eventsource/";
+
+self.importScripts("eventsource_worker_helper.js");
+
+self.addEventListener("fetch", function(event) {
+ var request = event.request;
+ var url = new URL(request.url);
+
+ if (
+ url.pathname !==
+ "/tests/dom/serviceworkers/test/eventsource/eventsource.resource"
+ ) {
+ return;
+ }
+
+ ok(request.mode === "cors", "EventSource should make a CORS request");
+ ok(
+ request.cache === "no-store",
+ "EventSource should make a no-store request"
+ );
+ var fetchRequest = new Request(prefix + "eventsource.resource", {
+ mode: "cors",
+ });
+ event.respondWith(
+ fetch(fetchRequest).then(fetchResponse => {
+ return fetchResponse;
+ })
+ );
+});
diff --git a/dom/serviceworkers/test/eventsource/eventsource_mixed_content_cors_response.html b/dom/serviceworkers/test/eventsource/eventsource_mixed_content_cors_response.html
new file mode 100644
index 0000000000..970cae517f
--- /dev/null
+++ b/dom/serviceworkers/test/eventsource/eventsource_mixed_content_cors_response.html
@@ -0,0 +1,75 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1182103 - Test EventSource scenarios with fetch interception</title>
+ <script type="text/javascript">
+
+ var prefix = "https://example.com/tests/dom/serviceworkers/test/eventsource/";
+
+ function ok(aCondition, aMessage) {
+ parent.postMessage({status: "callback", data: "ok", condition: aCondition, message: aMessage}, "*");
+ }
+
+ function doUnregister() {
+ navigator.serviceWorker.getRegistration().then(swr => {
+ swr.unregister().then(function(result) {
+ ok(result, "Unregister should return true.");
+ parent.postMessage({status: "callback", data: "done"}, "*");
+ }, function(e) {
+ ok(false, "Unregistering the SW failed with " + e);
+ });
+ });
+ }
+
+ function doEventSource() {
+ var source = new EventSource(prefix + "eventsource.resource");
+ source.onmessage = function(e) {
+ source.onmessage = null;
+ source.close();
+ ok(false, "Something went wrong");
+ };
+ source.onerror = function(error) {
+ source.onerror = null;
+ source.close();
+ ok(true, "EventSource should not work with mixed content cors responses");
+ doUnregister();
+ };
+ }
+
+ function onLoad() {
+ if (!parent) {
+ dump("eventsource/eventsource_cors_response.html shouldn't be launched directly!");
+ }
+
+ window.addEventListener("message", function onMessage(e) {
+ if (e.data.status === "callback") {
+ switch(e.data.data) {
+ case "eventsource":
+ doEventSource();
+ window.removeEventListener("message", onMessage);
+ break;
+ default:
+ ok(false, "Something went wrong")
+ break
+ }
+ }
+ });
+
+ navigator.serviceWorker.ready.then(function() {
+ parent.postMessage({status: "callback", data: "ready"}, "*");
+ });
+
+ navigator.serviceWorker.addEventListener("message", function(event) {
+ parent.postMessage(event.data, "*");
+ });
+ }
+
+ </script>
+</head>
+<body onload="onLoad()">
+</body>
+</html>
diff --git a/dom/serviceworkers/test/eventsource/eventsource_mixed_content_cors_response_intercept_worker.js b/dom/serviceworkers/test/eventsource/eventsource_mixed_content_cors_response_intercept_worker.js
new file mode 100644
index 0000000000..754e461a01
--- /dev/null
+++ b/dom/serviceworkers/test/eventsource/eventsource_mixed_content_cors_response_intercept_worker.js
@@ -0,0 +1,29 @@
+var prefix = "http://example.com/tests/dom/serviceworkers/test/eventsource/";
+
+self.importScripts("eventsource_worker_helper.js");
+
+self.addEventListener("fetch", function(event) {
+ var request = event.request;
+ var url = new URL(request.url);
+
+ if (
+ url.pathname !==
+ "/tests/dom/serviceworkers/test/eventsource/eventsource.resource"
+ ) {
+ return;
+ }
+
+ ok(request.mode === "cors", "EventSource should make a CORS request");
+ ok(
+ request.cache === "no-store",
+ "EventSource should make a no-store request"
+ );
+ var fetchRequest = new Request(prefix + "eventsource.resource", {
+ mode: "cors",
+ });
+ event.respondWith(
+ fetch(fetchRequest).then(fetchResponse => {
+ return fetchResponse;
+ })
+ );
+});
diff --git a/dom/serviceworkers/test/eventsource/eventsource_opaque_response.html b/dom/serviceworkers/test/eventsource/eventsource_opaque_response.html
new file mode 100644
index 0000000000..bce12259cc
--- /dev/null
+++ b/dom/serviceworkers/test/eventsource/eventsource_opaque_response.html
@@ -0,0 +1,75 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1182103 - Test EventSource scenarios with fetch interception</title>
+ <script type="text/javascript">
+
+ var prefix = "http://mochi.test:8888/tests/dom/serviceworkers/test/eventsource/";
+
+ function ok(aCondition, aMessage) {
+ parent.postMessage({status: "callback", data: "ok", condition: aCondition, message: aMessage}, "*");
+ }
+
+ function doUnregister() {
+ navigator.serviceWorker.getRegistration().then(swr => {
+ swr.unregister().then(function(result) {
+ ok(result, "Unregister should return true.");
+ parent.postMessage({status: "callback", data: "done"}, "*");
+ }, function(e) {
+ ok(false, "Unregistering the SW failed with " + e);
+ });
+ });
+ }
+
+ function doEventSource() {
+ var source = new EventSource(prefix + "eventsource.resource");
+ source.onmessage = function(e) {
+ source.onmessage = null;
+ source.close();
+ ok(false, "Something went wrong");
+ };
+ source.onerror = function(error) {
+ source.onerror = null;
+ source.close();
+ ok(true, "EventSource should not work with opaque responses");
+ doUnregister();
+ };
+ }
+
+ function onLoad() {
+ if (!parent) {
+ dump("eventsource/eventsource_opaque_response.html shouldn't be launched directly!");
+ }
+
+ window.addEventListener("message", function onMessage(e) {
+ if (e.data.status === "callback") {
+ switch(e.data.data) {
+ case "eventsource":
+ doEventSource();
+ window.removeEventListener("message", onMessage);
+ break;
+ default:
+ ok(false, "Something went wrong")
+ break
+ }
+ }
+ });
+
+ navigator.serviceWorker.ready.then(function() {
+ parent.postMessage({status: "callback", data: "ready"}, "*");
+ });
+
+ navigator.serviceWorker.addEventListener("message", function(event) {
+ parent.postMessage(event.data, "*");
+ });
+ }
+
+ </script>
+</head>
+<body onload="onLoad()">
+</body>
+</html>
diff --git a/dom/serviceworkers/test/eventsource/eventsource_opaque_response_intercept_worker.js b/dom/serviceworkers/test/eventsource/eventsource_opaque_response_intercept_worker.js
new file mode 100644
index 0000000000..f663e7e39c
--- /dev/null
+++ b/dom/serviceworkers/test/eventsource/eventsource_opaque_response_intercept_worker.js
@@ -0,0 +1,30 @@
+// Cross origin request
+var prefix = "http://example.com/tests/dom/serviceworkers/test/eventsource/";
+
+self.importScripts("eventsource_worker_helper.js");
+
+self.addEventListener("fetch", function(event) {
+ var request = event.request;
+ var url = new URL(request.url);
+
+ if (
+ url.pathname !==
+ "/tests/dom/serviceworkers/test/eventsource/eventsource.resource"
+ ) {
+ return;
+ }
+
+ ok(request.mode === "cors", "EventSource should make a CORS request");
+ ok(
+ request.cache === "no-store",
+ "EventSource should make a no-store request"
+ );
+ var fetchRequest = new Request(prefix + "eventsource.resource", {
+ mode: "no-cors",
+ });
+ event.respondWith(
+ fetch(fetchRequest).then(fetchResponse => {
+ return fetchResponse;
+ })
+ );
+});
diff --git a/dom/serviceworkers/test/eventsource/eventsource_register_worker.html b/dom/serviceworkers/test/eventsource/eventsource_register_worker.html
new file mode 100644
index 0000000000..59e8e92ab6
--- /dev/null
+++ b/dom/serviceworkers/test/eventsource/eventsource_register_worker.html
@@ -0,0 +1,27 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1182103 - Test EventSource scenarios with fetch interception</title>
+ <script type="text/javascript">
+
+ function getURLParam (aTarget, aValue) {
+ return decodeURI(aTarget.search.replace(new RegExp("^(?:.*[&\\?]" + encodeURI(aValue).replace(/[\.\+\*]/g, "\\$&") + "(?:\\=([^&]*))?)?.*$", "i"), "$1"));
+ }
+
+ function onLoad() {
+ navigator.serviceWorker.ready.then(function() {
+ parent.postMessage({status: "callback", data: "done"}, "*");
+ });
+
+ navigator.serviceWorker.register(getURLParam(document.location, "script"), {scope: "."});
+ }
+
+ </script>
+</head>
+<body onload="onLoad()">
+</body>
+</html>
diff --git a/dom/serviceworkers/test/eventsource/eventsource_synthetic_response.html b/dom/serviceworkers/test/eventsource/eventsource_synthetic_response.html
new file mode 100644
index 0000000000..7f6228c91e
--- /dev/null
+++ b/dom/serviceworkers/test/eventsource/eventsource_synthetic_response.html
@@ -0,0 +1,75 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1182103 - Test EventSource scenarios with fetch interception</title>
+ <script type="text/javascript">
+
+ var prefix = "http://mochi.test:8888/tests/dom/serviceworkers/test/eventsource/";
+
+ function ok(aCondition, aMessage) {
+ parent.postMessage({status: "callback", data: "ok", condition: aCondition, message: aMessage}, "*");
+ }
+
+ function doUnregister() {
+ navigator.serviceWorker.getRegistration().then(swr => {
+ swr.unregister().then(function(result) {
+ ok(result, "Unregister should return true.");
+ parent.postMessage({status: "callback", data: "done"}, "*");
+ }, function(e) {
+ ok(false, "Unregistering the SW failed with " + e);
+ });
+ });
+ }
+
+ function doEventSource() {
+ var source = new EventSource(prefix + "eventsource.resource");
+ source.onmessage = function(e) {
+ source.onmessage = null;
+ source.close();
+ ok(true, "EventSource should work with synthetic responses");
+ doUnregister();
+ };
+ source.onerror = function(error) {
+ source.onmessage = null;
+ source.close();
+ ok(false, "Something went wrong");
+ };
+ }
+
+ function onLoad() {
+ if (!parent) {
+ dump("eventsource/eventsource_synthetic_response.html shouldn't be launched directly!");
+ }
+
+ window.addEventListener("message", function onMessage(e) {
+ if (e.data.status === "callback") {
+ switch(e.data.data) {
+ case "eventsource":
+ doEventSource();
+ window.removeEventListener("message", onMessage);
+ break;
+ default:
+ ok(false, "Something went wrong")
+ break
+ }
+ }
+ });
+
+ navigator.serviceWorker.ready.then(function() {
+ parent.postMessage({status: "callback", data: "ready"}, "*");
+ });
+
+ navigator.serviceWorker.addEventListener("message", function(event) {
+ parent.postMessage(event.data, "*");
+ });
+ }
+
+ </script>
+</head>
+<body onload="onLoad()">
+</body>
+</html>
diff --git a/dom/serviceworkers/test/eventsource/eventsource_synthetic_response_intercept_worker.js b/dom/serviceworkers/test/eventsource/eventsource_synthetic_response_intercept_worker.js
new file mode 100644
index 0000000000..32aeb91b81
--- /dev/null
+++ b/dom/serviceworkers/test/eventsource/eventsource_synthetic_response_intercept_worker.js
@@ -0,0 +1,27 @@
+self.importScripts("eventsource_worker_helper.js");
+
+self.addEventListener("fetch", function(event) {
+ var request = event.request;
+ var url = new URL(request.url);
+
+ if (
+ url.pathname !==
+ "/tests/dom/serviceworkers/test/eventsource/eventsource.resource"
+ ) {
+ return;
+ }
+
+ ok(request.mode === "cors", "EventSource should make a CORS request");
+ var headerList = {
+ "Content-Type": "text/event-stream",
+ "Cache-Control": "no-cache, must-revalidate",
+ };
+ var headers = new Headers(headerList);
+ var init = {
+ headers,
+ mode: "cors",
+ };
+ var body = "data: data0\r\r";
+ var response = new Response(body, init);
+ event.respondWith(response);
+});
diff --git a/dom/serviceworkers/test/eventsource/eventsource_worker_helper.js b/dom/serviceworkers/test/eventsource/eventsource_worker_helper.js
new file mode 100644
index 0000000000..b110ae5f58
--- /dev/null
+++ b/dom/serviceworkers/test/eventsource/eventsource_worker_helper.js
@@ -0,0 +1,17 @@
+function ok(aCondition, aMessage) {
+ return new Promise(function(resolve, reject) {
+ self.clients.matchAll().then(function(res) {
+ if (!res.length) {
+ reject();
+ return;
+ }
+ res[0].postMessage({
+ status: "callback",
+ data: "ok",
+ condition: aCondition,
+ message: aMessage,
+ });
+ resolve();
+ });
+ });
+}
diff --git a/dom/serviceworkers/test/fetch.js b/dom/serviceworkers/test/fetch.js
new file mode 100644
index 0000000000..ca723821ea
--- /dev/null
+++ b/dom/serviceworkers/test/fetch.js
@@ -0,0 +1,33 @@
+function get_query_params(url) {
+ var search = new URL(url).search;
+ if (!search) {
+ return {};
+ }
+ var ret = {};
+ var params = search.substring(1).split("&");
+ params.forEach(function(param) {
+ var element = param.split("=");
+ ret[decodeURIComponent(element[0])] = decodeURIComponent(element[1]);
+ });
+ return ret;
+}
+
+addEventListener("fetch", function(event) {
+ if (event.request.url.includes("fail.html")) {
+ event.respondWith(fetch("hello.html", { integrity: "abc" }));
+ } else if (event.request.url.includes("fake.html")) {
+ event.respondWith(fetch("hello.html"));
+ } else if (event.request.url.includes("file_js_cache")) {
+ event.respondWith(fetch(event.request));
+ } else if (event.request.url.includes("redirect")) {
+ let param = get_query_params(event.request.url);
+ let url = param.url;
+ let mode = param.mode;
+
+ event.respondWith(fetch(url, { mode }));
+ }
+});
+
+addEventListener("activate", function(event) {
+ event.waitUntil(clients.claim());
+});
diff --git a/dom/serviceworkers/test/fetch/cookie/cookie_test.js b/dom/serviceworkers/test/fetch/cookie/cookie_test.js
new file mode 100644
index 0000000000..cdfee8949d
--- /dev/null
+++ b/dom/serviceworkers/test/fetch/cookie/cookie_test.js
@@ -0,0 +1,11 @@
+self.addEventListener("fetch", function(event) {
+ if (event.request.url.includes("synth.html")) {
+ var body =
+ "<script>" +
+ 'window.parent.postMessage({status: "done", cookie: document.cookie}, "*");' +
+ "</script>";
+ event.respondWith(
+ new Response(body, { headers: { "Content-Type": "text/html" } })
+ );
+ }
+});
diff --git a/dom/serviceworkers/test/fetch/cookie/register.html b/dom/serviceworkers/test/fetch/cookie/register.html
new file mode 100644
index 0000000000..99eabaf0a2
--- /dev/null
+++ b/dom/serviceworkers/test/fetch/cookie/register.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<script src="../../utils.js"></script>
+<script>
+ function ok(v, msg) {
+ window.parent.postMessage({status: "ok", result: !!v, message: msg}, "*");
+ }
+
+ function done(reg) {
+ ok(reg.active, "The active worker should be available.");
+ window.parent.postMessage({status: "registrationdone"}, "*");
+ }
+
+ document.cookie = "foo=bar";
+
+ navigator.serviceWorker.register("cookie_test.js", {scope: "."})
+ .then(reg => {
+ return waitForState(reg.installing, "activated", reg);
+ }).then(done);
+</script>
diff --git a/dom/serviceworkers/test/fetch/cookie/unregister.html b/dom/serviceworkers/test/fetch/cookie/unregister.html
new file mode 100644
index 0000000000..1f13508fa7
--- /dev/null
+++ b/dom/serviceworkers/test/fetch/cookie/unregister.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<script>
+ navigator.serviceWorker.getRegistration(".").then(function(registration) {
+ registration.unregister().then(function(success) {
+ if (success) {
+ window.parent.postMessage({status: "unregistrationdone"}, "*");
+ }
+ }, function(e) {
+ dump("Unregistering the SW failed with " + e + "\n");
+ });
+ });
+</script>
diff --git a/dom/serviceworkers/test/fetch/deliver-gzip.sjs b/dom/serviceworkers/test/fetch/deliver-gzip.sjs
new file mode 100644
index 0000000000..31d2a71f7a
--- /dev/null
+++ b/dom/serviceworkers/test/fetch/deliver-gzip.sjs
@@ -0,0 +1,23 @@
+"use strict";
+
+function handleRequest(request, response) {
+ // The string "hello" repeated 10 times followed by newline. Compressed using gzip.
+ /* eslint-disable prettier/prettier */
+ let bytes = [0x1f, 0x8b, 0x08, 0x08, 0x4d, 0xe2, 0xf9, 0x54, 0x00, 0x03, 0x68,
+ 0x65, 0x6c, 0x6c, 0x6f, 0x00, 0xcb, 0x48, 0xcd, 0xc9, 0xc9, 0xcf,
+ 0x20, 0x85, 0xe0, 0x02, 0x00, 0xf5, 0x4b, 0x38, 0xcf, 0x33, 0x00,
+ 0x00, 0x00];
+
+ /* eslint-enable prettier/prettier */
+
+ response.setHeader("Content-Encoding", "gzip", false);
+ response.setHeader("Content-Length", "" + bytes.length, false);
+ response.setHeader("Content-Type", "text/plain", false);
+
+ let bos = Components.classes[
+ "@mozilla.org/binaryoutputstream;1"
+ ].createInstance(Components.interfaces.nsIBinaryOutputStream);
+ bos.setOutputStream(response.bodyOutputStream);
+
+ bos.writeByteArray(bytes);
+}
diff --git a/dom/serviceworkers/test/fetch/fetch_tests.js b/dom/serviceworkers/test/fetch/fetch_tests.js
new file mode 100644
index 0000000000..bbe55c3a35
--- /dev/null
+++ b/dom/serviceworkers/test/fetch/fetch_tests.js
@@ -0,0 +1,716 @@
+var origin = "http://mochi.test:8888";
+
+function fetchXHRWithMethod(name, method, onload, onerror, headers) {
+ expectAsyncResult();
+
+ onload =
+ onload ||
+ function() {
+ my_ok(false, "XHR load should not complete successfully");
+ finish();
+ };
+ onerror =
+ onerror ||
+ function() {
+ my_ok(
+ false,
+ "XHR load for " + name + " should be intercepted successfully"
+ );
+ finish();
+ };
+
+ var x = new XMLHttpRequest();
+ x.open(method, name, true);
+ x.onload = function() {
+ onload(x);
+ };
+ x.onerror = function() {
+ onerror(x);
+ };
+ headers = headers || [];
+ headers.forEach(function(header) {
+ x.setRequestHeader(header[0], header[1]);
+ });
+ x.send();
+}
+
+var corsServerPath =
+ "/tests/dom/security/test/cors/file_CrossSiteXHR_server.sjs";
+var corsServerURL = "http://example.com" + corsServerPath;
+
+function redirectURL(hops) {
+ return (
+ hops[0].server +
+ corsServerPath +
+ "?hop=1&hops=" +
+ encodeURIComponent(JSON.stringify(hops))
+ );
+}
+
+function fetchXHR(name, onload, onerror, headers) {
+ return fetchXHRWithMethod(name, "GET", onload, onerror, headers);
+}
+
+fetchXHR("bare-synthesized.txt", function(xhr) {
+ my_ok(xhr.status == 200, "load should be successful");
+ my_ok(
+ xhr.responseText == "synthesized response body",
+ "load should have synthesized response"
+ );
+ finish();
+});
+
+fetchXHR("test-respondwith-response.txt", function(xhr) {
+ my_ok(
+ xhr.status == 200,
+ "test-respondwith-response load should be successful"
+ );
+ my_ok(
+ xhr.responseText == "test-respondwith-response response body",
+ "load should have response"
+ );
+ finish();
+});
+
+fetchXHR("synthesized-404.txt", function(xhr) {
+ my_ok(xhr.status == 404, "load should 404");
+ my_ok(
+ xhr.responseText == "synthesized response body",
+ "404 load should have synthesized response"
+ );
+ finish();
+});
+
+fetchXHR("synthesized-headers.txt", function(xhr) {
+ my_ok(xhr.status == 200, "load should be successful");
+ my_ok(
+ xhr.getResponseHeader("X-Custom-Greeting") === "Hello",
+ "custom header should be set"
+ );
+ my_ok(
+ xhr.responseText == "synthesized response body",
+ "custom header load should have synthesized response"
+ );
+ finish();
+});
+
+fetchXHR("synthesized-redirect-real-file.txt", function(xhr) {
+ dump("Got status AARRGH " + xhr.status + " " + xhr.responseText + "\n");
+ my_ok(xhr.status == 200, "load should be successful");
+ my_ok(
+ xhr.responseText == "This is a real file.\n",
+ "Redirect to real file should complete."
+ );
+ finish();
+});
+
+fetchXHR("synthesized-redirect-twice-real-file.txt", function(xhr) {
+ my_ok(xhr.status == 200, "load should be successful");
+ my_ok(
+ xhr.responseText == "This is a real file.\n",
+ "Redirect to real file (twice) should complete."
+ );
+ finish();
+});
+
+fetchXHR("synthesized-redirect-synthesized.txt", function(xhr) {
+ my_ok(xhr.status == 200, "synth+redirect+synth load should be successful");
+ my_ok(
+ xhr.responseText == "synthesized response body",
+ "load should have redirected+synthesized response"
+ );
+ finish();
+});
+
+fetchXHR("synthesized-redirect-twice-synthesized.txt", function(xhr) {
+ my_ok(
+ xhr.status == 200,
+ "synth+redirect+synth (twice) load should be successful"
+ );
+ my_ok(
+ xhr.responseText == "synthesized response body",
+ "load should have redirected+synthesized (twice) response"
+ );
+ finish();
+});
+
+fetchXHR("redirect.sjs", function(xhr) {
+ my_ok(xhr.status == 404, "redirected load should be uninterrupted");
+ finish();
+});
+
+fetchXHR("ignored.txt", function(xhr) {
+ my_ok(xhr.status == 404, "load should be uninterrupted");
+ finish();
+});
+
+fetchXHR("rejected.txt", null, function(xhr) {
+ my_ok(xhr.status == 0, "load should not complete");
+ finish();
+});
+
+fetchXHR("nonresponse.txt", null, function(xhr) {
+ my_ok(xhr.status == 0, "load should not complete");
+ finish();
+});
+
+fetchXHR("nonresponse2.txt", null, function(xhr) {
+ my_ok(xhr.status == 0, "load should not complete");
+ finish();
+});
+
+fetchXHR("nonpromise.txt", null, function(xhr) {
+ my_ok(xhr.status == 0, "load should not complete");
+ finish();
+});
+
+fetchXHR(
+ "headers.txt",
+ function(xhr) {
+ my_ok(xhr.status == 200, "load should be successful");
+ my_ok(xhr.responseText == "1", "request header checks should have passed");
+ finish();
+ },
+ null,
+ [
+ ["X-Test1", "header1"],
+ ["X-Test2", "header2"],
+ ]
+);
+
+fetchXHR("http://user:pass@mochi.test:8888/user-pass", function(xhr) {
+ my_ok(xhr.status == 200, "load should be successful");
+ my_ok(
+ xhr.responseText == "http://user:pass@mochi.test:8888/user-pass",
+ "The username and password should be preserved"
+ );
+ finish();
+});
+
+fetchXHR("readable-stream.txt", function(xhr) {
+ my_ok(xhr.status == 200, "loading completed");
+ my_ok(xhr.responseText == "Hello!", "The message is correct!");
+ finish();
+});
+
+fetchXHR(
+ "readable-stream-locked.txt",
+ function(xhr) {
+ my_ok(false, "This should not be called!");
+ finish();
+ },
+ function() {
+ my_ok(true, "The exception has been correctly handled!");
+ finish();
+ }
+);
+
+fetchXHR(
+ "readable-stream-with-exception.txt",
+ function(xhr) {
+ my_ok(false, "This should not be called!");
+ finish();
+ },
+ function() {
+ my_ok(true, "The exception has been correctly handled!");
+ finish();
+ }
+);
+
+fetchXHR(
+ "readable-stream-with-exception2.txt",
+ function(xhr) {
+ my_ok(false, "This should not be called!");
+ finish();
+ },
+ function() {
+ my_ok(true, "The exception has been correctly handled!");
+ finish();
+ }
+);
+
+fetchXHR(
+ "readable-stream-already-consumed.txt",
+ function(xhr) {
+ my_ok(false, "This should not be called!");
+ finish();
+ },
+ function() {
+ my_ok(true, "The exception has been correctly handled!");
+ finish();
+ }
+);
+
+var expectedUncompressedResponse = "";
+for (let i = 0; i < 10; ++i) {
+ expectedUncompressedResponse += "hello";
+}
+expectedUncompressedResponse += "\n";
+
+// ServiceWorker does not intercept, at which point the network request should
+// be correctly decoded.
+fetchXHR("deliver-gzip.sjs", function(xhr) {
+ my_ok(xhr.status == 200, "network gzip load should be successful");
+ my_ok(
+ xhr.responseText == expectedUncompressedResponse,
+ "network gzip load should have synthesized response."
+ );
+ my_ok(
+ xhr.getResponseHeader("Content-Encoding") == "gzip",
+ "network Content-Encoding should be gzip."
+ );
+ my_ok(
+ xhr.getResponseHeader("Content-Length") == "35",
+ "network Content-Length should be of original gzipped file."
+ );
+ finish();
+});
+
+fetchXHR("hello.gz", function(xhr) {
+ my_ok(xhr.status == 200, "gzip load should be successful");
+ my_ok(
+ xhr.responseText == expectedUncompressedResponse,
+ "gzip load should have synthesized response."
+ );
+ my_ok(
+ xhr.getResponseHeader("Content-Encoding") == "gzip",
+ "Content-Encoding should be gzip."
+ );
+ my_ok(
+ xhr.getResponseHeader("Content-Length") == "35",
+ "Content-Length should be of original gzipped file."
+ );
+ finish();
+});
+
+fetchXHR("hello-after-extracting.gz", function(xhr) {
+ my_ok(xhr.status == 200, "gzip load after extracting should be successful");
+ my_ok(
+ xhr.responseText == expectedUncompressedResponse,
+ "gzip load after extracting should have synthesized response."
+ );
+ my_ok(
+ xhr.getResponseHeader("Content-Encoding") == "gzip",
+ "Content-Encoding after extracting should be gzip."
+ );
+ my_ok(
+ xhr.getResponseHeader("Content-Length") == "35",
+ "Content-Length after extracting should be of original gzipped file."
+ );
+ finish();
+});
+
+fetchXHR(corsServerURL + "?status=200&allowOrigin=*", function(xhr) {
+ my_ok(
+ xhr.status == 200,
+ "cross origin load with correct headers should be successful"
+ );
+ my_ok(
+ xhr.getResponseHeader("access-control-allow-origin") == null,
+ "cors headers should be filtered out"
+ );
+ finish();
+});
+
+// Verify origin header is sent properly even when we have a no-intercept SW.
+var uriOrigin = encodeURIComponent(origin);
+fetchXHR(
+ "http://example.org" +
+ corsServerPath +
+ "?ignore&status=200&origin=" +
+ uriOrigin +
+ "&allowOrigin=" +
+ uriOrigin,
+ function(xhr) {
+ my_ok(
+ xhr.status == 200,
+ "cross origin load with correct headers should be successful"
+ );
+ my_ok(
+ xhr.getResponseHeader("access-control-allow-origin") == null,
+ "cors headers should be filtered out"
+ );
+ finish();
+ }
+);
+
+// Verify that XHR is considered CORS tainted even when original URL is same-origin
+// redirected to cross-origin.
+fetchXHR(
+ redirectURL([
+ { server: origin },
+ { server: "http://example.org", allowOrigin: origin },
+ ]),
+ function(xhr) {
+ my_ok(
+ xhr.status == 200,
+ "cross origin load with correct headers should be successful"
+ );
+ my_ok(
+ xhr.getResponseHeader("access-control-allow-origin") == null,
+ "cors headers should be filtered out"
+ );
+ finish();
+ }
+);
+
+// Test that CORS preflight requests cannot be intercepted. Performs a
+// cross-origin XHR that the SW chooses not to intercept. This requires a
+// preflight request, which the SW must not be allowed to intercept.
+fetchXHR(
+ corsServerURL + "?status=200&allowOrigin=*",
+ null,
+ function(xhr) {
+ my_ok(
+ xhr.status == 0,
+ "cross origin load with incorrect headers should be a failure"
+ );
+ finish();
+ },
+ [["X-Unsafe", "unsafe"]]
+);
+
+// Test that CORS preflight requests cannot be intercepted. Performs a
+// cross-origin XHR that the SW chooses to intercept and respond with a
+// cross-origin fetch. This requires a preflight request, which the SW must not
+// be allowed to intercept.
+fetchXHR(
+ "http://example.org" + corsServerPath + "?status=200&allowOrigin=*",
+ null,
+ function(xhr) {
+ my_ok(
+ xhr.status == 0,
+ "cross origin load with incorrect headers should be a failure"
+ );
+ finish();
+ },
+ [["X-Unsafe", "unsafe"]]
+);
+
+// Test that when the page fetches a url the controlling SW forces a redirect to
+// another location. This other location fetch should also be intercepted by
+// the SW.
+fetchXHR("something.txt", function(xhr) {
+ my_ok(xhr.status == 200, "load should be successful");
+ my_ok(
+ xhr.responseText == "something else response body",
+ "load should have something else"
+ );
+ finish();
+});
+
+// Test fetch will internally get it's SkipServiceWorker flag set. The request is
+// made from the SW through fetch(). fetch() fetches a server-side JavaScript
+// file that force a redirect. The redirect location fetch does not go through
+// the SW.
+fetchXHR("redirect_serviceworker.sjs", function(xhr) {
+ my_ok(xhr.status == 200, "load should be successful");
+ my_ok(
+ xhr.responseText == "// empty worker, always succeed!\n",
+ "load should have redirection content"
+ );
+ finish();
+});
+
+fetchXHR(
+ "empty-header",
+ function(xhr) {
+ my_ok(xhr.status == 200, "load should be successful");
+ my_ok(
+ xhr.responseText == "emptyheader",
+ "load should have the expected content"
+ );
+ finish();
+ },
+ null,
+ [["emptyheader", ""]]
+);
+
+expectAsyncResult();
+fetch(
+ "http://example.com/tests/dom/security/test/cors/file_CrossSiteXHR_server.sjs?status=200&allowOrigin=*"
+).then(
+ function(res) {
+ my_ok(res.ok, "Valid CORS request should receive valid response");
+ my_ok(res.type == "cors", "Response type should be CORS");
+ res.text().then(function(body) {
+ my_ok(
+ body === "<res>hello pass</res>\n",
+ "cors response body should match"
+ );
+ finish();
+ });
+ },
+ function(e) {
+ my_ok(false, "CORS Fetch failed");
+ finish();
+ }
+);
+
+expectAsyncResult();
+fetch(
+ "http://example.com/tests/dom/security/test/cors/file_CrossSiteXHR_server.sjs?status=200",
+ { mode: "no-cors" }
+).then(
+ function(res) {
+ my_ok(res.type == "opaque", "Response type should be opaque");
+ my_ok(res.status == 0, "Status should be 0");
+ res.text().then(function(body) {
+ my_ok(body === "", "opaque response body should be empty");
+ finish();
+ });
+ },
+ function(e) {
+ my_ok(false, "no-cors Fetch failed");
+ finish();
+ }
+);
+
+expectAsyncResult();
+fetch("opaque-on-same-origin").then(
+ function(res) {
+ my_ok(
+ false,
+ "intercepted opaque response for non no-cors request should fail."
+ );
+ finish();
+ },
+ function(e) {
+ my_ok(
+ true,
+ "intercepted opaque response for non no-cors request should fail."
+ );
+ finish();
+ }
+);
+
+expectAsyncResult();
+fetch("http://example.com/opaque-no-cors", { mode: "no-cors" }).then(
+ function(res) {
+ my_ok(
+ res.type == "opaque",
+ "intercepted opaque response for no-cors request should have type opaque."
+ );
+ finish();
+ },
+ function(e) {
+ my_ok(
+ false,
+ "intercepted opaque response for no-cors request should pass."
+ );
+ finish();
+ }
+);
+
+expectAsyncResult();
+fetch("http://example.com/cors-for-no-cors", { mode: "no-cors" }).then(
+ function(res) {
+ my_ok(
+ res.type == "cors",
+ "synthesize CORS response should result in outer CORS response"
+ );
+ finish();
+ },
+ function(e) {
+ my_ok(false, "cors-for-no-cors request should not reject");
+ finish();
+ }
+);
+
+function arrayBufferFromString(str) {
+ var arr = new Uint8Array(str.length);
+ for (let i = 0; i < str.length; ++i) {
+ arr[i] = str.charCodeAt(i);
+ }
+ return arr;
+}
+
+expectAsyncResult();
+fetch(new Request("body-simple", { method: "POST", body: "my body" }))
+ .then(function(res) {
+ return res.text();
+ })
+ .then(function(body) {
+ my_ok(
+ body == "my bodymy body",
+ "the body of the intercepted fetch should be visible in the SW"
+ );
+ finish();
+ });
+
+expectAsyncResult();
+fetch(
+ new Request("body-arraybufferview", {
+ method: "POST",
+ body: arrayBufferFromString("my body"),
+ })
+)
+ .then(function(res) {
+ return res.text();
+ })
+ .then(function(body) {
+ my_ok(
+ body == "my bodymy body",
+ "the ArrayBufferView body of the intercepted fetch should be visible in the SW"
+ );
+ finish();
+ });
+
+expectAsyncResult();
+fetch(
+ new Request("body-arraybuffer", {
+ method: "POST",
+ body: arrayBufferFromString("my body").buffer,
+ })
+)
+ .then(function(res) {
+ return res.text();
+ })
+ .then(function(body) {
+ my_ok(
+ body == "my bodymy body",
+ "the ArrayBuffer body of the intercepted fetch should be visible in the SW"
+ );
+ finish();
+ });
+
+expectAsyncResult();
+var usp = new URLSearchParams();
+usp.set("foo", "bar");
+usp.set("baz", "qux");
+fetch(new Request("body-urlsearchparams", { method: "POST", body: usp }))
+ .then(function(res) {
+ return res.text();
+ })
+ .then(function(body) {
+ my_ok(
+ body == "foo=bar&baz=quxfoo=bar&baz=qux",
+ "the URLSearchParams body of the intercepted fetch should be visible in the SW"
+ );
+ finish();
+ });
+
+expectAsyncResult();
+var fd = new FormData();
+fd.set("foo", "bar");
+fd.set("baz", "qux");
+fetch(new Request("body-formdata", { method: "POST", body: fd }))
+ .then(function(res) {
+ return res.text();
+ })
+ .then(function(body) {
+ my_ok(
+ body.indexOf('Content-Disposition: form-data; name="foo"\r\n\r\nbar') <
+ body.indexOf('Content-Disposition: form-data; name="baz"\r\n\r\nqux'),
+ "the FormData body of the intercepted fetch should be visible in the SW"
+ );
+ finish();
+ });
+
+expectAsyncResult();
+fetch(
+ new Request("body-blob", {
+ method: "POST",
+ body: new Blob(new String("my body")),
+ })
+)
+ .then(function(res) {
+ return res.text();
+ })
+ .then(function(body) {
+ my_ok(
+ body == "my bodymy body",
+ "the Blob body of the intercepted fetch should be visible in the SW"
+ );
+ finish();
+ });
+
+expectAsyncResult();
+fetch("interrupt.sjs").then(
+ function(res) {
+ my_ok(true, "interrupted fetch succeeded");
+ res.text().then(
+ function(body) {
+ my_ok(false, "interrupted fetch shouldn't have complete body");
+ finish();
+ },
+ function() {
+ my_ok(true, "interrupted fetch shouldn't have complete body");
+ finish();
+ }
+ );
+ },
+ function(e) {
+ my_ok(false, "interrupted fetch failed");
+ finish();
+ }
+);
+
+["DELETE", "GET", "HEAD", "OPTIONS", "POST", "PUT"].forEach(function(method) {
+ fetchXHRWithMethod("xhr-method-test.txt", method, function(xhr) {
+ my_ok(xhr.status == 200, method + " load should be successful");
+ if (method === "HEAD") {
+ my_ok(
+ xhr.responseText == "",
+ method + "load should not have synthesized response"
+ );
+ } else {
+ my_ok(
+ xhr.responseText == "intercepted " + method,
+ method + " load should have synthesized response"
+ );
+ }
+ finish();
+ });
+});
+
+expectAsyncResult();
+fetch(new Request("empty-header", { headers: { emptyheader: "" } }))
+ .then(function(res) {
+ return res.text();
+ })
+ .then(
+ function(body) {
+ my_ok(
+ body == "emptyheader",
+ "The empty header was observed in the fetch event"
+ );
+ finish();
+ },
+ function(err) {
+ my_ok(false, "A promise was rejected with " + err);
+ finish();
+ }
+ );
+
+expectAsyncResult();
+fetch("fetchevent-extendable")
+ .then(function(res) {
+ return res.text();
+ })
+ .then(
+ function(body) {
+ my_ok(body == "extendable", "FetchEvent inherits from ExtendableEvent");
+ finish();
+ },
+ function(err) {
+ my_ok(false, "A promise was rejected with " + err);
+ finish();
+ }
+ );
+
+expectAsyncResult();
+fetch("fetchevent-request")
+ .then(function(res) {
+ return res.text();
+ })
+ .then(
+ function(body) {
+ my_ok(body == "non-nullable", "FetchEvent.request must be non-nullable");
+ finish();
+ },
+ function(err) {
+ my_ok(false, "A promise was rejected with " + err);
+ finish();
+ }
+ );
diff --git a/dom/serviceworkers/test/fetch/fetch_worker_script.js b/dom/serviceworkers/test/fetch/fetch_worker_script.js
new file mode 100644
index 0000000000..6eb0b18a77
--- /dev/null
+++ b/dom/serviceworkers/test/fetch/fetch_worker_script.js
@@ -0,0 +1,28 @@
+function my_ok(v, msg) {
+ postMessage({ type: "ok", value: v, msg });
+}
+
+function finish() {
+ postMessage("finish");
+}
+
+function expectAsyncResult() {
+ postMessage("expect");
+}
+
+expectAsyncResult();
+try {
+ var success = false;
+ importScripts("nonexistent_imported_script.js");
+} catch (x) {}
+
+my_ok(success, "worker imported script should be intercepted");
+finish();
+
+function check_intercepted_script() {
+ success = true;
+}
+
+importScripts("fetch_tests.js");
+
+finish(); //corresponds to the gExpected increment before creating this worker
diff --git a/dom/serviceworkers/test/fetch/hsts/embedder.html b/dom/serviceworkers/test/fetch/hsts/embedder.html
new file mode 100644
index 0000000000..ad44809042
--- /dev/null
+++ b/dom/serviceworkers/test/fetch/hsts/embedder.html
@@ -0,0 +1,7 @@
+<!DOCTYPE html>
+<script>
+ window.onmessage = function(e) {
+ window.parent.postMessage(e.data, "*");
+ };
+</script>
+<iframe src="http://example.com/tests/dom/serviceworkers/test/fetch/hsts/index.html"></iframe>
diff --git a/dom/serviceworkers/test/fetch/hsts/hsts_test.js b/dom/serviceworkers/test/fetch/hsts/hsts_test.js
new file mode 100644
index 0000000000..f79307f4e4
--- /dev/null
+++ b/dom/serviceworkers/test/fetch/hsts/hsts_test.js
@@ -0,0 +1,11 @@
+self.addEventListener("fetch", function(event) {
+ if (event.request.url.includes("index.html")) {
+ event.respondWith(fetch("realindex.html"));
+ } else if (event.request.url.includes("image-20px.png")) {
+ if (event.request.url.indexOf("https://") == 0) {
+ event.respondWith(fetch("image-40px.png"));
+ } else {
+ event.respondWith(Response.error());
+ }
+ }
+});
diff --git a/dom/serviceworkers/test/fetch/hsts/image-20px.png b/dom/serviceworkers/test/fetch/hsts/image-20px.png
new file mode 100644
index 0000000000..ae6a8a6b88
--- /dev/null
+++ b/dom/serviceworkers/test/fetch/hsts/image-20px.png
Binary files differ
diff --git a/dom/serviceworkers/test/fetch/hsts/image-40px.png b/dom/serviceworkers/test/fetch/hsts/image-40px.png
new file mode 100644
index 0000000000..fe391dc8a2
--- /dev/null
+++ b/dom/serviceworkers/test/fetch/hsts/image-40px.png
Binary files differ
diff --git a/dom/serviceworkers/test/fetch/hsts/image.html b/dom/serviceworkers/test/fetch/hsts/image.html
new file mode 100644
index 0000000000..7036ea954e
--- /dev/null
+++ b/dom/serviceworkers/test/fetch/hsts/image.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<script>
+onload=function(){
+ var img = new Image();
+ img.src = "http://example.com/tests/dom/serviceworkers/test/fetch/hsts/image-20px.png";
+ img.onload = function() {
+ window.parent.postMessage({status: "image", data: img.width}, "*");
+ };
+ img.onerror = function() {
+ window.parent.postMessage({status: "image", data: "error"}, "*");
+ };
+};
+</script>
diff --git a/dom/serviceworkers/test/fetch/hsts/realindex.html b/dom/serviceworkers/test/fetch/hsts/realindex.html
new file mode 100644
index 0000000000..e7d282fe83
--- /dev/null
+++ b/dom/serviceworkers/test/fetch/hsts/realindex.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<script>
+ var securityInfoPresent = !!SpecialPowers.wrap(window).docShell.currentDocumentChannel.securityInfo;
+ window.parent.postMessage({status: "protocol",
+ data: location.protocol,
+ securityInfoPresent},
+ "*");
+</script>
diff --git a/dom/serviceworkers/test/fetch/hsts/register.html b/dom/serviceworkers/test/fetch/hsts/register.html
new file mode 100644
index 0000000000..bcdc146aec
--- /dev/null
+++ b/dom/serviceworkers/test/fetch/hsts/register.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<script>
+ function ok(v, msg) {
+ window.parent.postMessage({status: "ok", result: !!v, message: msg}, "*");
+ }
+
+ function done(reg) {
+ ok(reg.active, "The active worker should be available.");
+ window.parent.postMessage({status: "registrationdone"}, "*");
+ }
+
+ navigator.serviceWorker.ready.then(done);
+ navigator.serviceWorker.register("hsts_test.js", {scope: "."});
+</script>
diff --git a/dom/serviceworkers/test/fetch/hsts/register.html^headers^ b/dom/serviceworkers/test/fetch/hsts/register.html^headers^
new file mode 100644
index 0000000000..a46bf65bd9
--- /dev/null
+++ b/dom/serviceworkers/test/fetch/hsts/register.html^headers^
@@ -0,0 +1,2 @@
+Cache-Control: no-cache
+Strict-Transport-Security: max-age=60
diff --git a/dom/serviceworkers/test/fetch/hsts/unregister.html b/dom/serviceworkers/test/fetch/hsts/unregister.html
new file mode 100644
index 0000000000..1f13508fa7
--- /dev/null
+++ b/dom/serviceworkers/test/fetch/hsts/unregister.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<script>
+ navigator.serviceWorker.getRegistration(".").then(function(registration) {
+ registration.unregister().then(function(success) {
+ if (success) {
+ window.parent.postMessage({status: "unregistrationdone"}, "*");
+ }
+ }, function(e) {
+ dump("Unregistering the SW failed with " + e + "\n");
+ });
+ });
+</script>
diff --git a/dom/serviceworkers/test/fetch/https/clonedresponse/https_test.js b/dom/serviceworkers/test/fetch/https/clonedresponse/https_test.js
new file mode 100644
index 0000000000..0490017332
--- /dev/null
+++ b/dom/serviceworkers/test/fetch/https/clonedresponse/https_test.js
@@ -0,0 +1,19 @@
+self.addEventListener("install", function(event) {
+ event.waitUntil(
+ caches.open("cache").then(function(cache) {
+ return cache.add("index.html");
+ })
+ );
+});
+
+self.addEventListener("fetch", function(event) {
+ if (event.request.url.includes("index.html")) {
+ event.respondWith(
+ new Promise(function(resolve, reject) {
+ caches.match(event.request).then(function(response) {
+ resolve(response.clone());
+ });
+ })
+ );
+ }
+});
diff --git a/dom/serviceworkers/test/fetch/https/clonedresponse/index.html b/dom/serviceworkers/test/fetch/https/clonedresponse/index.html
new file mode 100644
index 0000000000..a435548443
--- /dev/null
+++ b/dom/serviceworkers/test/fetch/https/clonedresponse/index.html
@@ -0,0 +1,4 @@
+<!DOCTYPE html>
+<script>
+ window.parent.postMessage({status: "done"}, "*");
+</script>
diff --git a/dom/serviceworkers/test/fetch/https/clonedresponse/register.html b/dom/serviceworkers/test/fetch/https/clonedresponse/register.html
new file mode 100644
index 0000000000..41774f70d1
--- /dev/null
+++ b/dom/serviceworkers/test/fetch/https/clonedresponse/register.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<script>
+ function ok(v, msg) {
+ window.parent.postMessage({status: "ok", result: !!v, message: msg}, "*");
+ }
+
+ function done(reg) {
+ ok(reg.active, "The active worker should be available.");
+ window.parent.postMessage({status: "registrationdone"}, "*");
+ }
+
+ navigator.serviceWorker.ready.then(done);
+ navigator.serviceWorker.register("https_test.js", {scope: "."});
+</script>
diff --git a/dom/serviceworkers/test/fetch/https/clonedresponse/unregister.html b/dom/serviceworkers/test/fetch/https/clonedresponse/unregister.html
new file mode 100644
index 0000000000..1f13508fa7
--- /dev/null
+++ b/dom/serviceworkers/test/fetch/https/clonedresponse/unregister.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<script>
+ navigator.serviceWorker.getRegistration(".").then(function(registration) {
+ registration.unregister().then(function(success) {
+ if (success) {
+ window.parent.postMessage({status: "unregistrationdone"}, "*");
+ }
+ }, function(e) {
+ dump("Unregistering the SW failed with " + e + "\n");
+ });
+ });
+</script>
diff --git a/dom/serviceworkers/test/fetch/https/https_test.js b/dom/serviceworkers/test/fetch/https/https_test.js
new file mode 100644
index 0000000000..ad144d58ab
--- /dev/null
+++ b/dom/serviceworkers/test/fetch/https/https_test.js
@@ -0,0 +1,31 @@
+self.addEventListener("install", function(event) {
+ event.waitUntil(
+ caches.open("cache").then(function(cache) {
+ var synth = new Response(
+ '<!DOCTYPE html><script>window.parent.postMessage({status: "done-synth-sw"}, "*");</script>',
+ { headers: { "Content-Type": "text/html" } }
+ );
+ return Promise.all([
+ cache.add("index.html"),
+ cache.put("synth-sw.html", synth),
+ ]);
+ })
+ );
+});
+
+self.addEventListener("fetch", function(event) {
+ if (event.request.url.includes("index.html")) {
+ event.respondWith(caches.match(event.request));
+ } else if (event.request.url.includes("synth-sw.html")) {
+ event.respondWith(caches.match(event.request));
+ } else if (event.request.url.includes("synth-window.html")) {
+ event.respondWith(caches.match(event.request));
+ } else if (event.request.url.includes("synth.html")) {
+ event.respondWith(
+ new Response(
+ '<!DOCTYPE html><script>window.parent.postMessage({status: "done-synth"}, "*");</script>',
+ { headers: { "Content-Type": "text/html" } }
+ )
+ );
+ }
+});
diff --git a/dom/serviceworkers/test/fetch/https/index.html b/dom/serviceworkers/test/fetch/https/index.html
new file mode 100644
index 0000000000..a435548443
--- /dev/null
+++ b/dom/serviceworkers/test/fetch/https/index.html
@@ -0,0 +1,4 @@
+<!DOCTYPE html>
+<script>
+ window.parent.postMessage({status: "done"}, "*");
+</script>
diff --git a/dom/serviceworkers/test/fetch/https/register.html b/dom/serviceworkers/test/fetch/https/register.html
new file mode 100644
index 0000000000..fa666fe957
--- /dev/null
+++ b/dom/serviceworkers/test/fetch/https/register.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<script>
+ function ok(v, msg) {
+ window.parent.postMessage({status: "ok", result: !!v, message: msg}, "*");
+ }
+
+ function done(reg) {
+ ok(reg.active, "The active worker should be available.");
+ window.parent.postMessage({status: "registrationdone"}, "*");
+ }
+
+ navigator.serviceWorker.ready.then(reg => {
+ return window.caches.open("cache").then(function(cache) {
+ var synth = new Response('<!DOCTYPE html><script>window.parent.postMessage({status: "done-synth-window"}, "*");</scri' + 'pt>',
+ {headers:{"Content-Type": "text/html"}});
+ return cache.put('synth-window.html', synth).then(_ => done(reg));
+ });
+ });
+ navigator.serviceWorker.register("https_test.js", {scope: "."});
+</script>
diff --git a/dom/serviceworkers/test/fetch/https/unregister.html b/dom/serviceworkers/test/fetch/https/unregister.html
new file mode 100644
index 0000000000..1f13508fa7
--- /dev/null
+++ b/dom/serviceworkers/test/fetch/https/unregister.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<script>
+ navigator.serviceWorker.getRegistration(".").then(function(registration) {
+ registration.unregister().then(function(success) {
+ if (success) {
+ window.parent.postMessage({status: "unregistrationdone"}, "*");
+ }
+ }, function(e) {
+ dump("Unregistering the SW failed with " + e + "\n");
+ });
+ });
+</script>
diff --git a/dom/serviceworkers/test/fetch/imagecache-maxage/image-20px.png b/dom/serviceworkers/test/fetch/imagecache-maxage/image-20px.png
new file mode 100644
index 0000000000..ae6a8a6b88
--- /dev/null
+++ b/dom/serviceworkers/test/fetch/imagecache-maxage/image-20px.png
Binary files differ
diff --git a/dom/serviceworkers/test/fetch/imagecache-maxage/image-40px.png b/dom/serviceworkers/test/fetch/imagecache-maxage/image-40px.png
new file mode 100644
index 0000000000..fe391dc8a2
--- /dev/null
+++ b/dom/serviceworkers/test/fetch/imagecache-maxage/image-40px.png
Binary files differ
diff --git a/dom/serviceworkers/test/fetch/imagecache-maxage/index.html b/dom/serviceworkers/test/fetch/imagecache-maxage/index.html
new file mode 100644
index 0000000000..0d4c52eedd
--- /dev/null
+++ b/dom/serviceworkers/test/fetch/imagecache-maxage/index.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<script>
+var width, url, width2, url2;
+function maybeReport() {
+ if (width !== undefined && url !== undefined &&
+ width2 !== undefined && url2 !== undefined) {
+ window.parent.postMessage({status: "result",
+ width,
+ width2,
+ url,
+ url2}, "*");
+ }
+}
+onload = function() {
+ width = document.querySelector("img").width;
+ width2 = document.querySelector("img").width;
+ maybeReport();
+};
+navigator.serviceWorker.onmessage = function(event) {
+ if (event.data.suffix == "2") {
+ url2 = event.data.url;
+ } else {
+ url = event.data.url;
+ }
+ maybeReport();
+};
+</script>
+<img src="image.png">
+<img src="image2.png">
diff --git a/dom/serviceworkers/test/fetch/imagecache-maxage/maxage_test.js b/dom/serviceworkers/test/fetch/imagecache-maxage/maxage_test.js
new file mode 100644
index 0000000000..62707566ea
--- /dev/null
+++ b/dom/serviceworkers/test/fetch/imagecache-maxage/maxage_test.js
@@ -0,0 +1,45 @@
+function synthesizeImage(suffix) {
+ // Serve image-20px for the first page, and image-40px for the second page.
+ return clients
+ .matchAll()
+ .then(clients => {
+ var url = "image-20px.png";
+ clients.forEach(client => {
+ if (client.url.indexOf("?new") > 0) {
+ url = "image-40px.png";
+ }
+ client.postMessage({ suffix, url });
+ });
+ return fetch(url);
+ })
+ .then(response => {
+ return response.arrayBuffer();
+ })
+ .then(ab => {
+ var headers;
+ if (suffix == "") {
+ headers = {
+ "Content-Type": "image/png",
+ Date: "Tue, 1 Jan 1990 01:02:03 GMT",
+ "Cache-Control": "max-age=1",
+ };
+ } else {
+ headers = {
+ "Content-Type": "image/png",
+ "Cache-Control": "no-cache",
+ };
+ }
+ return new Response(ab, {
+ status: 200,
+ headers,
+ });
+ });
+}
+
+self.addEventListener("fetch", function(event) {
+ if (event.request.url.includes("image.png")) {
+ event.respondWith(synthesizeImage(""));
+ } else if (event.request.url.includes("image2.png")) {
+ event.respondWith(synthesizeImage("2"));
+ }
+});
diff --git a/dom/serviceworkers/test/fetch/imagecache-maxage/register.html b/dom/serviceworkers/test/fetch/imagecache-maxage/register.html
new file mode 100644
index 0000000000..af4dde2e29
--- /dev/null
+++ b/dom/serviceworkers/test/fetch/imagecache-maxage/register.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<script>
+ function ok(v, msg) {
+ window.parent.postMessage({status: "ok", result: !!v, message: msg}, "*");
+ }
+
+ function done(reg) {
+ ok(reg.active, "The active worker should be available.");
+ window.parent.postMessage({status: "registrationdone"}, "*");
+ }
+
+ navigator.serviceWorker.ready.then(done);
+ navigator.serviceWorker.register("maxage_test.js", {scope: "."});
+</script>
diff --git a/dom/serviceworkers/test/fetch/imagecache-maxage/unregister.html b/dom/serviceworkers/test/fetch/imagecache-maxage/unregister.html
new file mode 100644
index 0000000000..1f13508fa7
--- /dev/null
+++ b/dom/serviceworkers/test/fetch/imagecache-maxage/unregister.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<script>
+ navigator.serviceWorker.getRegistration(".").then(function(registration) {
+ registration.unregister().then(function(success) {
+ if (success) {
+ window.parent.postMessage({status: "unregistrationdone"}, "*");
+ }
+ }, function(e) {
+ dump("Unregistering the SW failed with " + e + "\n");
+ });
+ });
+</script>
diff --git a/dom/serviceworkers/test/fetch/imagecache/image-20px.png b/dom/serviceworkers/test/fetch/imagecache/image-20px.png
new file mode 100644
index 0000000000..ae6a8a6b88
--- /dev/null
+++ b/dom/serviceworkers/test/fetch/imagecache/image-20px.png
Binary files differ
diff --git a/dom/serviceworkers/test/fetch/imagecache/image-40px.png b/dom/serviceworkers/test/fetch/imagecache/image-40px.png
new file mode 100644
index 0000000000..fe391dc8a2
--- /dev/null
+++ b/dom/serviceworkers/test/fetch/imagecache/image-40px.png
Binary files differ
diff --git a/dom/serviceworkers/test/fetch/imagecache/imagecache_test.js b/dom/serviceworkers/test/fetch/imagecache/imagecache_test.js
new file mode 100644
index 0000000000..e1f2af736b
--- /dev/null
+++ b/dom/serviceworkers/test/fetch/imagecache/imagecache_test.js
@@ -0,0 +1,15 @@
+function synthesizeImage() {
+ return clients.matchAll().then(clients => {
+ var url = "image-40px.png";
+ clients.forEach(client => {
+ client.postMessage(url);
+ });
+ return fetch(url);
+ });
+}
+
+self.addEventListener("fetch", function(event) {
+ if (event.request.url.includes("image-20px.png")) {
+ event.respondWith(synthesizeImage());
+ }
+});
diff --git a/dom/serviceworkers/test/fetch/imagecache/index.html b/dom/serviceworkers/test/fetch/imagecache/index.html
new file mode 100644
index 0000000000..f634f68bb7
--- /dev/null
+++ b/dom/serviceworkers/test/fetch/imagecache/index.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<script>
+var width, url;
+function maybeReport() {
+ if (width !== undefined && url !== undefined) {
+ window.parent.postMessage({status: "result",
+ width,
+ url}, "*");
+ }
+}
+onload = function() {
+ width = document.querySelector("img").width;
+ maybeReport();
+};
+navigator.serviceWorker.onmessage = function(event) {
+ url = event.data;
+ maybeReport();
+};
+</script>
+<img src="image-20px.png">
diff --git a/dom/serviceworkers/test/fetch/imagecache/postmortem.html b/dom/serviceworkers/test/fetch/imagecache/postmortem.html
new file mode 100644
index 0000000000..53356cd02c
--- /dev/null
+++ b/dom/serviceworkers/test/fetch/imagecache/postmortem.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<script>
+onload = function() {
+ var width = document.querySelector("img").width;
+ window.parent.postMessage({status: "postmortem",
+ width}, "*");
+};
+</script>
+<img src="image-20px.png">
diff --git a/dom/serviceworkers/test/fetch/imagecache/register.html b/dom/serviceworkers/test/fetch/imagecache/register.html
new file mode 100644
index 0000000000..f6d1eb382f
--- /dev/null
+++ b/dom/serviceworkers/test/fetch/imagecache/register.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<!-- Load the image here to put it in the image cache -->
+<img src="image-20px.png">
+<script>
+ function ok(v, msg) {
+ window.parent.postMessage({status: "ok", result: !!v, message: msg}, "*");
+ }
+
+ function done(reg) {
+ ok(reg.active, "The active worker should be available.");
+ window.parent.postMessage({status: "registrationdone"}, "*");
+ }
+
+ navigator.serviceWorker.ready.then(done);
+ navigator.serviceWorker.register("imagecache_test.js", {scope: "."});
+</script>
diff --git a/dom/serviceworkers/test/fetch/imagecache/unregister.html b/dom/serviceworkers/test/fetch/imagecache/unregister.html
new file mode 100644
index 0000000000..1f13508fa7
--- /dev/null
+++ b/dom/serviceworkers/test/fetch/imagecache/unregister.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<script>
+ navigator.serviceWorker.getRegistration(".").then(function(registration) {
+ registration.unregister().then(function(success) {
+ if (success) {
+ window.parent.postMessage({status: "unregistrationdone"}, "*");
+ }
+ }, function(e) {
+ dump("Unregistering the SW failed with " + e + "\n");
+ });
+ });
+</script>
diff --git a/dom/serviceworkers/test/fetch/importscript-mixedcontent/https_test.js b/dom/serviceworkers/test/fetch/importscript-mixedcontent/https_test.js
new file mode 100644
index 0000000000..593171bafe
--- /dev/null
+++ b/dom/serviceworkers/test/fetch/importscript-mixedcontent/https_test.js
@@ -0,0 +1,31 @@
+function sendResponseToParent(response) {
+ return `
+ <!DOCTYPE html>
+ <script>
+ window.parent.postMessage({status: "done", data: "${response}"}, "*");
+ </script>
+ `;
+}
+
+self.addEventListener("fetch", function(event) {
+ if (event.request.url.includes("index.html")) {
+ var response = "good";
+ try {
+ importScripts("http://example.org/tests/dom/workers/test/foreign.js");
+ } catch (e) {
+ dump("Got error " + e + " when importing the script\n");
+ }
+ if (response === "good") {
+ try {
+ importScripts("/tests/dom/workers/test/redirect_to_foreign.sjs");
+ } catch (e) {
+ dump("Got error " + e + " when importing the script\n");
+ }
+ }
+ event.respondWith(
+ new Response(sendResponseToParent(response), {
+ headers: { "Content-Type": "text/html" },
+ })
+ );
+ }
+});
diff --git a/dom/serviceworkers/test/fetch/importscript-mixedcontent/register.html b/dom/serviceworkers/test/fetch/importscript-mixedcontent/register.html
new file mode 100644
index 0000000000..41774f70d1
--- /dev/null
+++ b/dom/serviceworkers/test/fetch/importscript-mixedcontent/register.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<script>
+ function ok(v, msg) {
+ window.parent.postMessage({status: "ok", result: !!v, message: msg}, "*");
+ }
+
+ function done(reg) {
+ ok(reg.active, "The active worker should be available.");
+ window.parent.postMessage({status: "registrationdone"}, "*");
+ }
+
+ navigator.serviceWorker.ready.then(done);
+ navigator.serviceWorker.register("https_test.js", {scope: "."});
+</script>
diff --git a/dom/serviceworkers/test/fetch/importscript-mixedcontent/unregister.html b/dom/serviceworkers/test/fetch/importscript-mixedcontent/unregister.html
new file mode 100644
index 0000000000..1f13508fa7
--- /dev/null
+++ b/dom/serviceworkers/test/fetch/importscript-mixedcontent/unregister.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<script>
+ navigator.serviceWorker.getRegistration(".").then(function(registration) {
+ registration.unregister().then(function(success) {
+ if (success) {
+ window.parent.postMessage({status: "unregistrationdone"}, "*");
+ }
+ }, function(e) {
+ dump("Unregistering the SW failed with " + e + "\n");
+ });
+ });
+</script>
diff --git a/dom/serviceworkers/test/fetch/index.html b/dom/serviceworkers/test/fetch/index.html
new file mode 100644
index 0000000000..693810c6fc
--- /dev/null
+++ b/dom/serviceworkers/test/fetch/index.html
@@ -0,0 +1,191 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 94048 - test install event.</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<div id="style-test" style="background-color: white"></div>
+<pre id="test"></pre>
+<script class="testbody" type="text/javascript">
+ function my_ok(result, msg) {
+ window.opener.postMessage({status: "ok", result, message: msg}, "*");
+ }
+
+ function check_intercepted_script() {
+ document.getElementById('intercepted-script').test_result =
+ document.currentScript == document.getElementById('intercepted-script');
+ }
+
+ function fetchXHR(name, onload, onerror, headers) {
+ gExpected++;
+
+ onload = onload || function() {
+ my_ok(false, "load should not complete successfully");
+ finish();
+ };
+ onerror = onerror || function() {
+ my_ok(false, "load should be intercepted successfully");
+ finish();
+ };
+
+ var x = new XMLHttpRequest();
+ x.open('GET', name, true);
+ x.onload = function() { onload(x) };
+ x.onerror = function() { onerror(x) };
+ headers = headers || [];
+ headers.forEach(function(header) {
+ x.setRequestHeader(header[0], header[1]);
+ });
+ x.send();
+ }
+
+ var gExpected = 0;
+ var gEncountered = 0;
+ function finish() {
+ gEncountered++;
+ if (gEncountered == gExpected) {
+ window.opener.postMessage({status: "done"}, "*");
+ }
+ }
+
+ function test_onload(creator, complete) {
+ gExpected++;
+ var elem = creator();
+ elem.onload = function() {
+ complete.call(elem);
+ finish();
+ };
+ elem.onerror = function() {
+ my_ok(false, elem.tagName + " load should complete successfully");
+ finish();
+ };
+ document.body.appendChild(elem);
+ }
+
+ function expectAsyncResult() {
+ gExpected++;
+ }
+
+ my_ok(navigator.serviceWorker.controller != null, "should be controlled");
+</script>
+<script src="fetch_tests.js"></script>
+<script>
+ test_onload(function() {
+ var elem = document.createElement('img');
+ elem.src = "nonexistent_image.gifs";
+ elem.id = 'intercepted-img';
+ return elem;
+ }, function() {
+ my_ok(this.complete, "image should be complete");
+ my_ok(this.naturalWidth == 1 && this.naturalHeight == 1, "image should be 1x1 gif");
+ });
+
+ test_onload(function() {
+ var elem = document.createElement('script');
+ elem.id = 'intercepted-script';
+ elem.src = "nonexistent_script.js";
+ return elem;
+ }, function() {
+ my_ok(this.test_result, "script load should be intercepted");
+ });
+
+ test_onload(function() {
+ var elem = document.createElement('link');
+ elem.href = "nonexistent_stylesheet.css";
+ elem.rel = "stylesheet";
+ return elem;
+ }, function() {
+ var styled = document.getElementById('style-test');
+ my_ok(window.getComputedStyle(styled).backgroundColor == 'rgb(0, 0, 0)',
+ "stylesheet load should be intercepted");
+ });
+
+ test_onload(function() {
+ var elem = document.createElement('iframe');
+ elem.id = 'intercepted-iframe';
+ elem.src = "nonexistent_page.html";
+ return elem;
+ }, function() {
+ my_ok(this.test_result, "iframe load should be intercepted");
+ });
+
+ test_onload(function() {
+ var elem = document.createElement('iframe');
+ elem.id = 'intercepted-iframe-2';
+ elem.src = "navigate.html";
+ return elem;
+ }, function() {
+ my_ok(this.test_result, "iframe should successfully load");
+ });
+
+ gExpected++;
+ var xhr = new XMLHttpRequest();
+ xhr.addEventListener("load", function(evt) {
+ my_ok(evt.target.responseXML === null, "Load synthetic cross origin XML Document should be allowed");
+ finish();
+ });
+ xhr.responseType = "document";
+ xhr.open("GET", "load_cross_origin_xml_document_synthetic.xml");
+ xhr.send();
+
+ gExpected++;
+ fetch(
+ "load_cross_origin_xml_document_cors.xml",
+ {mode: "same-origin"}
+ ).then(function(response) {
+ // issue: https://github.com/whatwg/fetch/issues/629
+ my_ok(false, "Load CORS cross origin XML Document should not be allowed");
+ finish();
+ }, function(error) {
+ my_ok(true, "Load CORS cross origin XML Document should not be allowed");
+ finish();
+ });
+
+ gExpected++;
+ fetch(
+ "load_cross_origin_xml_document_opaque.xml",
+ {mode: "same-origin"}
+ ).then(function(response) {
+ my_ok(false, "Load opaque cross origin XML Document should not be allowed");
+ finish();
+ }, function(error) {
+ my_ok(true, "Load opaque cross origin XML Document should not be allowed");
+ finish();
+ });
+
+ gExpected++;
+ var worker = new Worker('nonexistent_worker_script.js');
+ worker.onmessage = function(e) {
+ my_ok(e.data == "worker-intercept-success", "worker load intercepted");
+ finish();
+ };
+ worker.onerror = function() {
+ my_ok(false, "worker load should be intercepted");
+ };
+
+ gExpected++;
+ var worker = new Worker('fetch_worker_script.js');
+ worker.onmessage = function(e) {
+ if (e.data == "finish") {
+ finish();
+ } else if (e.data == "expect") {
+ gExpected++;
+ } else if (e.data.type == "ok") {
+ my_ok(e.data.value, "Fetch test on worker: " + e.data.msg);
+ }
+ };
+ worker.onerror = function() {
+ my_ok(false, "worker should not cause any errors");
+ };
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/fetch/interrupt.sjs b/dom/serviceworkers/test/fetch/interrupt.sjs
new file mode 100644
index 0000000000..6e5deeb832
--- /dev/null
+++ b/dom/serviceworkers/test/fetch/interrupt.sjs
@@ -0,0 +1,20 @@
+function handleRequest(request, response) {
+ var body = "a";
+ for (var i = 0; i < 20; i++) {
+ body += body;
+ }
+
+ response.seizePower();
+ response.write("HTTP/1.1 200 OK\r\n");
+ var count = 10;
+ response.write("Content-Length: " + body.length * count + "\r\n");
+ response.write("Content-Type: text/plain; charset=utf-8\r\n");
+ response.write("Cache-Control: no-cache, must-revalidate\r\n");
+ response.write("\r\n");
+
+ for (var i = 0; i < count; i++) {
+ response.write(body);
+ }
+
+ throw Components.Exception("", Components.results.NS_BINDING_ABORTED);
+}
diff --git a/dom/serviceworkers/test/fetch/origin/https/index-https.sjs b/dom/serviceworkers/test/fetch/origin/https/index-https.sjs
new file mode 100644
index 0000000000..5250467ec7
--- /dev/null
+++ b/dom/serviceworkers/test/fetch/origin/https/index-https.sjs
@@ -0,0 +1,8 @@
+function handleRequest(request, response) {
+ response.setStatusLine(null, 308, "Permanent Redirect");
+ response.setHeader(
+ "Location",
+ "https://example.org/tests/dom/serviceworkers/test/fetch/origin/https/realindex.html",
+ false
+ );
+}
diff --git a/dom/serviceworkers/test/fetch/origin/https/origin_test.js b/dom/serviceworkers/test/fetch/origin/https/origin_test.js
new file mode 100644
index 0000000000..5f76befc00
--- /dev/null
+++ b/dom/serviceworkers/test/fetch/origin/https/origin_test.js
@@ -0,0 +1,29 @@
+var prefix = "/tests/dom/serviceworkers/test/fetch/origin/https/";
+
+function addOpaqueRedirect(cache, file) {
+ return fetch(new Request(prefix + file, { redirect: "manual" })).then(
+ function(response) {
+ return cache.put(prefix + file, response);
+ }
+ );
+}
+
+self.addEventListener("install", function(event) {
+ event.waitUntil(
+ self.caches.open("origin-cache").then(c => {
+ return addOpaqueRedirect(c, "index-https.sjs");
+ })
+ );
+});
+
+self.addEventListener("fetch", function(event) {
+ if (event.request.url.includes("index-cached-https.sjs")) {
+ event.respondWith(
+ self.caches.open("origin-cache").then(c => {
+ return c.match(prefix + "index-https.sjs");
+ })
+ );
+ } else {
+ event.respondWith(fetch(event.request));
+ }
+});
diff --git a/dom/serviceworkers/test/fetch/origin/https/realindex.html b/dom/serviceworkers/test/fetch/origin/https/realindex.html
new file mode 100644
index 0000000000..87f3489455
--- /dev/null
+++ b/dom/serviceworkers/test/fetch/origin/https/realindex.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<script>
+ window.opener.postMessage({status: "domain", data: document.domain}, "*");
+ window.opener.postMessage({status: "origin", data: location.origin}, "*");
+ window.opener.postMessage({status: "done"}, "*");
+</script>
diff --git a/dom/serviceworkers/test/fetch/origin/https/realindex.html^headers^ b/dom/serviceworkers/test/fetch/origin/https/realindex.html^headers^
new file mode 100644
index 0000000000..5ed82fd065
--- /dev/null
+++ b/dom/serviceworkers/test/fetch/origin/https/realindex.html^headers^
@@ -0,0 +1 @@
+Access-Control-Allow-Origin: https://example.com
diff --git a/dom/serviceworkers/test/fetch/origin/https/register.html b/dom/serviceworkers/test/fetch/origin/https/register.html
new file mode 100644
index 0000000000..2e99adba53
--- /dev/null
+++ b/dom/serviceworkers/test/fetch/origin/https/register.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<script>
+ function ok(v, msg) {
+ window.parent.postMessage({status: "ok", result: !!v, message: msg}, "*");
+ }
+
+ function done(reg) {
+ ok(reg.active, "The active worker should be available.");
+ window.parent.postMessage({status: "registrationdone"}, "*");
+ }
+
+ navigator.serviceWorker.ready.then(done);
+ navigator.serviceWorker.register("origin_test.js", {scope: "."});
+</script>
diff --git a/dom/serviceworkers/test/fetch/origin/https/unregister.html b/dom/serviceworkers/test/fetch/origin/https/unregister.html
new file mode 100644
index 0000000000..1f13508fa7
--- /dev/null
+++ b/dom/serviceworkers/test/fetch/origin/https/unregister.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<script>
+ navigator.serviceWorker.getRegistration(".").then(function(registration) {
+ registration.unregister().then(function(success) {
+ if (success) {
+ window.parent.postMessage({status: "unregistrationdone"}, "*");
+ }
+ }, function(e) {
+ dump("Unregistering the SW failed with " + e + "\n");
+ });
+ });
+</script>
diff --git a/dom/serviceworkers/test/fetch/origin/index-to-https.sjs b/dom/serviceworkers/test/fetch/origin/index-to-https.sjs
new file mode 100644
index 0000000000..2505c03686
--- /dev/null
+++ b/dom/serviceworkers/test/fetch/origin/index-to-https.sjs
@@ -0,0 +1,8 @@
+function handleRequest(request, response) {
+ response.setStatusLine(null, 308, "Permanent Redirect");
+ response.setHeader(
+ "Location",
+ "https://example.org/tests/dom/serviceworkers/test/fetch/origin/realindex.html",
+ false
+ );
+}
diff --git a/dom/serviceworkers/test/fetch/origin/index.sjs b/dom/serviceworkers/test/fetch/origin/index.sjs
new file mode 100644
index 0000000000..ca11827792
--- /dev/null
+++ b/dom/serviceworkers/test/fetch/origin/index.sjs
@@ -0,0 +1,8 @@
+function handleRequest(request, response) {
+ response.setStatusLine(null, 308, "Permanent Redirect");
+ response.setHeader(
+ "Location",
+ "http://example.org/tests/dom/serviceworkers/test/fetch/origin/realindex.html",
+ false
+ );
+}
diff --git a/dom/serviceworkers/test/fetch/origin/origin_test.js b/dom/serviceworkers/test/fetch/origin/origin_test.js
new file mode 100644
index 0000000000..005c3b8c39
--- /dev/null
+++ b/dom/serviceworkers/test/fetch/origin/origin_test.js
@@ -0,0 +1,38 @@
+var prefix = "/tests/dom/serviceworkers/test/fetch/origin/";
+
+function addOpaqueRedirect(cache, file) {
+ return fetch(new Request(prefix + file, { redirect: "manual" })).then(
+ function(response) {
+ return cache.put(prefix + file, response);
+ }
+ );
+}
+
+self.addEventListener("install", function(event) {
+ event.waitUntil(
+ self.caches.open("origin-cache").then(c => {
+ return Promise.all([
+ addOpaqueRedirect(c, "index.sjs"),
+ addOpaqueRedirect(c, "index-to-https.sjs"),
+ ]);
+ })
+ );
+});
+
+self.addEventListener("fetch", function(event) {
+ if (event.request.url.includes("index-cached.sjs")) {
+ event.respondWith(
+ self.caches.open("origin-cache").then(c => {
+ return c.match(prefix + "index.sjs");
+ })
+ );
+ } else if (event.request.url.includes("index-to-https-cached.sjs")) {
+ event.respondWith(
+ self.caches.open("origin-cache").then(c => {
+ return c.match(prefix + "index-to-https.sjs");
+ })
+ );
+ } else {
+ event.respondWith(fetch(event.request));
+ }
+});
diff --git a/dom/serviceworkers/test/fetch/origin/realindex.html b/dom/serviceworkers/test/fetch/origin/realindex.html
new file mode 100644
index 0000000000..87f3489455
--- /dev/null
+++ b/dom/serviceworkers/test/fetch/origin/realindex.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<script>
+ window.opener.postMessage({status: "domain", data: document.domain}, "*");
+ window.opener.postMessage({status: "origin", data: location.origin}, "*");
+ window.opener.postMessage({status: "done"}, "*");
+</script>
diff --git a/dom/serviceworkers/test/fetch/origin/realindex.html^headers^ b/dom/serviceworkers/test/fetch/origin/realindex.html^headers^
new file mode 100644
index 0000000000..3a6a85d894
--- /dev/null
+++ b/dom/serviceworkers/test/fetch/origin/realindex.html^headers^
@@ -0,0 +1 @@
+Access-Control-Allow-Origin: http://mochi.test:8888
diff --git a/dom/serviceworkers/test/fetch/origin/register.html b/dom/serviceworkers/test/fetch/origin/register.html
new file mode 100644
index 0000000000..2e99adba53
--- /dev/null
+++ b/dom/serviceworkers/test/fetch/origin/register.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<script>
+ function ok(v, msg) {
+ window.parent.postMessage({status: "ok", result: !!v, message: msg}, "*");
+ }
+
+ function done(reg) {
+ ok(reg.active, "The active worker should be available.");
+ window.parent.postMessage({status: "registrationdone"}, "*");
+ }
+
+ navigator.serviceWorker.ready.then(done);
+ navigator.serviceWorker.register("origin_test.js", {scope: "."});
+</script>
diff --git a/dom/serviceworkers/test/fetch/origin/unregister.html b/dom/serviceworkers/test/fetch/origin/unregister.html
new file mode 100644
index 0000000000..1f13508fa7
--- /dev/null
+++ b/dom/serviceworkers/test/fetch/origin/unregister.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<script>
+ navigator.serviceWorker.getRegistration(".").then(function(registration) {
+ registration.unregister().then(function(success) {
+ if (success) {
+ window.parent.postMessage({status: "unregistrationdone"}, "*");
+ }
+ }, function(e) {
+ dump("Unregistering the SW failed with " + e + "\n");
+ });
+ });
+</script>
diff --git a/dom/serviceworkers/test/fetch/plugin/plugins.html b/dom/serviceworkers/test/fetch/plugin/plugins.html
new file mode 100644
index 0000000000..b268f6d99e
--- /dev/null
+++ b/dom/serviceworkers/test/fetch/plugin/plugins.html
@@ -0,0 +1,43 @@
+<!DOCTYPE html>
+<script>
+ var obj, embed;
+
+ function ok(v, msg) {
+ window.opener.postMessage({status: "ok", result: !!v, message: msg}, "*");
+ }
+
+ function finish() {
+ document.documentElement.removeChild(obj);
+ document.documentElement.removeChild(embed);
+ window.opener.postMessage({status: "done"}, "*");
+ }
+
+ function test_object() {
+ obj = document.createElement("object");
+ obj.setAttribute('data', "object");
+ document.documentElement.appendChild(obj);
+ }
+
+ function test_embed() {
+ embed = document.createElement("embed");
+ embed.setAttribute('src', "embed");
+ document.documentElement.appendChild(embed);
+ }
+
+ navigator.serviceWorker.addEventListener("message", function onMessage(e) {
+ if (e.data.destination === "object") {
+ ok(false, "<object> should not be intercepted");
+ } else if (e.data.destination === "embed") {
+ ok(false, "<embed> should not be intercepted");
+ } else if (e.data.destination === "" && e.data.resource === "foo.txt") {
+ navigator.serviceWorker.removeEventListener("message", onMessage);
+ finish();
+ }
+ });
+
+ test_object();
+ test_embed();
+ // SW will definitely intercept fetch API, use this to see if plugins are
+ // intercepted before fetch().
+ fetch("foo.txt");
+</script>
diff --git a/dom/serviceworkers/test/fetch/plugin/worker.js b/dom/serviceworkers/test/fetch/plugin/worker.js
new file mode 100644
index 0000000000..da3c1ba54a
--- /dev/null
+++ b/dom/serviceworkers/test/fetch/plugin/worker.js
@@ -0,0 +1,15 @@
+self.addEventListener("fetch", function(event) {
+ var resource = event.request.url.split("/").pop();
+ event.waitUntil(
+ clients.matchAll().then(clients => {
+ clients.forEach(client => {
+ if (client.url.includes("plugins.html")) {
+ client.postMessage({
+ destination: event.request.destination,
+ resource,
+ });
+ }
+ });
+ })
+ );
+});
diff --git a/dom/serviceworkers/test/fetch/real-file.txt b/dom/serviceworkers/test/fetch/real-file.txt
new file mode 100644
index 0000000000..3ca2088ec0
--- /dev/null
+++ b/dom/serviceworkers/test/fetch/real-file.txt
@@ -0,0 +1 @@
+This is a real file.
diff --git a/dom/serviceworkers/test/fetch/redirect.sjs b/dom/serviceworkers/test/fetch/redirect.sjs
new file mode 100644
index 0000000000..dab558f4a8
--- /dev/null
+++ b/dom/serviceworkers/test/fetch/redirect.sjs
@@ -0,0 +1,4 @@
+function handleRequest(request, response) {
+ response.setStatusLine(request.httpVersion, 301, "Moved Permanently");
+ response.setHeader("Location", "synthesized-redirect-twice-real-file.txt");
+}
diff --git a/dom/serviceworkers/test/fetch/requesturl/index.html b/dom/serviceworkers/test/fetch/requesturl/index.html
new file mode 100644
index 0000000000..bc3e400a94
--- /dev/null
+++ b/dom/serviceworkers/test/fetch/requesturl/index.html
@@ -0,0 +1,7 @@
+<!DOCTYPE html>
+<script>
+ navigator.serviceWorker.onmessage = window.onmessage = e => {
+ window.parent.postMessage(e.data, "*");
+ };
+</script>
+<iframe src="redirector.html"></iframe>
diff --git a/dom/serviceworkers/test/fetch/requesturl/redirect.sjs b/dom/serviceworkers/test/fetch/requesturl/redirect.sjs
new file mode 100644
index 0000000000..ae50a78aef
--- /dev/null
+++ b/dom/serviceworkers/test/fetch/requesturl/redirect.sjs
@@ -0,0 +1,8 @@
+function handleRequest(request, response) {
+ response.setStatusLine(null, 308, "Permanent Redirect");
+ response.setHeader(
+ "Location",
+ "http://example.org/tests/dom/serviceworkers/test/fetch/requesturl/secret.html",
+ false
+ );
+}
diff --git a/dom/serviceworkers/test/fetch/requesturl/redirector.html b/dom/serviceworkers/test/fetch/requesturl/redirector.html
new file mode 100644
index 0000000000..0a3afab9ee
--- /dev/null
+++ b/dom/serviceworkers/test/fetch/requesturl/redirector.html
@@ -0,0 +1,2 @@
+<!DOCTYPE html>
+<meta http-equiv="refresh" content="3;URL=/tests/dom/serviceworkers/test/fetch/requesturl/redirect.sjs">
diff --git a/dom/serviceworkers/test/fetch/requesturl/register.html b/dom/serviceworkers/test/fetch/requesturl/register.html
new file mode 100644
index 0000000000..19a2e022c2
--- /dev/null
+++ b/dom/serviceworkers/test/fetch/requesturl/register.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<script>
+ function ok(v, msg) {
+ window.parent.postMessage({status: "ok", result: !!v, message: msg}, "*");
+ }
+
+ function done(reg) {
+ ok(reg.active, "The active worker should be available.");
+ window.parent.postMessage({status: "registrationdone"}, "*");
+ }
+
+ navigator.serviceWorker.ready.then(done);
+ navigator.serviceWorker.register("requesturl_test.js", {scope: "."});
+</script>
diff --git a/dom/serviceworkers/test/fetch/requesturl/requesturl_test.js b/dom/serviceworkers/test/fetch/requesturl/requesturl_test.js
new file mode 100644
index 0000000000..4d2680538f
--- /dev/null
+++ b/dom/serviceworkers/test/fetch/requesturl/requesturl_test.js
@@ -0,0 +1,21 @@
+addEventListener("fetch", event => {
+ var url = event.request.url;
+ var badURL = url.indexOf("secret.html") > -1;
+ event.respondWith(
+ new Promise(resolve => {
+ clients.matchAll().then(clients => {
+ for (var client of clients) {
+ if (client.url.indexOf("index.html") > -1) {
+ client.postMessage({
+ status: "ok",
+ result: !badURL,
+ message: "Should not find a bad URL (" + url + ")",
+ });
+ break;
+ }
+ }
+ resolve(fetch(event.request));
+ });
+ })
+ );
+});
diff --git a/dom/serviceworkers/test/fetch/requesturl/secret.html b/dom/serviceworkers/test/fetch/requesturl/secret.html
new file mode 100644
index 0000000000..694c336355
--- /dev/null
+++ b/dom/serviceworkers/test/fetch/requesturl/secret.html
@@ -0,0 +1,5 @@
+<!DOCTYPE html>
+secret stuff
+<script>
+ window.parent.postMessage({status: "done"}, "*");
+</script>
diff --git a/dom/serviceworkers/test/fetch/requesturl/unregister.html b/dom/serviceworkers/test/fetch/requesturl/unregister.html
new file mode 100644
index 0000000000..1f13508fa7
--- /dev/null
+++ b/dom/serviceworkers/test/fetch/requesturl/unregister.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<script>
+ navigator.serviceWorker.getRegistration(".").then(function(registration) {
+ registration.unregister().then(function(success) {
+ if (success) {
+ window.parent.postMessage({status: "unregistrationdone"}, "*");
+ }
+ }, function(e) {
+ dump("Unregistering the SW failed with " + e + "\n");
+ });
+ });
+</script>
diff --git a/dom/serviceworkers/test/fetch/sandbox/index.html b/dom/serviceworkers/test/fetch/sandbox/index.html
new file mode 100644
index 0000000000..1094a3995d
--- /dev/null
+++ b/dom/serviceworkers/test/fetch/sandbox/index.html
@@ -0,0 +1,5 @@
+<!DOCTYPE html>
+<script>
+ window.parent.postMessage({status: "ok", result: true, message: "The iframe is not being intercepted"}, "*");
+ window.parent.postMessage({status: "done"}, "*");
+</script>
diff --git a/dom/serviceworkers/test/fetch/sandbox/intercepted_index.html b/dom/serviceworkers/test/fetch/sandbox/intercepted_index.html
new file mode 100644
index 0000000000..87261a495f
--- /dev/null
+++ b/dom/serviceworkers/test/fetch/sandbox/intercepted_index.html
@@ -0,0 +1,5 @@
+<!DOCTYPE html>
+<script>
+ window.parent.postMessage({status: "ok", result: false, message: "The iframe is being intercepted"}, "*");
+ window.parent.postMessage({status: "done"}, "*");
+</script>
diff --git a/dom/serviceworkers/test/fetch/sandbox/register.html b/dom/serviceworkers/test/fetch/sandbox/register.html
new file mode 100644
index 0000000000..427b1a8da9
--- /dev/null
+++ b/dom/serviceworkers/test/fetch/sandbox/register.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<script>
+ function ok(v, msg) {
+ window.parent.postMessage({status: "ok", result: !!v, message: msg}, "*");
+ }
+
+ function done(reg) {
+ ok(reg.active, "The active worker should be available.");
+ window.parent.postMessage({status: "registrationdone"}, "*");
+ }
+
+ navigator.serviceWorker.ready.then(done);
+ navigator.serviceWorker.register("sandbox_test.js", {scope: "."});
+</script>
diff --git a/dom/serviceworkers/test/fetch/sandbox/sandbox_test.js b/dom/serviceworkers/test/fetch/sandbox/sandbox_test.js
new file mode 100644
index 0000000000..81740edf6d
--- /dev/null
+++ b/dom/serviceworkers/test/fetch/sandbox/sandbox_test.js
@@ -0,0 +1,5 @@
+self.addEventListener("fetch", function(event) {
+ if (event.request.url.includes("index.html")) {
+ event.respondWith(fetch("intercepted_index.html"));
+ }
+});
diff --git a/dom/serviceworkers/test/fetch/sandbox/unregister.html b/dom/serviceworkers/test/fetch/sandbox/unregister.html
new file mode 100644
index 0000000000..1f13508fa7
--- /dev/null
+++ b/dom/serviceworkers/test/fetch/sandbox/unregister.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<script>
+ navigator.serviceWorker.getRegistration(".").then(function(registration) {
+ registration.unregister().then(function(success) {
+ if (success) {
+ window.parent.postMessage({status: "unregistrationdone"}, "*");
+ }
+ }, function(e) {
+ dump("Unregistering the SW failed with " + e + "\n");
+ });
+ });
+</script>
diff --git a/dom/serviceworkers/test/fetch/upgrade-insecure/embedder.html b/dom/serviceworkers/test/fetch/upgrade-insecure/embedder.html
new file mode 100644
index 0000000000..e99209aa4d
--- /dev/null
+++ b/dom/serviceworkers/test/fetch/upgrade-insecure/embedder.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<script>
+ window.onmessage = function(e) {
+ window.parent.postMessage(e.data, "*");
+ if (e.data.status == "protocol") {
+ document.querySelector("iframe").src = "image.html";
+ }
+ };
+</script>
+<iframe src="http://example.com/tests/dom/serviceworkers/test/fetch/upgrade-insecure/index.html"></iframe>
diff --git a/dom/serviceworkers/test/fetch/upgrade-insecure/embedder.html^headers^ b/dom/serviceworkers/test/fetch/upgrade-insecure/embedder.html^headers^
new file mode 100644
index 0000000000..602d9dc38d
--- /dev/null
+++ b/dom/serviceworkers/test/fetch/upgrade-insecure/embedder.html^headers^
@@ -0,0 +1 @@
+Content-Security-Policy: upgrade-insecure-requests
diff --git a/dom/serviceworkers/test/fetch/upgrade-insecure/image-20px.png b/dom/serviceworkers/test/fetch/upgrade-insecure/image-20px.png
new file mode 100644
index 0000000000..ae6a8a6b88
--- /dev/null
+++ b/dom/serviceworkers/test/fetch/upgrade-insecure/image-20px.png
Binary files differ
diff --git a/dom/serviceworkers/test/fetch/upgrade-insecure/image-40px.png b/dom/serviceworkers/test/fetch/upgrade-insecure/image-40px.png
new file mode 100644
index 0000000000..fe391dc8a2
--- /dev/null
+++ b/dom/serviceworkers/test/fetch/upgrade-insecure/image-40px.png
Binary files differ
diff --git a/dom/serviceworkers/test/fetch/upgrade-insecure/image.html b/dom/serviceworkers/test/fetch/upgrade-insecure/image.html
new file mode 100644
index 0000000000..dfcfd80014
--- /dev/null
+++ b/dom/serviceworkers/test/fetch/upgrade-insecure/image.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<script>
+onload=function(){
+ var img = new Image();
+ img.src = "http://example.com/tests/dom/serviceworkers/test/fetch/upgrade-insecure/image-20px.png";
+ img.onload = function() {
+ window.parent.postMessage({status: "image", data: img.width}, "*");
+ };
+ img.onerror = function() {
+ window.parent.postMessage({status: "image", data: "error"}, "*");
+ };
+};
+</script>
diff --git a/dom/serviceworkers/test/fetch/upgrade-insecure/realindex.html b/dom/serviceworkers/test/fetch/upgrade-insecure/realindex.html
new file mode 100644
index 0000000000..aaa255aad3
--- /dev/null
+++ b/dom/serviceworkers/test/fetch/upgrade-insecure/realindex.html
@@ -0,0 +1,4 @@
+<!DOCTYPE html>
+<script>
+ window.parent.postMessage({status: "protocol", data: location.protocol}, "*");
+</script>
diff --git a/dom/serviceworkers/test/fetch/upgrade-insecure/register.html b/dom/serviceworkers/test/fetch/upgrade-insecure/register.html
new file mode 100644
index 0000000000..6309b9b218
--- /dev/null
+++ b/dom/serviceworkers/test/fetch/upgrade-insecure/register.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<script>
+ function ok(v, msg) {
+ window.parent.postMessage({status: "ok", result: !!v, message: msg}, "*");
+ }
+
+ function done(reg) {
+ ok(reg.active, "The active worker should be available.");
+ window.parent.postMessage({status: "registrationdone"}, "*");
+ }
+
+ navigator.serviceWorker.ready.then(done);
+ navigator.serviceWorker.register("upgrade-insecure_test.js", {scope: "."});
+</script>
diff --git a/dom/serviceworkers/test/fetch/upgrade-insecure/unregister.html b/dom/serviceworkers/test/fetch/upgrade-insecure/unregister.html
new file mode 100644
index 0000000000..1f13508fa7
--- /dev/null
+++ b/dom/serviceworkers/test/fetch/upgrade-insecure/unregister.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<script>
+ navigator.serviceWorker.getRegistration(".").then(function(registration) {
+ registration.unregister().then(function(success) {
+ if (success) {
+ window.parent.postMessage({status: "unregistrationdone"}, "*");
+ }
+ }, function(e) {
+ dump("Unregistering the SW failed with " + e + "\n");
+ });
+ });
+</script>
diff --git a/dom/serviceworkers/test/fetch/upgrade-insecure/upgrade-insecure_test.js b/dom/serviceworkers/test/fetch/upgrade-insecure/upgrade-insecure_test.js
new file mode 100644
index 0000000000..f79307f4e4
--- /dev/null
+++ b/dom/serviceworkers/test/fetch/upgrade-insecure/upgrade-insecure_test.js
@@ -0,0 +1,11 @@
+self.addEventListener("fetch", function(event) {
+ if (event.request.url.includes("index.html")) {
+ event.respondWith(fetch("realindex.html"));
+ } else if (event.request.url.includes("image-20px.png")) {
+ if (event.request.url.indexOf("https://") == 0) {
+ event.respondWith(fetch("image-40px.png"));
+ } else {
+ event.respondWith(Response.error());
+ }
+ }
+});
diff --git a/dom/serviceworkers/test/fetch_event_worker.js b/dom/serviceworkers/test/fetch_event_worker.js
new file mode 100644
index 0000000000..2775ef5068
--- /dev/null
+++ b/dom/serviceworkers/test/fetch_event_worker.js
@@ -0,0 +1,364 @@
+// eslint-disable-next-line complexity
+onfetch = function(ev) {
+ if (ev.request.url.includes("ignore")) {
+ return;
+ }
+
+ if (ev.request.url.includes("bare-synthesized.txt")) {
+ ev.respondWith(
+ Promise.resolve(new Response("synthesized response body", {}))
+ );
+ } else if (ev.request.url.includes("file_CrossSiteXHR_server.sjs")) {
+ // N.B. this response would break the rules of CORS if it were allowed, but
+ // this test relies upon the preflight request not being intercepted and
+ // thus this response should not be used.
+ if (ev.request.method == "OPTIONS") {
+ ev.respondWith(
+ new Response("", {
+ headers: {
+ "Access-Control-Allow-Origin": "*",
+ "Access-Control-Allow-Headers": "X-Unsafe",
+ },
+ })
+ );
+ } else if (ev.request.url.includes("example.org")) {
+ ev.respondWith(fetch(ev.request));
+ }
+ } else if (ev.request.url.includes("synthesized-404.txt")) {
+ ev.respondWith(
+ Promise.resolve(
+ new Response("synthesized response body", { status: 404 })
+ )
+ );
+ } else if (ev.request.url.includes("synthesized-headers.txt")) {
+ ev.respondWith(
+ Promise.resolve(
+ new Response("synthesized response body", {
+ headers: {
+ "X-Custom-Greeting": "Hello",
+ },
+ })
+ )
+ );
+ } else if (ev.request.url.includes("test-respondwith-response.txt")) {
+ ev.respondWith(new Response("test-respondwith-response response body", {}));
+ } else if (ev.request.url.includes("synthesized-redirect-real-file.txt")) {
+ ev.respondWith(Promise.resolve(Response.redirect("fetch/real-file.txt")));
+ } else if (
+ ev.request.url.includes("synthesized-redirect-twice-real-file.txt")
+ ) {
+ ev.respondWith(
+ Promise.resolve(Response.redirect("synthesized-redirect-real-file.txt"))
+ );
+ } else if (ev.request.url.includes("synthesized-redirect-synthesized.txt")) {
+ ev.respondWith(Promise.resolve(Response.redirect("bare-synthesized.txt")));
+ } else if (
+ ev.request.url.includes("synthesized-redirect-twice-synthesized.txt")
+ ) {
+ ev.respondWith(
+ Promise.resolve(Response.redirect("synthesized-redirect-synthesized.txt"))
+ );
+ } else if (ev.request.url.includes("rejected.txt")) {
+ ev.respondWith(Promise.reject());
+ } else if (ev.request.url.includes("nonresponse.txt")) {
+ ev.respondWith(Promise.resolve(5));
+ } else if (ev.request.url.includes("nonresponse2.txt")) {
+ ev.respondWith(Promise.resolve({}));
+ } else if (ev.request.url.includes("nonpromise.txt")) {
+ try {
+ // This should coerce to Promise(5) instead of throwing
+ ev.respondWith(5);
+ } catch (e) {
+ // test is expecting failure, so return a success if we get a thrown
+ // exception
+ ev.respondWith(new Response("respondWith(5) threw " + e));
+ }
+ } else if (ev.request.url.includes("headers.txt")) {
+ var ok = true;
+ ok &= ev.request.headers.get("X-Test1") == "header1";
+ ok &= ev.request.headers.get("X-Test2") == "header2";
+ ev.respondWith(Promise.resolve(new Response(ok.toString(), {})));
+ } else if (ev.request.url.includes("readable-stream.txt")) {
+ ev.respondWith(
+ new Response(
+ new ReadableStream({
+ start(controller) {
+ controller.enqueue(
+ new Uint8Array([0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x21])
+ );
+ controller.close();
+ },
+ })
+ )
+ );
+ } else if (ev.request.url.includes("readable-stream-locked.txt")) {
+ let stream = new ReadableStream({
+ start(controller) {
+ controller.enqueue(
+ new Uint8Array([0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x21])
+ );
+ controller.close();
+ },
+ });
+
+ ev.respondWith(new Response(stream));
+
+ // This locks the stream.
+ stream.getReader();
+ } else if (ev.request.url.includes("readable-stream-with-exception.txt")) {
+ ev.respondWith(
+ new Response(
+ new ReadableStream({
+ start(controller) {},
+ pull() {
+ throw "EXCEPTION!";
+ },
+ })
+ )
+ );
+ } else if (ev.request.url.includes("readable-stream-with-exception2.txt")) {
+ ev.respondWith(
+ new Response(
+ new ReadableStream({
+ _controller: null,
+ _count: 0,
+
+ start(controller) {
+ this._controller = controller;
+ },
+ pull() {
+ if (++this._count == 5) {
+ throw "EXCEPTION 2!";
+ }
+ this._controller.enqueue(new Uint8Array([this._count]));
+ },
+ })
+ )
+ );
+ } else if (ev.request.url.includes("readable-stream-already-consumed.txt")) {
+ let r = new Response(
+ new ReadableStream({
+ start(controller) {
+ controller.enqueue(
+ new Uint8Array([0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x21])
+ );
+ controller.close();
+ },
+ })
+ );
+
+ r.blob();
+
+ ev.respondWith(r);
+ } else if (ev.request.url.includes("user-pass")) {
+ ev.respondWith(new Response(ev.request.url));
+ } else if (ev.request.url.includes("nonexistent_image.gif")) {
+ var imageAsBinaryString = atob(
+ "R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs"
+ );
+ var imageLength = imageAsBinaryString.length;
+
+ // If we just pass |imageAsBinaryString| to the Response constructor, an
+ // encoding conversion occurs that corrupts the image. Instead, we need to
+ // convert it to a typed array.
+ // typed array.
+ var imageAsArray = new Uint8Array(imageLength);
+ for (var i = 0; i < imageLength; ++i) {
+ imageAsArray[i] = imageAsBinaryString.charCodeAt(i);
+ }
+
+ ev.respondWith(
+ Promise.resolve(
+ new Response(imageAsArray, { headers: { "Content-Type": "image/gif" } })
+ )
+ );
+ } else if (ev.request.url.includes("nonexistent_script.js")) {
+ ev.respondWith(
+ Promise.resolve(new Response("check_intercepted_script();", {}))
+ );
+ } else if (ev.request.url.includes("nonexistent_stylesheet.css")) {
+ ev.respondWith(
+ Promise.resolve(
+ new Response("#style-test { background-color: black !important; }", {
+ headers: {
+ "Content-Type": "text/css",
+ },
+ })
+ )
+ );
+ } else if (ev.request.url.includes("nonexistent_page.html")) {
+ ev.respondWith(
+ Promise.resolve(
+ new Response(
+ "<script>window.frameElement.test_result = true;</script>",
+ {
+ headers: {
+ "Content-Type": "text/html",
+ },
+ }
+ )
+ )
+ );
+ } else if (ev.request.url.includes("navigate.html")) {
+ var requests = [
+ // should not throw
+ new Request(ev.request),
+ new Request(ev.request, undefined),
+ new Request(ev.request, null),
+ new Request(ev.request, {}),
+ new Request(ev.request, { someUnrelatedProperty: 42 }),
+ new Request(ev.request, { method: "GET" }),
+ ];
+ ev.respondWith(
+ Promise.resolve(
+ new Response(
+ "<script>window.frameElement.test_result = true;</script>",
+ {
+ headers: {
+ "Content-Type": "text/html",
+ },
+ }
+ )
+ )
+ );
+ } else if (ev.request.url.includes("nonexistent_worker_script.js")) {
+ ev.respondWith(
+ Promise.resolve(
+ new Response("postMessage('worker-intercept-success')", {
+ headers: { "Content-Type": "text/javascript" },
+ })
+ )
+ );
+ } else if (ev.request.url.includes("nonexistent_imported_script.js")) {
+ ev.respondWith(
+ Promise.resolve(
+ new Response("check_intercepted_script();", {
+ headers: { "Content-Type": "text/javascript" },
+ })
+ )
+ );
+ } else if (ev.request.url.includes("deliver-gzip")) {
+ // Don't handle the request, this will make Necko perform a network request, at
+ // which point SetApplyConversion must be re-enabled, otherwise the request
+ // will fail.
+ return;
+ } else if (ev.request.url.includes("hello.gz")) {
+ ev.respondWith(fetch("fetch/deliver-gzip.sjs"));
+ } else if (ev.request.url.includes("hello-after-extracting.gz")) {
+ ev.respondWith(
+ fetch("fetch/deliver-gzip.sjs").then(function(res) {
+ return res.text().then(function(body) {
+ return new Response(body, {
+ status: res.status,
+ statusText: res.statusText,
+ headers: res.headers,
+ });
+ });
+ })
+ );
+ } else if (ev.request.url.includes("opaque-on-same-origin")) {
+ var url =
+ "http://example.com/tests/dom/security/test/cors/file_CrossSiteXHR_server.sjs?status=200";
+ ev.respondWith(fetch(url, { mode: "no-cors" }));
+ } else if (ev.request.url.includes("opaque-no-cors")) {
+ if (ev.request.mode != "no-cors") {
+ ev.respondWith(Promise.reject());
+ return;
+ }
+
+ var url =
+ "http://example.com/tests/dom/security/test/cors/file_CrossSiteXHR_server.sjs?status=200";
+ ev.respondWith(fetch(url, { mode: ev.request.mode }));
+ } else if (ev.request.url.includes("cors-for-no-cors")) {
+ if (ev.request.mode != "no-cors") {
+ ev.respondWith(Promise.reject());
+ return;
+ }
+
+ var url =
+ "http://example.com/tests/dom/security/test/cors/file_CrossSiteXHR_server.sjs?status=200&allowOrigin=*";
+ ev.respondWith(fetch(url));
+ } else if (ev.request.url.includes("example.com")) {
+ ev.respondWith(fetch(ev.request));
+ } else if (ev.request.url.includes("body-")) {
+ ev.respondWith(
+ ev.request.text().then(function(body) {
+ return new Response(body + body);
+ })
+ );
+ } else if (ev.request.url.includes("something.txt")) {
+ ev.respondWith(Response.redirect("fetch/somethingelse.txt"));
+ } else if (ev.request.url.includes("somethingelse.txt")) {
+ ev.respondWith(new Response("something else response body", {}));
+ } else if (ev.request.url.includes("redirect_serviceworker.sjs")) {
+ // The redirect_serviceworker.sjs server-side JavaScript file redirects to
+ // 'http://mochi.test:8888/tests/dom/serviceworkers/test/worker.js'
+ // The redirected fetch should not go through the SW since the original
+ // fetch was initiated from a SW.
+ ev.respondWith(fetch("redirect_serviceworker.sjs"));
+ } else if (
+ ev.request.url.includes("load_cross_origin_xml_document_synthetic.xml")
+ ) {
+ ev.respondWith(
+ Promise.resolve(
+ new Response("<response>body</response>", {
+ headers: { "Content-Type": "text/xtml" },
+ })
+ )
+ );
+ } else if (
+ ev.request.url.includes("load_cross_origin_xml_document_cors.xml")
+ ) {
+ if (ev.request.mode != "same-origin") {
+ ev.respondWith(Promise.reject());
+ return;
+ }
+
+ var url =
+ "http://example.com/tests/dom/security/test/cors/file_CrossSiteXHR_server.sjs?status=200&allowOrigin=*";
+ ev.respondWith(fetch(url, { mode: "cors" }));
+ } else if (
+ ev.request.url.includes("load_cross_origin_xml_document_opaque.xml")
+ ) {
+ if (ev.request.mode != "same-origin") {
+ Promise.resolve(
+ new Response("<error>Invalid Request mode</error>", {
+ headers: { "Content-Type": "text/xtml" },
+ })
+ );
+ return;
+ }
+
+ var url =
+ "http://example.com/tests/dom/security/test/cors/file_CrossSiteXHR_server.sjs?status=200";
+ ev.respondWith(fetch(url, { mode: "no-cors" }));
+ } else if (ev.request.url.includes("xhr-method-test.txt")) {
+ ev.respondWith(new Response("intercepted " + ev.request.method));
+ } else if (ev.request.url.includes("empty-header")) {
+ if (
+ !ev.request.headers.has("emptyheader") ||
+ ev.request.headers.get("emptyheader") !== ""
+ ) {
+ ev.respondWith(Promise.reject());
+ return;
+ }
+ ev.respondWith(new Response("emptyheader"));
+ } else if (ev.request.url.includes("fetchevent-extendable")) {
+ if (ev instanceof ExtendableEvent) {
+ ev.respondWith(new Response("extendable"));
+ } else {
+ ev.respondWith(Promise.reject());
+ }
+ } else if (ev.request.url.includes("fetchevent-request")) {
+ var threw = false;
+ try {
+ new FetchEvent("foo");
+ } catch (e) {
+ if (e.name == "TypeError") {
+ threw = true;
+ }
+ } finally {
+ ev.respondWith(new Response(threw ? "non-nullable" : "nullable"));
+ }
+ }
+};
diff --git a/dom/serviceworkers/test/file_blob_response_worker.js b/dom/serviceworkers/test/file_blob_response_worker.js
new file mode 100644
index 0000000000..05e82b8c5d
--- /dev/null
+++ b/dom/serviceworkers/test/file_blob_response_worker.js
@@ -0,0 +1,39 @@
+function makeFileBlob(obj) {
+ return new Promise(function(resolve, reject) {
+ var request = indexedDB.open("file_blob_response_worker", 1);
+ request.onerror = reject;
+ request.onupgradeneeded = function(evt) {
+ var db = evt.target.result;
+ db.onerror = reject;
+
+ var objectStore = db.createObjectStore("test", { autoIncrement: true });
+ var index = objectStore.createIndex("test", "index");
+ };
+
+ request.onsuccess = function(evt) {
+ var db = evt.target.result;
+ db.onerror = reject;
+
+ var blob = new Blob([JSON.stringify(obj)], { type: "application/json" });
+ var data = { blob, index: 5 };
+
+ objectStore = db.transaction("test", "readwrite").objectStore("test");
+ objectStore.add(data).onsuccess = function(event) {
+ var key = event.target.result;
+ objectStore = db.transaction("test").objectStore("test");
+ objectStore.get(key).onsuccess = function(event1) {
+ resolve(event1.target.result.blob);
+ };
+ };
+ };
+ });
+}
+
+self.addEventListener("fetch", function(evt) {
+ var result = { value: "success" };
+ evt.respondWith(
+ makeFileBlob(result).then(function(blob) {
+ return new Response(blob);
+ })
+ );
+});
diff --git a/dom/serviceworkers/test/file_js_cache.html b/dom/serviceworkers/test/file_js_cache.html
new file mode 100644
index 0000000000..6feb94d872
--- /dev/null
+++ b/dom/serviceworkers/test/file_js_cache.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Add a tag script to save the bytecode</title>
+</head>
+<body>
+ <script id="watchme" src="file_js_cache.js"></script>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/file_js_cache.js b/dom/serviceworkers/test/file_js_cache.js
new file mode 100644
index 0000000000..b9b966775c
--- /dev/null
+++ b/dom/serviceworkers/test/file_js_cache.js
@@ -0,0 +1,5 @@
+function baz() {}
+function bar() {}
+function foo() { bar() }
+foo();
+
diff --git a/dom/serviceworkers/test/file_js_cache_cleanup.js b/dom/serviceworkers/test/file_js_cache_cleanup.js
new file mode 100644
index 0000000000..5a647046a1
--- /dev/null
+++ b/dom/serviceworkers/test/file_js_cache_cleanup.js
@@ -0,0 +1,16 @@
+"use strict";
+const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+function clearCache() {
+ const cacheStorageSrv = Cc[
+ "@mozilla.org/netwerk/cache-storage-service;1"
+ ].getService(Ci.nsICacheStorageService);
+ cacheStorageSrv.clear();
+}
+
+addMessageListener("teardown", function() {
+ clearCache();
+ sendAsyncMessage("teardown-complete");
+});
diff --git a/dom/serviceworkers/test/file_js_cache_save_after_load.html b/dom/serviceworkers/test/file_js_cache_save_after_load.html
new file mode 100644
index 0000000000..8a696c0026
--- /dev/null
+++ b/dom/serviceworkers/test/file_js_cache_save_after_load.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Save the bytecode when all scripts are executed</title>
+</head>
+<body>
+ <script id="watchme" src="file_js_cache_save_after_load.js"></script>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/file_js_cache_save_after_load.js b/dom/serviceworkers/test/file_js_cache_save_after_load.js
new file mode 100644
index 0000000000..2c2536c584
--- /dev/null
+++ b/dom/serviceworkers/test/file_js_cache_save_after_load.js
@@ -0,0 +1,15 @@
+function send_ping() {
+ window.dispatchEvent(new Event("ping"));
+}
+send_ping(); // ping (=1)
+
+window.addEventListener("load", function() {
+ send_ping(); // ping (=2)
+
+ // Append a script which should call |foo|, before the encoding of this script
+ // bytecode.
+ var script = document.createElement("script");
+ script.type = "text/javascript";
+ script.innerText = "send_ping();"; // ping (=3)
+ document.head.appendChild(script);
+});
diff --git a/dom/serviceworkers/test/file_js_cache_syntax_error.html b/dom/serviceworkers/test/file_js_cache_syntax_error.html
new file mode 100644
index 0000000000..cc4a9b2daa
--- /dev/null
+++ b/dom/serviceworkers/test/file_js_cache_syntax_error.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Do not save bytecode on compilation errors</title>
+</head>
+<body>
+ <script id="watchme" src="file_js_cache_syntax_error.js"></script>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/file_js_cache_syntax_error.js b/dom/serviceworkers/test/file_js_cache_syntax_error.js
new file mode 100644
index 0000000000..fcf587ae70
--- /dev/null
+++ b/dom/serviceworkers/test/file_js_cache_syntax_error.js
@@ -0,0 +1 @@
+var // SyntaxError: missing variable name.
diff --git a/dom/serviceworkers/test/file_js_cache_with_sri.html b/dom/serviceworkers/test/file_js_cache_with_sri.html
new file mode 100644
index 0000000000..38ecb26984
--- /dev/null
+++ b/dom/serviceworkers/test/file_js_cache_with_sri.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Add a tag script to save the bytecode</title>
+</head>
+<body>
+ <script id="watchme" src="file_js_cache.js"
+ integrity="sha384-8YSwN2ywq1SVThihWhj7uTVZ4UeIDwo3GgdPYnug+C+OS0oa6kH2IXBclwMaDJFb">
+ </script>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/file_notification_openWindow.html b/dom/serviceworkers/test/file_notification_openWindow.html
new file mode 100644
index 0000000000..f220f4808d
--- /dev/null
+++ b/dom/serviceworkers/test/file_notification_openWindow.html
@@ -0,0 +1,26 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1578070</title>
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+<script class="testbody" type="text/javascript">
+window.onload = () => {
+ navigator.serviceWorker.ready.then(() => {
+ // Open and close a new window.
+ window.open("https://example.org/").close();
+
+ // If we make it here, then we didn't crash. Tell the worker we're done.
+ navigator.serviceWorker.controller.postMessage("DONE");
+
+ // We're done!
+ window.close();
+ });
+}
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/file_userContextId_openWindow.js b/dom/serviceworkers/test/file_userContextId_openWindow.js
new file mode 100644
index 0000000000..649a3152ba
--- /dev/null
+++ b/dom/serviceworkers/test/file_userContextId_openWindow.js
@@ -0,0 +1,3 @@
+onnotificationclick = event => {
+ clients.openWindow("empty.html");
+};
diff --git a/dom/serviceworkers/test/force_refresh_browser_worker.js b/dom/serviceworkers/test/force_refresh_browser_worker.js
new file mode 100644
index 0000000000..9c5be48530
--- /dev/null
+++ b/dom/serviceworkers/test/force_refresh_browser_worker.js
@@ -0,0 +1,42 @@
+var name = "browserRefresherCache";
+
+self.addEventListener("install", function(event) {
+ event.waitUntil(
+ Promise.all([
+ caches.open(name),
+ fetch("./browser_cached_force_refresh.html"),
+ ]).then(function(results) {
+ var cache = results[0];
+ var response = results[1];
+ return cache.put("./browser_base_force_refresh.html", response);
+ })
+ );
+});
+
+self.addEventListener("fetch", function(event) {
+ event.respondWith(
+ caches
+ .open(name)
+ .then(function(cache) {
+ return cache.match(event.request);
+ })
+ .then(function(response) {
+ return response || fetch(event.request);
+ })
+ );
+});
+
+self.addEventListener("message", function(event) {
+ if (event.data.type === "GET_UNCONTROLLED_CLIENTS") {
+ event.waitUntil(
+ clients
+ .matchAll({ includeUncontrolled: true })
+ .then(function(clientList) {
+ var resultList = clientList.map(function(c) {
+ return { url: c.url, frameType: c.frameType };
+ });
+ event.source.postMessage({ type: "CLIENTS", detail: resultList });
+ })
+ );
+ }
+});
diff --git a/dom/serviceworkers/test/force_refresh_worker.js b/dom/serviceworkers/test/force_refresh_worker.js
new file mode 100644
index 0000000000..2e266b54d7
--- /dev/null
+++ b/dom/serviceworkers/test/force_refresh_worker.js
@@ -0,0 +1,43 @@
+var name = "refresherCache";
+
+self.addEventListener("install", function(event) {
+ event.waitUntil(
+ Promise.all([
+ caches.open(name),
+ fetch("./sw_clients/refresher_cached.html"),
+ fetch("./sw_clients/refresher_cached_compressed.html"),
+ ]).then(function(results) {
+ var cache = results[0];
+ var response = results[1];
+ var compressed = results[2];
+ return Promise.all([
+ cache.put("./sw_clients/refresher.html", response),
+ cache.put("./sw_clients/refresher_compressed.html", compressed),
+ ]);
+ })
+ );
+});
+
+self.addEventListener("fetch", function(event) {
+ event.respondWith(
+ caches
+ .open(name)
+ .then(function(cache) {
+ return cache.match(event.request);
+ })
+ .then(function(response) {
+ // If this is one of our primary cached responses, then the window
+ // must have generated the request via a normal window reload. That
+ // should be detectable in the event.request.cache attribute.
+ if (response && event.request.cache !== "no-cache") {
+ dump(
+ '### ### FetchEvent.request.cache is "' +
+ event.request.cache +
+ '" instead of expected "no-cache"\n'
+ );
+ return Response.error();
+ }
+ return response || fetch(event.request);
+ })
+ );
+});
diff --git a/dom/serviceworkers/test/gtest/TestReadWrite.cpp b/dom/serviceworkers/test/gtest/TestReadWrite.cpp
new file mode 100644
index 0000000000..fbe71d7108
--- /dev/null
+++ b/dom/serviceworkers/test/gtest/TestReadWrite.cpp
@@ -0,0 +1,952 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "gtest/gtest.h"
+#include "mozilla/BasePrincipal.h"
+#include "mozilla/dom/ServiceWorkerRegistrar.h"
+#include "mozilla/dom/ServiceWorkerRegistrarTypes.h"
+#include "mozilla/ipc/PBackgroundSharedTypes.h"
+
+#include "nsAppDirectoryServiceDefs.h"
+#include "nsIFile.h"
+#include "nsIOutputStream.h"
+#include "nsNetUtil.h"
+#include "nsPrintfCString.h"
+#include "nsIServiceWorkerManager.h"
+
+#include "prtime.h"
+
+using namespace mozilla::dom;
+using namespace mozilla::ipc;
+
+class ServiceWorkerRegistrarTest : public ServiceWorkerRegistrar {
+ public:
+ ServiceWorkerRegistrarTest() {
+#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED
+ nsresult rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR,
+ getter_AddRefs(mProfileDir));
+ MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(rv));
+#else
+ NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR,
+ getter_AddRefs(mProfileDir));
+#endif
+ MOZ_DIAGNOSTIC_ASSERT(mProfileDir);
+ }
+
+ nsresult TestReadData() { return ReadData(); }
+ nsresult TestWriteData() MOZ_NO_THREAD_SAFETY_ANALYSIS {
+ return WriteData(mData);
+ }
+ void TestDeleteData() { DeleteData(); }
+
+ void TestRegisterServiceWorker(const ServiceWorkerRegistrationData& aData) {
+ mozilla::MonitorAutoLock lock(mMonitor);
+ RegisterServiceWorkerInternal(aData);
+ }
+
+ nsTArray<ServiceWorkerRegistrationData>& TestGetData() { return mData; }
+};
+
+already_AddRefed<nsIFile> GetFile() {
+ nsCOMPtr<nsIFile> file;
+ nsresult rv =
+ NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, getter_AddRefs(file));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return nullptr;
+ }
+
+ file->Append(nsLiteralString(SERVICEWORKERREGISTRAR_FILE));
+ return file.forget();
+}
+
+bool CreateFile(const nsACString& aData) {
+ nsCOMPtr<nsIFile> file = GetFile();
+
+ nsCOMPtr<nsIOutputStream> stream;
+ nsresult rv = NS_NewLocalFileOutputStream(getter_AddRefs(stream), file);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return false;
+ }
+
+ uint32_t count;
+ rv = stream->Write(aData.Data(), aData.Length(), &count);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return false;
+ }
+
+ if (count != aData.Length()) {
+ return false;
+ }
+
+ return true;
+}
+
+TEST(ServiceWorkerRegistrar, TestNoFile)
+{
+ nsCOMPtr<nsIFile> file = GetFile();
+ ASSERT_TRUE(file)
+ << "GetFile must return a nsIFIle";
+
+ bool exists;
+ nsresult rv = file->Exists(&exists);
+ ASSERT_EQ(NS_OK, rv) << "nsIFile::Exists cannot fail";
+
+ if (exists) {
+ rv = file->Remove(false);
+ ASSERT_EQ(NS_OK, rv) << "nsIFile::Remove cannot fail";
+ }
+
+ RefPtr<ServiceWorkerRegistrarTest> swr = new ServiceWorkerRegistrarTest;
+
+ rv = swr->TestReadData();
+ ASSERT_EQ(NS_OK, rv) << "ReadData() should not fail";
+
+ const nsTArray<ServiceWorkerRegistrationData>& data = swr->TestGetData();
+ ASSERT_EQ((uint32_t)0, data.Length())
+ << "No data should be found in an empty file";
+}
+
+TEST(ServiceWorkerRegistrar, TestEmptyFile)
+{
+ ASSERT_TRUE(CreateFile(""_ns))
+ << "CreateFile should not fail";
+
+ RefPtr<ServiceWorkerRegistrarTest> swr = new ServiceWorkerRegistrarTest;
+
+ nsresult rv = swr->TestReadData();
+ ASSERT_NE(NS_OK, rv) << "ReadData() should fail if the file is empty";
+
+ const nsTArray<ServiceWorkerRegistrationData>& data = swr->TestGetData();
+ ASSERT_EQ((uint32_t)0, data.Length())
+ << "No data should be found in an empty file";
+}
+
+TEST(ServiceWorkerRegistrar, TestRightVersionFile)
+{
+ ASSERT_TRUE(CreateFile(nsLiteralCString(SERVICEWORKERREGISTRAR_VERSION "\n")))
+ << "CreateFile should not fail";
+
+ RefPtr<ServiceWorkerRegistrarTest> swr = new ServiceWorkerRegistrarTest;
+
+ nsresult rv = swr->TestReadData();
+ ASSERT_EQ(NS_OK, rv)
+ << "ReadData() should not fail when the version is correct";
+
+ const nsTArray<ServiceWorkerRegistrationData>& data = swr->TestGetData();
+ ASSERT_EQ((uint32_t)0, data.Length())
+ << "No data should be found in an empty file";
+}
+
+TEST(ServiceWorkerRegistrar, TestWrongVersionFile)
+{
+ ASSERT_TRUE(
+ CreateFile(nsLiteralCString(SERVICEWORKERREGISTRAR_VERSION "bla\n")))
+ << "CreateFile should not fail";
+
+ RefPtr<ServiceWorkerRegistrarTest> swr = new ServiceWorkerRegistrarTest;
+
+ nsresult rv = swr->TestReadData();
+ ASSERT_NE(NS_OK, rv)
+ << "ReadData() should fail when the version is not correct";
+
+ const nsTArray<ServiceWorkerRegistrationData>& data = swr->TestGetData();
+ ASSERT_EQ((uint32_t)0, data.Length())
+ << "No data should be found in an empty file";
+}
+
+TEST(ServiceWorkerRegistrar, TestReadData)
+{
+ nsAutoCString buffer(SERVICEWORKERREGISTRAR_VERSION "\n");
+
+ buffer.AppendLiteral("^inBrowser=1\n");
+ buffer.AppendLiteral("https://scope_0.org\ncurrentWorkerURL 0\n");
+ buffer.Append(SERVICEWORKERREGISTRAR_TRUE "\n");
+ buffer.AppendLiteral("cacheName 0\n");
+ buffer.AppendInt(nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_IMPORTS,
+ 16);
+ buffer.AppendLiteral("\n");
+ buffer.AppendInt(0);
+ buffer.AppendLiteral("\n");
+ buffer.AppendInt(0);
+ buffer.AppendLiteral("\n");
+ buffer.AppendInt(0);
+ buffer.AppendLiteral("\n");
+ buffer.AppendInt(0);
+ buffer.AppendLiteral("\n");
+ buffer.AppendLiteral("true\n");
+ buffer.Append(SERVICEWORKERREGISTRAR_TERMINATOR "\n");
+
+ buffer.AppendLiteral("\n");
+ buffer.AppendLiteral("https://scope_1.org\ncurrentWorkerURL 1\n");
+ buffer.Append(SERVICEWORKERREGISTRAR_FALSE "\n");
+ buffer.AppendLiteral("cacheName 1\n");
+ buffer.AppendInt(nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_ALL, 16);
+ buffer.AppendLiteral("\n");
+ PRTime ts = PR_Now();
+ buffer.AppendInt(ts);
+ buffer.AppendLiteral("\n");
+ buffer.AppendInt(ts);
+ buffer.AppendLiteral("\n");
+ buffer.AppendInt(ts);
+ buffer.AppendLiteral("\n");
+ buffer.AppendInt(1);
+ buffer.AppendLiteral("\n");
+ buffer.AppendLiteral("false\n");
+ buffer.Append(SERVICEWORKERREGISTRAR_TERMINATOR "\n");
+
+ ASSERT_TRUE(CreateFile(buffer))
+ << "CreateFile should not fail";
+
+ RefPtr<ServiceWorkerRegistrarTest> swr = new ServiceWorkerRegistrarTest;
+
+ nsresult rv = swr->TestReadData();
+ ASSERT_EQ(NS_OK, rv) << "ReadData() should not fail";
+
+ const nsTArray<ServiceWorkerRegistrationData>& data = swr->TestGetData();
+ ASSERT_EQ((uint32_t)2, data.Length()) << "2 entries should be found";
+
+ const mozilla::ipc::PrincipalInfo& info0 = data[0].principal();
+ ASSERT_EQ(info0.type(), mozilla::ipc::PrincipalInfo::TContentPrincipalInfo)
+ << "First principal must be content";
+ const mozilla::ipc::ContentPrincipalInfo& cInfo0 = data[0].principal();
+
+ nsAutoCString suffix0;
+ cInfo0.attrs().CreateSuffix(suffix0);
+
+ ASSERT_STREQ("^inBrowser=1", suffix0.get());
+ ASSERT_STREQ("https://scope_0.org", cInfo0.spec().get());
+ ASSERT_STREQ("https://scope_0.org", data[0].scope().get());
+ ASSERT_STREQ("currentWorkerURL 0", data[0].currentWorkerURL().get());
+ ASSERT_TRUE(data[0].currentWorkerHandlesFetch());
+ ASSERT_STREQ("cacheName 0", NS_ConvertUTF16toUTF8(data[0].cacheName()).get());
+ ASSERT_EQ(nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_IMPORTS,
+ data[0].updateViaCache());
+ ASSERT_EQ((int64_t)0, data[0].currentWorkerInstalledTime());
+ ASSERT_EQ((int64_t)0, data[0].currentWorkerActivatedTime());
+ ASSERT_EQ((int64_t)0, data[0].lastUpdateTime());
+ ASSERT_EQ(false, data[0].navigationPreloadState().enabled());
+ ASSERT_STREQ("true", data[0].navigationPreloadState().headerValue().get());
+
+ const mozilla::ipc::PrincipalInfo& info1 = data[1].principal();
+ ASSERT_EQ(info1.type(), mozilla::ipc::PrincipalInfo::TContentPrincipalInfo)
+ << "First principal must be content";
+ const mozilla::ipc::ContentPrincipalInfo& cInfo1 = data[1].principal();
+
+ nsAutoCString suffix1;
+ cInfo1.attrs().CreateSuffix(suffix1);
+
+ ASSERT_STREQ("", suffix1.get());
+ ASSERT_STREQ("https://scope_1.org", cInfo1.spec().get());
+ ASSERT_STREQ("https://scope_1.org", data[1].scope().get());
+ ASSERT_STREQ("currentWorkerURL 1", data[1].currentWorkerURL().get());
+ ASSERT_FALSE(data[1].currentWorkerHandlesFetch());
+ ASSERT_STREQ("cacheName 1", NS_ConvertUTF16toUTF8(data[1].cacheName()).get());
+ ASSERT_EQ(nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_ALL,
+ data[1].updateViaCache());
+ ASSERT_EQ((int64_t)ts, data[1].currentWorkerInstalledTime());
+ ASSERT_EQ((int64_t)ts, data[1].currentWorkerActivatedTime());
+ ASSERT_EQ((int64_t)ts, data[1].lastUpdateTime());
+ ASSERT_EQ(true, data[1].navigationPreloadState().enabled());
+ ASSERT_STREQ("false", data[1].navigationPreloadState().headerValue().get());
+}
+
+TEST(ServiceWorkerRegistrar, TestDeleteData)
+{
+ ASSERT_TRUE(CreateFile("Foobar"_ns))
+ << "CreateFile should not fail";
+
+ RefPtr<ServiceWorkerRegistrarTest> swr = new ServiceWorkerRegistrarTest;
+
+ swr->TestDeleteData();
+
+ nsCOMPtr<nsIFile> file = GetFile();
+
+ bool exists;
+ nsresult rv = file->Exists(&exists);
+ ASSERT_EQ(NS_OK, rv) << "nsIFile::Exists cannot fail";
+
+ ASSERT_FALSE(exists)
+ << "The file should not exist after a DeleteData().";
+}
+
+TEST(ServiceWorkerRegistrar, TestWriteData)
+{
+ {
+ RefPtr<ServiceWorkerRegistrarTest> swr = new ServiceWorkerRegistrarTest;
+
+ for (int i = 0; i < 2; ++i) {
+ ServiceWorkerRegistrationData reg;
+
+ reg.scope() = nsPrintfCString("https://scope_write_%d.org", i);
+ reg.currentWorkerURL() = nsPrintfCString("currentWorkerURL write %d", i);
+ reg.currentWorkerHandlesFetch() = true;
+ reg.cacheName() =
+ NS_ConvertUTF8toUTF16(nsPrintfCString("cacheName write %d", i));
+ reg.updateViaCache() =
+ nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_IMPORTS;
+
+ reg.currentWorkerInstalledTime() = PR_Now();
+ reg.currentWorkerActivatedTime() = PR_Now();
+ reg.lastUpdateTime() = PR_Now();
+
+ nsAutoCString spec;
+ spec.AppendPrintf("spec write %d", i);
+
+ reg.principal() = mozilla::ipc::ContentPrincipalInfo(
+ mozilla::OriginAttributes(i % 2), spec, spec, mozilla::Nothing(),
+ spec);
+
+ swr->TestRegisterServiceWorker(reg);
+ }
+
+ nsresult rv = swr->TestWriteData();
+ ASSERT_EQ(NS_OK, rv) << "WriteData() should not fail";
+ }
+
+ RefPtr<ServiceWorkerRegistrarTest> swr = new ServiceWorkerRegistrarTest;
+
+ nsresult rv = swr->TestReadData();
+ ASSERT_EQ(NS_OK, rv) << "ReadData() should not fail";
+
+ const nsTArray<ServiceWorkerRegistrationData>& data = swr->TestGetData();
+ ASSERT_EQ((uint32_t)2, data.Length()) << "2 entries should be found";
+
+ for (int i = 0; i < 2; ++i) {
+ nsAutoCString test;
+
+ ASSERT_EQ(data[i].principal().type(),
+ mozilla::ipc::PrincipalInfo::TContentPrincipalInfo);
+ const mozilla::ipc::ContentPrincipalInfo& cInfo = data[i].principal();
+
+ mozilla::OriginAttributes attrs(i % 2);
+ nsAutoCString suffix, expectSuffix;
+ attrs.CreateSuffix(expectSuffix);
+ cInfo.attrs().CreateSuffix(suffix);
+
+ ASSERT_STREQ(expectSuffix.get(), suffix.get());
+
+ test.AppendPrintf("https://scope_write_%d.org", i);
+ ASSERT_STREQ(test.get(), cInfo.spec().get());
+
+ test.Truncate();
+ test.AppendPrintf("https://scope_write_%d.org", i);
+ ASSERT_STREQ(test.get(), data[i].scope().get());
+
+ test.Truncate();
+ test.AppendPrintf("currentWorkerURL write %d", i);
+ ASSERT_STREQ(test.get(), data[i].currentWorkerURL().get());
+
+ ASSERT_EQ(true, data[i].currentWorkerHandlesFetch());
+
+ test.Truncate();
+ test.AppendPrintf("cacheName write %d", i);
+ ASSERT_STREQ(test.get(), NS_ConvertUTF16toUTF8(data[i].cacheName()).get());
+
+ ASSERT_EQ(nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_IMPORTS,
+ data[i].updateViaCache());
+
+ ASSERT_NE((int64_t)0, data[i].currentWorkerInstalledTime());
+ ASSERT_NE((int64_t)0, data[i].currentWorkerActivatedTime());
+ ASSERT_NE((int64_t)0, data[i].lastUpdateTime());
+ }
+}
+
+TEST(ServiceWorkerRegistrar, TestVersion2Migration)
+{
+ nsAutoCString buffer(
+ "2"
+ "\n");
+
+ buffer.AppendLiteral("^appId=123&inBrowser=1\n");
+ buffer.AppendLiteral(
+ "spec 0\nhttps://scope_0.org\nscriptSpec 0\ncurrentWorkerURL "
+ "0\nactiveCache 0\nwaitingCache 0\n");
+ buffer.AppendLiteral(SERVICEWORKERREGISTRAR_TERMINATOR "\n");
+
+ buffer.AppendLiteral("\n");
+ buffer.AppendLiteral(
+ "spec 1\nhttps://scope_1.org\nscriptSpec 1\ncurrentWorkerURL "
+ "1\nactiveCache 1\nwaitingCache 1\n");
+ buffer.AppendLiteral(SERVICEWORKERREGISTRAR_TERMINATOR "\n");
+
+ ASSERT_TRUE(CreateFile(buffer))
+ << "CreateFile should not fail";
+
+ RefPtr<ServiceWorkerRegistrarTest> swr = new ServiceWorkerRegistrarTest;
+
+ nsresult rv = swr->TestReadData();
+ ASSERT_EQ(NS_OK, rv) << "ReadData() should not fail";
+
+ const nsTArray<ServiceWorkerRegistrationData>& data = swr->TestGetData();
+ ASSERT_EQ((uint32_t)2, data.Length()) << "2 entries should be found";
+
+ const mozilla::ipc::PrincipalInfo& info0 = data[0].principal();
+ ASSERT_EQ(info0.type(), mozilla::ipc::PrincipalInfo::TContentPrincipalInfo)
+ << "First principal must be content";
+ const mozilla::ipc::ContentPrincipalInfo& cInfo0 = data[0].principal();
+
+ nsAutoCString suffix0;
+ cInfo0.attrs().CreateSuffix(suffix0);
+
+ ASSERT_STREQ("^inBrowser=1", suffix0.get());
+ ASSERT_STREQ("https://scope_0.org", cInfo0.spec().get());
+ ASSERT_STREQ("https://scope_0.org", data[0].scope().get());
+ ASSERT_STREQ("currentWorkerURL 0", data[0].currentWorkerURL().get());
+ ASSERT_EQ(true, data[0].currentWorkerHandlesFetch());
+ ASSERT_STREQ("activeCache 0",
+ NS_ConvertUTF16toUTF8(data[0].cacheName()).get());
+ ASSERT_EQ(nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_IMPORTS,
+ data[0].updateViaCache());
+ ASSERT_EQ((int64_t)0, data[0].currentWorkerInstalledTime());
+ ASSERT_EQ((int64_t)0, data[0].currentWorkerActivatedTime());
+ ASSERT_EQ((int64_t)0, data[0].lastUpdateTime());
+
+ const mozilla::ipc::PrincipalInfo& info1 = data[1].principal();
+ ASSERT_EQ(info1.type(), mozilla::ipc::PrincipalInfo::TContentPrincipalInfo)
+ << "First principal must be content";
+ const mozilla::ipc::ContentPrincipalInfo& cInfo1 = data[1].principal();
+
+ nsAutoCString suffix1;
+ cInfo1.attrs().CreateSuffix(suffix1);
+
+ ASSERT_STREQ("", suffix1.get());
+ ASSERT_STREQ("https://scope_1.org", cInfo1.spec().get());
+ ASSERT_STREQ("https://scope_1.org", data[1].scope().get());
+ ASSERT_STREQ("currentWorkerURL 1", data[1].currentWorkerURL().get());
+ ASSERT_EQ(true, data[1].currentWorkerHandlesFetch());
+ ASSERT_STREQ("activeCache 1",
+ NS_ConvertUTF16toUTF8(data[1].cacheName()).get());
+ ASSERT_EQ(nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_IMPORTS,
+ data[1].updateViaCache());
+ ASSERT_EQ((int64_t)0, data[1].currentWorkerInstalledTime());
+ ASSERT_EQ((int64_t)0, data[1].currentWorkerActivatedTime());
+ ASSERT_EQ((int64_t)0, data[1].lastUpdateTime());
+}
+
+TEST(ServiceWorkerRegistrar, TestVersion3Migration)
+{
+ nsAutoCString buffer(
+ "3"
+ "\n");
+
+ buffer.AppendLiteral("^appId=123&inBrowser=1\n");
+ buffer.AppendLiteral(
+ "spec 0\nhttps://scope_0.org\ncurrentWorkerURL 0\ncacheName 0\n");
+ buffer.AppendLiteral(SERVICEWORKERREGISTRAR_TERMINATOR "\n");
+
+ buffer.AppendLiteral("\n");
+ buffer.AppendLiteral(
+ "spec 1\nhttps://scope_1.org\ncurrentWorkerURL 1\ncacheName 1\n");
+ buffer.AppendLiteral(SERVICEWORKERREGISTRAR_TERMINATOR "\n");
+
+ ASSERT_TRUE(CreateFile(buffer))
+ << "CreateFile should not fail";
+
+ RefPtr<ServiceWorkerRegistrarTest> swr = new ServiceWorkerRegistrarTest;
+
+ nsresult rv = swr->TestReadData();
+ ASSERT_EQ(NS_OK, rv) << "ReadData() should not fail";
+
+ const nsTArray<ServiceWorkerRegistrationData>& data = swr->TestGetData();
+ ASSERT_EQ((uint32_t)2, data.Length()) << "2 entries should be found";
+
+ const mozilla::ipc::PrincipalInfo& info0 = data[0].principal();
+ ASSERT_EQ(info0.type(), mozilla::ipc::PrincipalInfo::TContentPrincipalInfo)
+ << "First principal must be content";
+ const mozilla::ipc::ContentPrincipalInfo& cInfo0 = data[0].principal();
+
+ nsAutoCString suffix0;
+ cInfo0.attrs().CreateSuffix(suffix0);
+
+ ASSERT_STREQ("^inBrowser=1", suffix0.get());
+ ASSERT_STREQ("https://scope_0.org", cInfo0.spec().get());
+ ASSERT_STREQ("https://scope_0.org", data[0].scope().get());
+ ASSERT_STREQ("currentWorkerURL 0", data[0].currentWorkerURL().get());
+ ASSERT_EQ(true, data[0].currentWorkerHandlesFetch());
+ ASSERT_STREQ("cacheName 0", NS_ConvertUTF16toUTF8(data[0].cacheName()).get());
+ ASSERT_EQ(nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_IMPORTS,
+ data[0].updateViaCache());
+ ASSERT_EQ((int64_t)0, data[0].currentWorkerInstalledTime());
+ ASSERT_EQ((int64_t)0, data[0].currentWorkerActivatedTime());
+ ASSERT_EQ((int64_t)0, data[0].lastUpdateTime());
+
+ const mozilla::ipc::PrincipalInfo& info1 = data[1].principal();
+ ASSERT_EQ(info1.type(), mozilla::ipc::PrincipalInfo::TContentPrincipalInfo)
+ << "First principal must be content";
+ const mozilla::ipc::ContentPrincipalInfo& cInfo1 = data[1].principal();
+
+ nsAutoCString suffix1;
+ cInfo1.attrs().CreateSuffix(suffix1);
+
+ ASSERT_STREQ("", suffix1.get());
+ ASSERT_STREQ("https://scope_1.org", cInfo1.spec().get());
+ ASSERT_STREQ("https://scope_1.org", data[1].scope().get());
+ ASSERT_STREQ("currentWorkerURL 1", data[1].currentWorkerURL().get());
+ ASSERT_EQ(true, data[1].currentWorkerHandlesFetch());
+ ASSERT_STREQ("cacheName 1", NS_ConvertUTF16toUTF8(data[1].cacheName()).get());
+ ASSERT_EQ(nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_IMPORTS,
+ data[1].updateViaCache());
+ ASSERT_EQ((int64_t)0, data[1].currentWorkerInstalledTime());
+ ASSERT_EQ((int64_t)0, data[1].currentWorkerActivatedTime());
+ ASSERT_EQ((int64_t)0, data[1].lastUpdateTime());
+}
+
+TEST(ServiceWorkerRegistrar, TestVersion4Migration)
+{
+ nsAutoCString buffer(
+ "4"
+ "\n");
+
+ buffer.AppendLiteral("^appId=123&inBrowser=1\n");
+ buffer.AppendLiteral(
+ "https://scope_0.org\ncurrentWorkerURL 0\ncacheName 0\n");
+ buffer.AppendLiteral(SERVICEWORKERREGISTRAR_TERMINATOR "\n");
+
+ buffer.AppendLiteral("\n");
+ buffer.AppendLiteral(
+ "https://scope_1.org\ncurrentWorkerURL 1\ncacheName 1\n");
+ buffer.AppendLiteral(SERVICEWORKERREGISTRAR_TERMINATOR "\n");
+
+ ASSERT_TRUE(CreateFile(buffer))
+ << "CreateFile should not fail";
+
+ RefPtr<ServiceWorkerRegistrarTest> swr = new ServiceWorkerRegistrarTest;
+
+ nsresult rv = swr->TestReadData();
+ ASSERT_EQ(NS_OK, rv) << "ReadData() should not fail";
+
+ const nsTArray<ServiceWorkerRegistrationData>& data = swr->TestGetData();
+ ASSERT_EQ((uint32_t)2, data.Length()) << "2 entries should be found";
+
+ const mozilla::ipc::PrincipalInfo& info0 = data[0].principal();
+ ASSERT_EQ(info0.type(), mozilla::ipc::PrincipalInfo::TContentPrincipalInfo)
+ << "First principal must be content";
+ const mozilla::ipc::ContentPrincipalInfo& cInfo0 = data[0].principal();
+
+ nsAutoCString suffix0;
+ cInfo0.attrs().CreateSuffix(suffix0);
+
+ ASSERT_STREQ("^inBrowser=1", suffix0.get());
+ ASSERT_STREQ("https://scope_0.org", cInfo0.spec().get());
+ ASSERT_STREQ("https://scope_0.org", data[0].scope().get());
+ ASSERT_STREQ("currentWorkerURL 0", data[0].currentWorkerURL().get());
+ // default is true
+ ASSERT_EQ(true, data[0].currentWorkerHandlesFetch());
+ ASSERT_STREQ("cacheName 0", NS_ConvertUTF16toUTF8(data[0].cacheName()).get());
+ ASSERT_EQ(nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_IMPORTS,
+ data[0].updateViaCache());
+ ASSERT_EQ((int64_t)0, data[0].currentWorkerInstalledTime());
+ ASSERT_EQ((int64_t)0, data[0].currentWorkerActivatedTime());
+ ASSERT_EQ((int64_t)0, data[0].lastUpdateTime());
+
+ const mozilla::ipc::PrincipalInfo& info1 = data[1].principal();
+ ASSERT_EQ(info1.type(), mozilla::ipc::PrincipalInfo::TContentPrincipalInfo)
+ << "First principal must be content";
+ const mozilla::ipc::ContentPrincipalInfo& cInfo1 = data[1].principal();
+
+ nsAutoCString suffix1;
+ cInfo1.attrs().CreateSuffix(suffix1);
+
+ ASSERT_STREQ("", suffix1.get());
+ ASSERT_STREQ("https://scope_1.org", cInfo1.spec().get());
+ ASSERT_STREQ("https://scope_1.org", data[1].scope().get());
+ ASSERT_STREQ("currentWorkerURL 1", data[1].currentWorkerURL().get());
+ // default is true
+ ASSERT_EQ(true, data[1].currentWorkerHandlesFetch());
+ ASSERT_STREQ("cacheName 1", NS_ConvertUTF16toUTF8(data[1].cacheName()).get());
+ ASSERT_EQ(nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_IMPORTS,
+ data[1].updateViaCache());
+ ASSERT_EQ((int64_t)0, data[1].currentWorkerInstalledTime());
+ ASSERT_EQ((int64_t)0, data[1].currentWorkerActivatedTime());
+ ASSERT_EQ((int64_t)0, data[1].lastUpdateTime());
+}
+
+TEST(ServiceWorkerRegistrar, TestVersion5Migration)
+{
+ nsAutoCString buffer(
+ "5"
+ "\n");
+
+ buffer.AppendLiteral("^appId=123&inBrowser=1\n");
+ buffer.AppendLiteral("https://scope_0.org\ncurrentWorkerURL 0\n");
+ buffer.AppendLiteral(SERVICEWORKERREGISTRAR_TRUE "\n");
+ buffer.AppendLiteral("cacheName 0\n");
+ buffer.AppendLiteral(SERVICEWORKERREGISTRAR_TERMINATOR "\n");
+
+ buffer.AppendLiteral("\n");
+ buffer.AppendLiteral("https://scope_1.org\ncurrentWorkerURL 1\n");
+ buffer.AppendLiteral(SERVICEWORKERREGISTRAR_FALSE "\n");
+ buffer.AppendLiteral("cacheName 1\n");
+ buffer.AppendLiteral(SERVICEWORKERREGISTRAR_TERMINATOR "\n");
+
+ ASSERT_TRUE(CreateFile(buffer))
+ << "CreateFile should not fail";
+
+ RefPtr<ServiceWorkerRegistrarTest> swr = new ServiceWorkerRegistrarTest;
+
+ nsresult rv = swr->TestReadData();
+ ASSERT_EQ(NS_OK, rv) << "ReadData() should not fail";
+
+ const nsTArray<ServiceWorkerRegistrationData>& data = swr->TestGetData();
+ ASSERT_EQ((uint32_t)2, data.Length()) << "2 entries should be found";
+
+ const mozilla::ipc::PrincipalInfo& info0 = data[0].principal();
+ ASSERT_EQ(info0.type(), mozilla::ipc::PrincipalInfo::TContentPrincipalInfo)
+ << "First principal must be content";
+ const mozilla::ipc::ContentPrincipalInfo& cInfo0 = data[0].principal();
+
+ nsAutoCString suffix0;
+ cInfo0.attrs().CreateSuffix(suffix0);
+
+ ASSERT_STREQ("^inBrowser=1", suffix0.get());
+ ASSERT_STREQ("https://scope_0.org", cInfo0.spec().get());
+ ASSERT_STREQ("https://scope_0.org", data[0].scope().get());
+ ASSERT_STREQ("currentWorkerURL 0", data[0].currentWorkerURL().get());
+ ASSERT_TRUE(data[0].currentWorkerHandlesFetch());
+ ASSERT_STREQ("cacheName 0", NS_ConvertUTF16toUTF8(data[0].cacheName()).get());
+ ASSERT_EQ(nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_IMPORTS,
+ data[0].updateViaCache());
+ ASSERT_EQ((int64_t)0, data[0].currentWorkerInstalledTime());
+ ASSERT_EQ((int64_t)0, data[0].currentWorkerActivatedTime());
+ ASSERT_EQ((int64_t)0, data[0].lastUpdateTime());
+
+ const mozilla::ipc::PrincipalInfo& info1 = data[1].principal();
+ ASSERT_EQ(info1.type(), mozilla::ipc::PrincipalInfo::TContentPrincipalInfo)
+ << "First principal must be content";
+ const mozilla::ipc::ContentPrincipalInfo& cInfo1 = data[1].principal();
+
+ nsAutoCString suffix1;
+ cInfo1.attrs().CreateSuffix(suffix1);
+
+ ASSERT_STREQ("", suffix1.get());
+ ASSERT_STREQ("https://scope_1.org", cInfo1.spec().get());
+ ASSERT_STREQ("https://scope_1.org", data[1].scope().get());
+ ASSERT_STREQ("currentWorkerURL 1", data[1].currentWorkerURL().get());
+ ASSERT_FALSE(data[1].currentWorkerHandlesFetch());
+ ASSERT_STREQ("cacheName 1", NS_ConvertUTF16toUTF8(data[1].cacheName()).get());
+ ASSERT_EQ(nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_IMPORTS,
+ data[1].updateViaCache());
+ ASSERT_EQ((int64_t)0, data[1].currentWorkerInstalledTime());
+ ASSERT_EQ((int64_t)0, data[1].currentWorkerActivatedTime());
+ ASSERT_EQ((int64_t)0, data[1].lastUpdateTime());
+}
+
+TEST(ServiceWorkerRegistrar, TestVersion6Migration)
+{
+ nsAutoCString buffer(
+ "6"
+ "\n");
+
+ buffer.AppendLiteral("^appId=123&inBrowser=1\n");
+ buffer.AppendLiteral("https://scope_0.org\ncurrentWorkerURL 0\n");
+ buffer.AppendLiteral(SERVICEWORKERREGISTRAR_TRUE "\n");
+ buffer.AppendLiteral("cacheName 0\n");
+ buffer.AppendInt(nsIRequest::LOAD_NORMAL, 16);
+ buffer.AppendLiteral("\n");
+ buffer.AppendLiteral(SERVICEWORKERREGISTRAR_TERMINATOR "\n");
+
+ buffer.AppendLiteral("\n");
+ buffer.AppendLiteral("https://scope_1.org\ncurrentWorkerURL 1\n");
+ buffer.AppendLiteral(SERVICEWORKERREGISTRAR_FALSE "\n");
+ buffer.AppendLiteral("cacheName 1\n");
+ buffer.AppendInt(nsIRequest::VALIDATE_ALWAYS, 16);
+ buffer.AppendLiteral("\n");
+ buffer.AppendLiteral(SERVICEWORKERREGISTRAR_TERMINATOR "\n");
+
+ ASSERT_TRUE(CreateFile(buffer))
+ << "CreateFile should not fail";
+
+ RefPtr<ServiceWorkerRegistrarTest> swr = new ServiceWorkerRegistrarTest;
+
+ nsresult rv = swr->TestReadData();
+ ASSERT_EQ(NS_OK, rv) << "ReadData() should not fail";
+
+ const nsTArray<ServiceWorkerRegistrationData>& data = swr->TestGetData();
+ ASSERT_EQ((uint32_t)2, data.Length()) << "2 entries should be found";
+
+ const mozilla::ipc::PrincipalInfo& info0 = data[0].principal();
+ ASSERT_EQ(info0.type(), mozilla::ipc::PrincipalInfo::TContentPrincipalInfo)
+ << "First principal must be content";
+ const mozilla::ipc::ContentPrincipalInfo& cInfo0 = data[0].principal();
+
+ nsAutoCString suffix0;
+ cInfo0.attrs().CreateSuffix(suffix0);
+
+ ASSERT_STREQ("^inBrowser=1", suffix0.get());
+ ASSERT_STREQ("https://scope_0.org", cInfo0.spec().get());
+ ASSERT_STREQ("https://scope_0.org", data[0].scope().get());
+ ASSERT_STREQ("currentWorkerURL 0", data[0].currentWorkerURL().get());
+ ASSERT_TRUE(data[0].currentWorkerHandlesFetch());
+ ASSERT_STREQ("cacheName 0", NS_ConvertUTF16toUTF8(data[0].cacheName()).get());
+ ASSERT_EQ(nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_ALL,
+ data[0].updateViaCache());
+ ASSERT_EQ((int64_t)0, data[0].currentWorkerInstalledTime());
+ ASSERT_EQ((int64_t)0, data[0].currentWorkerActivatedTime());
+ ASSERT_EQ((int64_t)0, data[0].lastUpdateTime());
+
+ const mozilla::ipc::PrincipalInfo& info1 = data[1].principal();
+ ASSERT_EQ(info1.type(), mozilla::ipc::PrincipalInfo::TContentPrincipalInfo)
+ << "First principal must be content";
+ const mozilla::ipc::ContentPrincipalInfo& cInfo1 = data[1].principal();
+
+ nsAutoCString suffix1;
+ cInfo1.attrs().CreateSuffix(suffix1);
+
+ ASSERT_STREQ("", suffix1.get());
+ ASSERT_STREQ("https://scope_1.org", cInfo1.spec().get());
+ ASSERT_STREQ("https://scope_1.org", data[1].scope().get());
+ ASSERT_STREQ("currentWorkerURL 1", data[1].currentWorkerURL().get());
+ ASSERT_FALSE(data[1].currentWorkerHandlesFetch());
+ ASSERT_STREQ("cacheName 1", NS_ConvertUTF16toUTF8(data[1].cacheName()).get());
+ ASSERT_EQ(nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_IMPORTS,
+ data[1].updateViaCache());
+ ASSERT_EQ((int64_t)0, data[1].currentWorkerInstalledTime());
+ ASSERT_EQ((int64_t)0, data[1].currentWorkerActivatedTime());
+ ASSERT_EQ((int64_t)0, data[1].lastUpdateTime());
+}
+
+TEST(ServiceWorkerRegistrar, TestVersion7Migration)
+{
+ nsAutoCString buffer(
+ "7"
+ "\n");
+
+ buffer.AppendLiteral("^appId=123&inBrowser=1\n");
+ buffer.AppendLiteral("https://scope_0.org\ncurrentWorkerURL 0\n");
+ buffer.AppendLiteral(SERVICEWORKERREGISTRAR_TRUE "\n");
+ buffer.AppendLiteral("cacheName 0\n");
+ buffer.AppendInt(nsIRequest::LOAD_NORMAL, 16);
+ buffer.AppendLiteral("\n");
+ buffer.AppendInt(0);
+ buffer.AppendLiteral("\n");
+ buffer.AppendInt(0);
+ buffer.AppendLiteral("\n");
+ buffer.AppendInt(0);
+ buffer.AppendLiteral("\n");
+ buffer.AppendLiteral(SERVICEWORKERREGISTRAR_TERMINATOR "\n");
+
+ buffer.AppendLiteral("\n");
+ buffer.AppendLiteral("https://scope_1.org\ncurrentWorkerURL 1\n");
+ buffer.AppendLiteral(SERVICEWORKERREGISTRAR_FALSE "\n");
+ buffer.AppendLiteral("cacheName 1\n");
+ buffer.AppendInt(nsIRequest::VALIDATE_ALWAYS, 16);
+ buffer.AppendLiteral("\n");
+ PRTime ts = PR_Now();
+ buffer.AppendInt(ts);
+ buffer.AppendLiteral("\n");
+ buffer.AppendInt(ts);
+ buffer.AppendLiteral("\n");
+ buffer.AppendInt(ts);
+ buffer.AppendLiteral("\n");
+ buffer.AppendLiteral(SERVICEWORKERREGISTRAR_TERMINATOR "\n");
+
+ ASSERT_TRUE(CreateFile(buffer))
+ << "CreateFile should not fail";
+
+ RefPtr<ServiceWorkerRegistrarTest> swr = new ServiceWorkerRegistrarTest;
+
+ nsresult rv = swr->TestReadData();
+ ASSERT_EQ(NS_OK, rv) << "ReadData() should not fail";
+
+ const nsTArray<ServiceWorkerRegistrationData>& data = swr->TestGetData();
+ ASSERT_EQ((uint32_t)2, data.Length()) << "2 entries should be found";
+
+ const mozilla::ipc::PrincipalInfo& info0 = data[0].principal();
+ ASSERT_EQ(info0.type(), mozilla::ipc::PrincipalInfo::TContentPrincipalInfo)
+ << "First principal must be content";
+ const mozilla::ipc::ContentPrincipalInfo& cInfo0 = data[0].principal();
+
+ nsAutoCString suffix0;
+ cInfo0.attrs().CreateSuffix(suffix0);
+
+ ASSERT_STREQ("^inBrowser=1", suffix0.get());
+ ASSERT_STREQ("https://scope_0.org", cInfo0.spec().get());
+ ASSERT_STREQ("https://scope_0.org", data[0].scope().get());
+ ASSERT_STREQ("currentWorkerURL 0", data[0].currentWorkerURL().get());
+ ASSERT_TRUE(data[0].currentWorkerHandlesFetch());
+ ASSERT_STREQ("cacheName 0", NS_ConvertUTF16toUTF8(data[0].cacheName()).get());
+ ASSERT_EQ(nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_ALL,
+ data[0].updateViaCache());
+ ASSERT_EQ((int64_t)0, data[0].currentWorkerInstalledTime());
+ ASSERT_EQ((int64_t)0, data[0].currentWorkerActivatedTime());
+ ASSERT_EQ((int64_t)0, data[0].lastUpdateTime());
+
+ const mozilla::ipc::PrincipalInfo& info1 = data[1].principal();
+ ASSERT_EQ(info1.type(), mozilla::ipc::PrincipalInfo::TContentPrincipalInfo)
+ << "First principal must be content";
+ const mozilla::ipc::ContentPrincipalInfo& cInfo1 = data[1].principal();
+
+ nsAutoCString suffix1;
+ cInfo1.attrs().CreateSuffix(suffix1);
+
+ ASSERT_STREQ("", suffix1.get());
+ ASSERT_STREQ("https://scope_1.org", cInfo1.spec().get());
+ ASSERT_STREQ("https://scope_1.org", data[1].scope().get());
+ ASSERT_STREQ("currentWorkerURL 1", data[1].currentWorkerURL().get());
+ ASSERT_FALSE(data[1].currentWorkerHandlesFetch());
+ ASSERT_STREQ("cacheName 1", NS_ConvertUTF16toUTF8(data[1].cacheName()).get());
+ ASSERT_EQ(nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_IMPORTS,
+ data[1].updateViaCache());
+ ASSERT_EQ((int64_t)ts, data[1].currentWorkerInstalledTime());
+ ASSERT_EQ((int64_t)ts, data[1].currentWorkerActivatedTime());
+ ASSERT_EQ((int64_t)ts, data[1].lastUpdateTime());
+}
+
+TEST(ServiceWorkerRegistrar, TestDedupeRead)
+{
+ nsAutoCString buffer(
+ "3"
+ "\n");
+
+ // unique entries
+ buffer.AppendLiteral("^inBrowser=1\n");
+ buffer.AppendLiteral(
+ "spec 0\nhttps://scope_0.org\ncurrentWorkerURL 0\ncacheName 0\n");
+ buffer.AppendLiteral(SERVICEWORKERREGISTRAR_TERMINATOR "\n");
+
+ buffer.AppendLiteral("\n");
+ buffer.AppendLiteral(
+ "spec 1\nhttps://scope_1.org\ncurrentWorkerURL 1\ncacheName 1\n");
+ buffer.AppendLiteral(SERVICEWORKERREGISTRAR_TERMINATOR "\n");
+
+ // dupe entries
+ buffer.AppendLiteral("^inBrowser=1\n");
+ buffer.AppendLiteral(
+ "spec 1\nhttps://scope_0.org\ncurrentWorkerURL 0\ncacheName 0\n");
+ buffer.AppendLiteral(SERVICEWORKERREGISTRAR_TERMINATOR "\n");
+
+ buffer.AppendLiteral("^inBrowser=1\n");
+ buffer.AppendLiteral(
+ "spec 2\nhttps://scope_0.org\ncurrentWorkerURL 0\ncacheName 0\n");
+ buffer.AppendLiteral(SERVICEWORKERREGISTRAR_TERMINATOR "\n");
+
+ buffer.AppendLiteral("\n");
+ buffer.AppendLiteral(
+ "spec 3\nhttps://scope_1.org\ncurrentWorkerURL 1\ncacheName 1\n");
+ buffer.AppendLiteral(SERVICEWORKERREGISTRAR_TERMINATOR "\n");
+
+ ASSERT_TRUE(CreateFile(buffer))
+ << "CreateFile should not fail";
+
+ RefPtr<ServiceWorkerRegistrarTest> swr = new ServiceWorkerRegistrarTest;
+
+ nsresult rv = swr->TestReadData();
+ ASSERT_EQ(NS_OK, rv) << "ReadData() should not fail";
+
+ const nsTArray<ServiceWorkerRegistrationData>& data = swr->TestGetData();
+ ASSERT_EQ((uint32_t)2, data.Length()) << "2 entries should be found";
+
+ const mozilla::ipc::PrincipalInfo& info0 = data[0].principal();
+ ASSERT_EQ(info0.type(), mozilla::ipc::PrincipalInfo::TContentPrincipalInfo)
+ << "First principal must be content";
+ const mozilla::ipc::ContentPrincipalInfo& cInfo0 = data[0].principal();
+
+ nsAutoCString suffix0;
+ cInfo0.attrs().CreateSuffix(suffix0);
+
+ ASSERT_STREQ("^inBrowser=1", suffix0.get());
+ ASSERT_STREQ("https://scope_0.org", cInfo0.spec().get());
+ ASSERT_STREQ("https://scope_0.org", data[0].scope().get());
+ ASSERT_STREQ("currentWorkerURL 0", data[0].currentWorkerURL().get());
+ ASSERT_EQ(true, data[0].currentWorkerHandlesFetch());
+ ASSERT_STREQ("cacheName 0", NS_ConvertUTF16toUTF8(data[0].cacheName()).get());
+ ASSERT_EQ(nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_IMPORTS,
+ data[0].updateViaCache());
+ ASSERT_EQ((int64_t)0, data[0].currentWorkerInstalledTime());
+ ASSERT_EQ((int64_t)0, data[0].currentWorkerActivatedTime());
+ ASSERT_EQ((int64_t)0, data[0].lastUpdateTime());
+
+ const mozilla::ipc::PrincipalInfo& info1 = data[1].principal();
+ ASSERT_EQ(info1.type(), mozilla::ipc::PrincipalInfo::TContentPrincipalInfo)
+ << "First principal must be content";
+ const mozilla::ipc::ContentPrincipalInfo& cInfo1 = data[1].principal();
+
+ nsAutoCString suffix1;
+ cInfo1.attrs().CreateSuffix(suffix1);
+
+ ASSERT_STREQ("", suffix1.get());
+ ASSERT_STREQ("https://scope_1.org", cInfo1.spec().get());
+ ASSERT_STREQ("https://scope_1.org", data[1].scope().get());
+ ASSERT_STREQ("currentWorkerURL 1", data[1].currentWorkerURL().get());
+ ASSERT_EQ(true, data[1].currentWorkerHandlesFetch());
+ ASSERT_STREQ("cacheName 1", NS_ConvertUTF16toUTF8(data[1].cacheName()).get());
+ ASSERT_EQ(nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_IMPORTS,
+ data[1].updateViaCache());
+ ASSERT_EQ((int64_t)0, data[1].currentWorkerInstalledTime());
+ ASSERT_EQ((int64_t)0, data[1].currentWorkerActivatedTime());
+ ASSERT_EQ((int64_t)0, data[1].lastUpdateTime());
+}
+
+TEST(ServiceWorkerRegistrar, TestDedupeWrite)
+{
+ {
+ RefPtr<ServiceWorkerRegistrarTest> swr = new ServiceWorkerRegistrarTest;
+
+ for (int i = 0; i < 2; ++i) {
+ ServiceWorkerRegistrationData reg;
+
+ reg.scope() = "https://scope_write.dedupe"_ns;
+ reg.currentWorkerURL() = nsPrintfCString("currentWorkerURL write %d", i);
+ reg.currentWorkerHandlesFetch() = true;
+ reg.cacheName() =
+ NS_ConvertUTF8toUTF16(nsPrintfCString("cacheName write %d", i));
+ reg.updateViaCache() =
+ nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_IMPORTS;
+
+ nsAutoCString spec;
+ spec.AppendPrintf("spec write dedupe/%d", i);
+
+ reg.principal() = mozilla::ipc::ContentPrincipalInfo(
+ mozilla::OriginAttributes(false), spec, spec, mozilla::Nothing(),
+ spec);
+
+ swr->TestRegisterServiceWorker(reg);
+ }
+
+ nsresult rv = swr->TestWriteData();
+ ASSERT_EQ(NS_OK, rv) << "WriteData() should not fail";
+ }
+
+ RefPtr<ServiceWorkerRegistrarTest> swr = new ServiceWorkerRegistrarTest;
+
+ nsresult rv = swr->TestReadData();
+ ASSERT_EQ(NS_OK, rv) << "ReadData() should not fail";
+
+ // Duplicate entries should be removed.
+ const nsTArray<ServiceWorkerRegistrationData>& data = swr->TestGetData();
+ ASSERT_EQ((uint32_t)1, data.Length()) << "1 entry should be found";
+
+ ASSERT_EQ(data[0].principal().type(),
+ mozilla::ipc::PrincipalInfo::TContentPrincipalInfo);
+ const mozilla::ipc::ContentPrincipalInfo& cInfo = data[0].principal();
+
+ mozilla::OriginAttributes attrs(false);
+ nsAutoCString suffix, expectSuffix;
+ attrs.CreateSuffix(expectSuffix);
+ cInfo.attrs().CreateSuffix(suffix);
+
+ // Last entry passed to RegisterServiceWorkerInternal() should overwrite
+ // previous values. So expect "1" in values here.
+ ASSERT_STREQ(expectSuffix.get(), suffix.get());
+ ASSERT_STREQ("https://scope_write.dedupe", cInfo.spec().get());
+ ASSERT_STREQ("https://scope_write.dedupe", data[0].scope().get());
+ ASSERT_STREQ("currentWorkerURL write 1", data[0].currentWorkerURL().get());
+ ASSERT_EQ(true, data[0].currentWorkerHandlesFetch());
+ ASSERT_STREQ("cacheName write 1",
+ NS_ConvertUTF16toUTF8(data[0].cacheName()).get());
+ ASSERT_EQ(nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_IMPORTS,
+ data[0].updateViaCache());
+ ASSERT_EQ((int64_t)0, data[0].currentWorkerInstalledTime());
+ ASSERT_EQ((int64_t)0, data[0].currentWorkerActivatedTime());
+ ASSERT_EQ((int64_t)0, data[0].lastUpdateTime());
+}
+
+int main(int argc, char** argv) {
+ ::testing::InitGoogleTest(&argc, argv);
+
+ int rv = RUN_ALL_TESTS();
+ return rv;
+}
diff --git a/dom/serviceworkers/test/gtest/moz.build b/dom/serviceworkers/test/gtest/moz.build
new file mode 100644
index 0000000000..99e2945332
--- /dev/null
+++ b/dom/serviceworkers/test/gtest/moz.build
@@ -0,0 +1,13 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, you can obtain one at http://mozilla.org/MPL/2.0/.
+
+UNIFIED_SOURCES = [
+ "TestReadWrite.cpp",
+]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+FINAL_LIBRARY = "xul-gtest"
diff --git a/dom/serviceworkers/test/gzip_redirect_worker.js b/dom/serviceworkers/test/gzip_redirect_worker.js
new file mode 100644
index 0000000000..b66df51478
--- /dev/null
+++ b/dom/serviceworkers/test/gzip_redirect_worker.js
@@ -0,0 +1,15 @@
+self.addEventListener("fetch", function(event) {
+ if (!event.request.url.endsWith("sw_clients/does_not_exist.html")) {
+ return;
+ }
+
+ event.respondWith(
+ new Response("", {
+ status: 301,
+ statusText: "Moved Permanently",
+ headers: {
+ Location: "refresher_compressed.html",
+ },
+ })
+ );
+});
diff --git a/dom/serviceworkers/test/header_checker.sjs b/dom/serviceworkers/test/header_checker.sjs
new file mode 100644
index 0000000000..7061041039
--- /dev/null
+++ b/dom/serviceworkers/test/header_checker.sjs
@@ -0,0 +1,9 @@
+function handleRequest(request, response) {
+ if (request.getHeader("Service-Worker") === "script") {
+ response.setStatusLine("1.1", 200, "OK");
+ response.setHeader("Content-Type", "text/javascript");
+ response.write("// empty");
+ } else {
+ response.setStatusLine("1.1", 404, "Not Found");
+ }
+}
diff --git a/dom/serviceworkers/test/hello.html b/dom/serviceworkers/test/hello.html
new file mode 100644
index 0000000000..97eb03c902
--- /dev/null
+++ b/dom/serviceworkers/test/hello.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ </head>
+ <body>
+ Hello.
+ </body>
+<html>
diff --git a/dom/serviceworkers/test/importscript.sjs b/dom/serviceworkers/test/importscript.sjs
new file mode 100644
index 0000000000..e075eadd87
--- /dev/null
+++ b/dom/serviceworkers/test/importscript.sjs
@@ -0,0 +1,11 @@
+function handleRequest(request, response) {
+ if (request.queryString == "clearcounter") {
+ setState("counter", "");
+ } else if (!getState("counter")) {
+ response.setHeader("Content-Type", "application/javascript", false);
+ response.write("callByScript();");
+ setState("counter", "1");
+ } else {
+ response.write("no cache no party!");
+ }
+}
diff --git a/dom/serviceworkers/test/importscript_worker.js b/dom/serviceworkers/test/importscript_worker.js
new file mode 100644
index 0000000000..6d639e792c
--- /dev/null
+++ b/dom/serviceworkers/test/importscript_worker.js
@@ -0,0 +1,46 @@
+var counter = 0;
+function callByScript() {
+ ++counter;
+}
+
+// Use multiple scripts in this load to verify we support that case correctly.
+// See bug 1249351 for a case where we broke this.
+importScripts("lorem_script.js", "importscript.sjs");
+
+importScripts("importscript.sjs");
+
+var missingScriptFailed = false;
+try {
+ importScripts(["there-is-nothing-here.js"]);
+} catch (e) {
+ missingScriptFailed = true;
+}
+
+onmessage = function(e) {
+ self.clients.matchAll().then(function(res) {
+ if (!res.length) {
+ dump("ERROR: no clients are currently controlled.\n");
+ }
+
+ if (!missingScriptFailed) {
+ res[0].postMessage("KO");
+ }
+
+ try {
+ // new unique script should fail
+ importScripts(["importscript.sjs?unique=true"]);
+ res[0].postMessage("KO");
+ return;
+ } catch (ex) {}
+
+ try {
+ // duplicate script previously offlined should succeed
+ importScripts(["importscript.sjs"]);
+ } catch (ex) {
+ res[0].postMessage("KO");
+ return;
+ }
+
+ res[0].postMessage(counter == 3 ? "OK" : "KO");
+ });
+};
diff --git a/dom/serviceworkers/test/install_event_error_worker.js b/dom/serviceworkers/test/install_event_error_worker.js
new file mode 100644
index 0000000000..e4208705b3
--- /dev/null
+++ b/dom/serviceworkers/test/install_event_error_worker.js
@@ -0,0 +1,9 @@
+// Worker that errors on receiving an install event.
+oninstall = function(e) {
+ e.waitUntil(
+ new Promise(function(resolve, reject) {
+ undefined.doSomething;
+ resolve();
+ })
+ );
+};
diff --git a/dom/serviceworkers/test/install_event_worker.js b/dom/serviceworkers/test/install_event_worker.js
new file mode 100644
index 0000000000..1f0815c8f0
--- /dev/null
+++ b/dom/serviceworkers/test/install_event_worker.js
@@ -0,0 +1,3 @@
+oninstall = function(e) {
+ dump("Got install event\n");
+};
diff --git a/dom/serviceworkers/test/intercepted_channel_process_swap_worker.js b/dom/serviceworkers/test/intercepted_channel_process_swap_worker.js
new file mode 100644
index 0000000000..ccc74bc895
--- /dev/null
+++ b/dom/serviceworkers/test/intercepted_channel_process_swap_worker.js
@@ -0,0 +1,7 @@
+onfetch = e => {
+ const url = new URL(e.request.url).searchParams.get("respondWith");
+
+ if (url) {
+ e.respondWith(fetch(url));
+ }
+};
diff --git a/dom/serviceworkers/test/isolated/README.md b/dom/serviceworkers/test/isolated/README.md
new file mode 100644
index 0000000000..2b462385af
--- /dev/null
+++ b/dom/serviceworkers/test/isolated/README.md
@@ -0,0 +1,19 @@
+This directory contains tests that are flaky when run with other tests
+but that we don't want to disable and where it's not trivial to make
+the tests not flaky at this time, but we have a plan to fix them via
+systemic fixes that are improving the codebase rather than hacking a
+test until it works.
+
+This directory and ugly hack structure needs to exist because of
+multi-e10s propagation races that will go away when we finish
+implementing the multi-e10s overhaul for ServiceWorkers. Most
+specifically, unregister() calls need to propagate across all
+content processes. There are fixes on bug 1318142, but they're
+ugly and complicate things.
+
+Specific test notes and rationalizations:
+- multi-e10s-update: This test relies on there being no registrations
+ existing at its start. The preceding test that induces the breakage
+ (`browser_force_refresh.js`) was made to clean itself up, but the
+ unregister() race issue is not easily/cleanly hacked around and this
+ test will itself become moot when the multi-e10s changes land.
diff --git a/dom/serviceworkers/test/isolated/multi-e10s-update/browser.ini b/dom/serviceworkers/test/isolated/multi-e10s-update/browser.ini
new file mode 100644
index 0000000000..bb913e2583
--- /dev/null
+++ b/dom/serviceworkers/test/isolated/multi-e10s-update/browser.ini
@@ -0,0 +1,7 @@
+[DEFAULT]
+support-files =
+ file_multie10s_update.html
+ server_multie10s_update.sjs
+
+[browser_multie10s_update.js]
+skip-if = true # bug 1429794 is to re-enable
diff --git a/dom/serviceworkers/test/isolated/multi-e10s-update/browser_multie10s_update.js b/dom/serviceworkers/test/isolated/multi-e10s-update/browser_multie10s_update.js
new file mode 100644
index 0000000000..4b9a2b9ca4
--- /dev/null
+++ b/dom/serviceworkers/test/isolated/multi-e10s-update/browser_multie10s_update.js
@@ -0,0 +1,143 @@
+"use strict";
+
+// Testing if 2 child processes are correctly managed when they both try to do
+// an SW update.
+
+const BASE_URI =
+ "http://mochi.test:8888/browser/dom/serviceworkers/test/isolated/multi-e10s-update/";
+
+add_task(async function test_update() {
+ info("Setting the prefs to having multi-e10s enabled");
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.ipc.processCount", 4],
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ ],
+ });
+
+ let url = BASE_URI + "file_multie10s_update.html";
+
+ info("Creating the first tab...");
+ let tab1 = BrowserTestUtils.addTab(gBrowser, url);
+ let browser1 = gBrowser.getBrowserForTab(tab1);
+ await BrowserTestUtils.browserLoaded(browser1);
+
+ info("Creating the second tab...");
+ let tab2 = BrowserTestUtils.addTab(gBrowser, url);
+ let browser2 = gBrowser.getBrowserForTab(tab2);
+ await BrowserTestUtils.browserLoaded(browser2);
+
+ let sw = BASE_URI + "server_multie10s_update.sjs";
+
+ info("Let's make sure there are no existing registrations...");
+ let existingCount = await SpecialPowers.spawn(browser1, [], async function() {
+ const regs = await content.navigator.serviceWorker.getRegistrations();
+ return regs.length;
+ });
+ is(existingCount, 0, "Previous tests should have cleaned up!");
+
+ info("Let's start the test...");
+ /* eslint-disable no-shadow */
+ let status = await SpecialPowers.spawn(browser1, [sw], function(url) {
+ // Let the SW be served immediately once by triggering a relase immediately.
+ // We don't need to await this. We do this from a frame script because
+ // it has fetch.
+ content.fetch(url + "?release");
+
+ // Registration of the SW
+ return (
+ content.navigator.serviceWorker
+ .register(url)
+
+ // Activation
+ .then(function(r) {
+ content.registration = r;
+ return new content.window.Promise(resolve => {
+ let worker = r.installing;
+ worker.addEventListener("statechange", () => {
+ if (worker.state === "installed") {
+ resolve(true);
+ }
+ });
+ });
+ })
+
+ // Waiting for the result.
+ .then(() => {
+ return new content.window.Promise(resolveResults => {
+ // Once both updates have been issued and a single update has failed, we
+ // can tell the .sjs to release a single copy of the SW script.
+ let updateCount = 0;
+ const uc = new content.window.BroadcastChannel("update");
+ // This promise tracks the updates tally.
+ const updatesIssued = new Promise(resolveUpdatesIssued => {
+ uc.onmessage = function(e) {
+ updateCount++;
+ console.log("got update() number", updateCount);
+ if (updateCount === 2) {
+ resolveUpdatesIssued();
+ }
+ };
+ });
+
+ let results = [];
+ const rc = new content.window.BroadcastChannel("result");
+ // This promise resolves when an update has failed.
+ const oneFailed = new Promise(resolveOneFailed => {
+ rc.onmessage = function(e) {
+ console.log("got result", e.data);
+ results.push(e.data);
+ if (e.data === 1) {
+ resolveOneFailed();
+ }
+ if (results.length != 2) {
+ return;
+ }
+
+ resolveResults(results[0] + results[1]);
+ };
+ });
+
+ Promise.all([updatesIssued, oneFailed]).then(() => {
+ console.log("releasing update");
+ content.fetch(url + "?release").catch(ex => {
+ console.error("problem releasing:", ex);
+ });
+ });
+
+ // Let's inform the tabs.
+ const sc = new content.window.BroadcastChannel("start");
+ sc.postMessage("go");
+ });
+ })
+ );
+ });
+ /* eslint-enable no-shadow */
+
+ if (status == 0) {
+ ok(false, "both succeeded. This is wrong.");
+ } else if (status == 1) {
+ ok(true, "one succeded, one failed. This is good.");
+ } else {
+ ok(false, "both failed. This is definitely wrong.");
+ }
+
+ // let's clean up the registration and get the fetch count. The count
+ // should be 1 for the initial fetch and 1 for the update.
+ /* eslint-disable no-shadow */
+ const count = await SpecialPowers.spawn(browser1, [sw], async function(url) {
+ // We stored the registration on the frame script's wrapper, hence directly
+ // accesss content without using wrappedJSObject.
+ await content.registration.unregister();
+ const { count } = await content
+ .fetch(url + "?get-and-clear-count")
+ .then(r => r.json());
+ return count;
+ });
+ /* eslint-enable no-shadow */
+ is(count, 2, "SW should have been fetched only twice");
+
+ BrowserTestUtils.removeTab(tab1);
+ BrowserTestUtils.removeTab(tab2);
+});
diff --git a/dom/serviceworkers/test/isolated/multi-e10s-update/file_multie10s_update.html b/dom/serviceworkers/test/isolated/multi-e10s-update/file_multie10s_update.html
new file mode 100644
index 0000000000..d611b01b59
--- /dev/null
+++ b/dom/serviceworkers/test/isolated/multi-e10s-update/file_multie10s_update.html
@@ -0,0 +1,40 @@
+<html>
+<body>
+<script>
+
+var bc = new BroadcastChannel('start');
+bc.onmessage = function(e) {
+ // This message is not for us.
+ if (e.data != 'go') {
+ return;
+ }
+
+ // It can happen that we don't have the registrations yet. Let's try with a
+ // timeout.
+ function proceed() {
+ return navigator.serviceWorker.getRegistrations().then(regs => {
+ if (!regs.length) {
+ setTimeout(proceed, 200);
+ return;
+ }
+
+ bc = new BroadcastChannel('result');
+ regs[0].update().then(() => {
+ bc.postMessage(0);
+ }, () => {
+ bc.postMessage(1);
+ });
+
+ // Tell the coordinating frame script that we've kicked off our update
+ // call so that the SW script can be released once both instances of us
+ // have triggered update() and 1 has failed.
+ const blockingChannel = new BroadcastChannel('update');
+ blockingChannel.postMessage(true);
+ });
+ }
+
+ proceed();
+}
+</script>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/isolated/multi-e10s-update/server_multie10s_update.sjs b/dom/serviceworkers/test/isolated/multi-e10s-update/server_multie10s_update.sjs
new file mode 100644
index 0000000000..154f8a3ecd
--- /dev/null
+++ b/dom/serviceworkers/test/isolated/multi-e10s-update/server_multie10s_update.sjs
@@ -0,0 +1,100 @@
+// stolen from file_blocked_script.sjs
+function setGlobalState(data, key) {
+ x = {
+ data,
+ QueryInterface(iid) {
+ return this;
+ },
+ };
+ x.wrappedJSObject = x;
+ setObjectState(key, x);
+}
+
+function getGlobalState(key) {
+ var data;
+ getObjectState(key, function(x) {
+ data = x && x.wrappedJSObject.data;
+ });
+ return data;
+}
+
+function completeBlockingRequest(response) {
+ response.write("42");
+ response.finish();
+}
+
+// This stores the response that's currently blocking, or true if the release
+// got here before the blocking request.
+const BLOCKING_KEY = "multie10s-update-release";
+// This tracks the number of blocking requests we received up to this point in
+// time. This value will be cleared when fetched. It's on the caller to make
+// sure that all the blocking requests that might occurr have already occurred.
+const COUNT_KEY = "multie10s-update-count";
+
+/**
+ * Serve a request that will only be completed when the ?release variant of this
+ * .sjs is fetched. This allows us to avoid using a timer, which slows down the
+ * tests and is brittle under slow hardware.
+ */
+function handleBlockingRequest(request, response) {
+ response.processAsync();
+ response.setHeader("Content-Type", "application/javascript", false);
+
+ const existingCount = getGlobalState(COUNT_KEY) || 0;
+ setGlobalState(existingCount + 1, COUNT_KEY);
+
+ const alreadyReleased = getGlobalState(BLOCKING_KEY);
+ if (alreadyReleased === true) {
+ completeBlockingRequest(response);
+ setGlobalState(null, BLOCKING_KEY);
+ } else if (alreadyReleased) {
+ // If we've got another response stacked up, this means something is wrong
+ // with the test. The count mechanism will detect this, so just let this
+ // one through so we fail fast rather than hanging.
+ dump("we got multiple blocking requests stacked up!!\n");
+ completeBlockingRequest(response);
+ } else {
+ setGlobalState(response, BLOCKING_KEY);
+ }
+}
+
+function handleReleaseRequest(request, response) {
+ const blockingResponse = getGlobalState(BLOCKING_KEY);
+ if (blockingResponse) {
+ completeBlockingRequest(blockingResponse);
+ setGlobalState(null, BLOCKING_KEY);
+ } else {
+ setGlobalState(true, BLOCKING_KEY);
+ }
+
+ response.setHeader("Content-Type", "application/json", false);
+ response.write(JSON.stringify({ released: true }));
+}
+
+function handleCountRequest(request, response) {
+ const count = getGlobalState(COUNT_KEY) || 0;
+ // --verify requires that we clear this so the test can be re-run.
+ setGlobalState(0, COUNT_KEY);
+
+ response.setHeader("Content-Type", "application/json", false);
+ response.write(JSON.stringify({ count }));
+}
+
+Components.utils.importGlobalProperties(["URLSearchParams"]);
+function handleRequest(request, response) {
+ dump(
+ "server_multie10s_update.sjs: processing request for " +
+ request.path +
+ "?" +
+ request.queryString +
+ "\n"
+ );
+ const query = new URLSearchParams(request.queryString);
+ if (query.has("release")) {
+ handleReleaseRequest(request, response);
+ } else if (query.has("get-and-clear-count")) {
+ handleCountRequest(request, response);
+ } else {
+ handleBlockingRequest(request, response);
+ }
+}
diff --git a/dom/serviceworkers/test/lazy_worker.js b/dom/serviceworkers/test/lazy_worker.js
new file mode 100644
index 0000000000..08a0ec13f7
--- /dev/null
+++ b/dom/serviceworkers/test/lazy_worker.js
@@ -0,0 +1,8 @@
+onactivate = function(event) {
+ var promise = new Promise(function(res) {
+ setTimeout(function() {
+ res();
+ }, 500);
+ });
+ event.waitUntil(promise);
+};
diff --git a/dom/serviceworkers/test/lorem_script.js b/dom/serviceworkers/test/lorem_script.js
new file mode 100644
index 0000000000..bc8f3c8085
--- /dev/null
+++ b/dom/serviceworkers/test/lorem_script.js
@@ -0,0 +1,8 @@
+var lorem_str = `
+Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
+incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis
+nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
+consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum
+dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident,
+sunt in culpa qui officia deserunt mollit anim id est laborum.
+`;
diff --git a/dom/serviceworkers/test/match_all_advanced_worker.js b/dom/serviceworkers/test/match_all_advanced_worker.js
new file mode 100644
index 0000000000..d1bc5ab323
--- /dev/null
+++ b/dom/serviceworkers/test/match_all_advanced_worker.js
@@ -0,0 +1,5 @@
+onmessage = function(e) {
+ self.clients.matchAll().then(function(clients) {
+ e.source.postMessage(clients.length);
+ });
+};
diff --git a/dom/serviceworkers/test/match_all_client/match_all_client_id.html b/dom/serviceworkers/test/match_all_client/match_all_client_id.html
new file mode 100644
index 0000000000..7ac6fc9d05
--- /dev/null
+++ b/dom/serviceworkers/test/match_all_client/match_all_client_id.html
@@ -0,0 +1,31 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1139425 - controlled page</title>
+<script class="testbody" type="text/javascript">
+ var testWindow = parent;
+ if (opener) {
+ testWindow = opener;
+ }
+
+ window.onload = function() {
+ navigator.serviceWorker.ready.then(function(swr) {
+ swr.active.postMessage("Start");
+ });
+ }
+
+ navigator.serviceWorker.onmessage = function(msg) {
+ // worker message;
+ testWindow.postMessage(msg.data, "*");
+ window.close();
+ };
+</script>
+
+</head>
+<body>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/match_all_client_id_worker.js b/dom/serviceworkers/test/match_all_client_id_worker.js
new file mode 100644
index 0000000000..e8e51a00e7
--- /dev/null
+++ b/dom/serviceworkers/test/match_all_client_id_worker.js
@@ -0,0 +1,28 @@
+onmessage = function(e) {
+ dump("MatchAllClientIdWorker:" + e.data + "\n");
+ var id = [];
+ var iterations = 5;
+ var counter = 0;
+
+ for (var i = 0; i < iterations; i++) {
+ self.clients.matchAll().then(function(res) {
+ if (!res.length) {
+ dump("ERROR: no clients are currently controlled.\n");
+ }
+
+ client = res[0];
+ id[counter] = client.id;
+ counter++;
+ if (counter >= iterations) {
+ var response = true;
+ for (var index = 1; index < iterations; index++) {
+ if (id[0] != id[index]) {
+ response = false;
+ break;
+ }
+ }
+ client.postMessage(response);
+ }
+ });
+ }
+};
diff --git a/dom/serviceworkers/test/match_all_clients/match_all_controlled.html b/dom/serviceworkers/test/match_all_clients/match_all_controlled.html
new file mode 100644
index 0000000000..35f064815d
--- /dev/null
+++ b/dom/serviceworkers/test/match_all_clients/match_all_controlled.html
@@ -0,0 +1,83 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1058311 - controlled page</title>
+<script class="testbody" type="text/javascript">
+ var re = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
+ var frameType = "none";
+ var testWindow = parent;
+
+ if (parent != window) {
+ frameType = "nested";
+ } else if (opener) {
+ frameType = "auxiliary";
+ testWindow = opener;
+ } else if (parent == window) {
+ frameType = "top-level";
+ } else {
+ postResult(false, "Unexpected frameType");
+ }
+
+ window.onload = function() {
+ navigator.serviceWorker.ready.then(function(swr) {
+ // Send a message to our SW that will cause it to do clients.matchAll()
+ // and send a message *to each client about themselves* (rather than
+ // replying directly to us with all the clients it finds).
+ swr.active.postMessage("Start");
+ });
+ }
+
+ function postResult(result, msg) {
+ response = {
+ result,
+ message: msg
+ };
+
+ testWindow.postMessage(response, "*");
+ }
+
+ navigator.serviceWorker.onmessage = async function(msg) {
+ // ## Verify the contents of the SW's serialized rep of our client info.
+ // Clients are opaque identifiers at a spec level, but we want to verify
+ // that they are UUID's *without wrapping "{}" characters*.
+ result = re.test(msg.data.id);
+ postResult(result, "Client id test");
+
+ result = msg.data.url == window.location;
+ postResult(result, "Client url test");
+
+ result = msg.data.visibilityState === document.visibilityState;
+ postResult(result, "Client visibility test. expected=" +document.visibilityState);
+
+ result = msg.data.focused === document.hasFocus();
+ postResult(result, "Client focus test. expected=" + document.hasFocus());
+
+ result = msg.data.frameType === frameType;
+ postResult(result, "Client frameType test. expected=" + frameType);
+
+ result = msg.data.type === "window";
+ postResult(result, "Client type test. expected=window");
+
+ // ## Verify the FetchEvent.clientId
+ // In bug 1446225 it turned out we provided UUID's wrapped with {}'s. We
+ // now also get coverage by forcing our clients.get() to forbid UUIDs
+ // with that form.
+
+ const clientIdResp = await fetch('clientId');
+ const fetchClientId = await clientIdResp.text();
+ result = re.test(fetchClientId);
+ postResult(result, "Fetch clientId test");
+
+ postResult(true, "DONE");
+ window.close();
+ };
+</script>
+
+</head>
+<body>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/match_all_properties_worker.js b/dom/serviceworkers/test/match_all_properties_worker.js
new file mode 100644
index 0000000000..84156c93e0
--- /dev/null
+++ b/dom/serviceworkers/test/match_all_properties_worker.js
@@ -0,0 +1,28 @@
+onfetch = function(e) {
+ if (/\/clientId$/.test(e.request.url)) {
+ e.respondWith(new Response(e.clientId));
+ return;
+ }
+};
+
+onmessage = function(e) {
+ dump("MatchAllPropertiesWorker:" + e.data + "\n");
+ self.clients.matchAll().then(function(res) {
+ if (!res.length) {
+ dump("ERROR: no clients are currently controlled.\n");
+ }
+
+ for (i = 0; i < res.length; i++) {
+ client = res[i];
+ response = {
+ type: client.type,
+ id: client.id,
+ url: client.url,
+ visibilityState: client.visibilityState,
+ focused: client.focused,
+ frameType: client.frameType,
+ };
+ client.postMessage(response);
+ }
+ });
+};
diff --git a/dom/serviceworkers/test/match_all_worker.js b/dom/serviceworkers/test/match_all_worker.js
new file mode 100644
index 0000000000..3eddbe1191
--- /dev/null
+++ b/dom/serviceworkers/test/match_all_worker.js
@@ -0,0 +1,10 @@
+function loop() {
+ self.clients.matchAll().then(function(result) {
+ setTimeout(loop, 0);
+ });
+}
+
+onactivate = function(e) {
+ // spam matchAll until the worker is closed.
+ loop();
+};
diff --git a/dom/serviceworkers/test/message_posting_worker.js b/dom/serviceworkers/test/message_posting_worker.js
new file mode 100644
index 0000000000..26db997759
--- /dev/null
+++ b/dom/serviceworkers/test/message_posting_worker.js
@@ -0,0 +1,8 @@
+onmessage = function(e) {
+ self.clients.matchAll().then(function(res) {
+ if (!res.length) {
+ dump("ERROR: no clients are currently controlled.\n");
+ }
+ res[0].postMessage(e.data);
+ });
+};
diff --git a/dom/serviceworkers/test/message_receiver.html b/dom/serviceworkers/test/message_receiver.html
new file mode 100644
index 0000000000..82cb587c72
--- /dev/null
+++ b/dom/serviceworkers/test/message_receiver.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<script>
+ navigator.serviceWorker.onmessage = function(e) {
+ window.parent.postMessage(e.data, "*");
+ };
+</script>
diff --git a/dom/serviceworkers/test/mochitest-common.ini b/dom/serviceworkers/test/mochitest-common.ini
new file mode 100644
index 0000000000..280273a7b4
--- /dev/null
+++ b/dom/serviceworkers/test/mochitest-common.ini
@@ -0,0 +1,347 @@
+[DEFAULT]
+tags = condprof
+support-files =
+ abrupt_completion_worker.js
+ worker.js
+ worker2.js
+ worker3.js
+ fetch_event_worker.js
+ parse_error_worker.js
+ activate_event_error_worker.js
+ install_event_worker.js
+ install_event_error_worker.js
+ simpleregister/index.html
+ simpleregister/ready.html
+ controller/index.html
+ unregister/index.html
+ unregister/unregister.html
+ workerUpdate/update.html
+ sw_clients/simple.html
+ sw_clients/service_worker_controlled.html
+ match_all_worker.js
+ match_all_advanced_worker.js
+ worker_unregister.js
+ worker_update.js
+ message_posting_worker.js
+ fetch/index.html
+ fetch/fetch_worker_script.js
+ fetch/fetch_tests.js
+ fetch/deliver-gzip.sjs
+ fetch/redirect.sjs
+ fetch/real-file.txt
+ fetch/cookie/cookie_test.js
+ fetch/cookie/register.html
+ fetch/cookie/unregister.html
+ fetch/hsts/hsts_test.js
+ fetch/hsts/embedder.html
+ fetch/hsts/image.html
+ fetch/hsts/image-20px.png
+ fetch/hsts/image-40px.png
+ fetch/hsts/realindex.html
+ fetch/hsts/register.html
+ fetch/hsts/register.html^headers^
+ fetch/hsts/unregister.html
+ fetch/https/index.html
+ fetch/https/register.html
+ fetch/https/unregister.html
+ fetch/https/https_test.js
+ fetch/https/clonedresponse/index.html
+ fetch/https/clonedresponse/register.html
+ fetch/https/clonedresponse/unregister.html
+ fetch/https/clonedresponse/https_test.js
+ fetch/imagecache/image-20px.png
+ fetch/imagecache/image-40px.png
+ fetch/imagecache/imagecache_test.js
+ fetch/imagecache/index.html
+ fetch/imagecache/postmortem.html
+ fetch/imagecache/register.html
+ fetch/imagecache/unregister.html
+ fetch/imagecache-maxage/index.html
+ fetch/imagecache-maxage/image-20px.png
+ fetch/imagecache-maxage/image-40px.png
+ fetch/imagecache-maxage/maxage_test.js
+ fetch/imagecache-maxage/register.html
+ fetch/imagecache-maxage/unregister.html
+ fetch/importscript-mixedcontent/register.html
+ fetch/importscript-mixedcontent/unregister.html
+ fetch/importscript-mixedcontent/https_test.js
+ fetch/interrupt.sjs
+ fetch/origin/index.sjs
+ fetch/origin/index-to-https.sjs
+ fetch/origin/realindex.html
+ fetch/origin/realindex.html^headers^
+ fetch/origin/register.html
+ fetch/origin/unregister.html
+ fetch/origin/origin_test.js
+ fetch/origin/https/index-https.sjs
+ fetch/origin/https/realindex.html
+ fetch/origin/https/realindex.html^headers^
+ fetch/origin/https/register.html
+ fetch/origin/https/unregister.html
+ fetch/origin/https/origin_test.js
+ fetch/requesturl/index.html
+ fetch/requesturl/redirect.sjs
+ fetch/requesturl/redirector.html
+ fetch/requesturl/register.html
+ fetch/requesturl/requesturl_test.js
+ fetch/requesturl/secret.html
+ fetch/requesturl/unregister.html
+ fetch/sandbox/index.html
+ fetch/sandbox/intercepted_index.html
+ fetch/sandbox/register.html
+ fetch/sandbox/unregister.html
+ fetch/sandbox/sandbox_test.js
+ fetch/upgrade-insecure/upgrade-insecure_test.js
+ fetch/upgrade-insecure/embedder.html
+ fetch/upgrade-insecure/embedder.html^headers^
+ fetch/upgrade-insecure/image.html
+ fetch/upgrade-insecure/image-20px.png
+ fetch/upgrade-insecure/image-40px.png
+ fetch/upgrade-insecure/realindex.html
+ fetch/upgrade-insecure/register.html
+ fetch/upgrade-insecure/unregister.html
+ match_all_properties_worker.js
+ match_all_clients/match_all_controlled.html
+ test_serviceworker_interfaces.js
+ serviceworker_wrapper.js
+ message_receiver.html
+ serviceworker_not_sharedworker.js
+ match_all_client/match_all_client_id.html
+ match_all_client_id_worker.js
+ source_message_posting_worker.js
+ scope/scope_worker.js
+ redirect_serviceworker.sjs
+ importscript.sjs
+ importscript_worker.js
+ bug1151916_worker.js
+ bug1151916_driver.html
+ bug1240436_worker.js
+ notificationclick.html
+ notificationclick-otherwindow.html
+ notificationclick.js
+ notificationclick_focus.html
+ notificationclick_focus.js
+ notificationclose.html
+ notificationclose.js
+ worker_updatefoundevent.js
+ worker_updatefoundevent2.js
+ updatefoundevent.html
+ empty.html
+ empty.js
+ notification_constructor_error.js
+ notification_get_sw.js
+ notification/register.html
+ notification/unregister.html
+ notification/listener.html
+ notification_alt/register.html
+ notification_alt/unregister.html
+ sanitize/frame.html
+ sanitize/register.html
+ sanitize/example_check_and_unregister.html
+ sanitize_worker.js
+ streamfilter_server.sjs
+ streamfilter_worker.js
+ swa/worker_scope_different.js
+ swa/worker_scope_different.js^headers^
+ swa/worker_scope_different2.js
+ swa/worker_scope_different2.js^headers^
+ swa/worker_scope_precise.js
+ swa/worker_scope_precise.js^headers^
+ swa/worker_scope_too_deep.js
+ swa/worker_scope_too_deep.js^headers^
+ swa/worker_scope_too_narrow.js
+ swa/worker_scope_too_narrow.js^headers^
+ claim_oninstall_worker.js
+ claim_worker_1.js
+ claim_worker_2.js
+ claim_clients/client.html
+ force_refresh_worker.js
+ sw_clients/refresher.html
+ sw_clients/refresher_compressed.html
+ sw_clients/refresher_compressed.html^headers^
+ sw_clients/refresher_cached.html
+ sw_clients/refresher_cached_compressed.html
+ sw_clients/refresher_cached_compressed.html^headers^
+ strict_mode_warning.js
+ skip_waiting_installed_worker.js
+ skip_waiting_scope/index.html
+ thirdparty/iframe1.html
+ thirdparty/iframe2.html
+ thirdparty/register.html
+ thirdparty/unregister.html
+ thirdparty/sw.js
+ thirdparty/worker.js
+ register_https.html
+ gzip_redirect_worker.js
+ sw_clients/navigator.html
+ eval_worker.js
+ test_eval_allowed.html^headers^
+ opaque_intercept_worker.js
+ notify_loaded.js
+ fetch/plugin/worker.js
+ fetch/plugin/plugins.html
+ eventsource/*
+ sw_clients/file_blob_upload_frame.html
+ redirect_post.sjs
+ xslt_worker.js
+ xslt/*
+ unresolved_fetch_worker.js
+ header_checker.sjs
+ openWindow_worker.js
+ redirect.sjs
+ open_window/client.sjs
+ lorem_script.js
+ file_blob_response_worker.js
+ file_js_cache_cleanup.js
+ file_js_cache.html
+ file_js_cache_with_sri.html
+ file_js_cache.js
+ file_js_cache_save_after_load.html
+ file_js_cache_save_after_load.js
+ file_js_cache_syntax_error.html
+ file_js_cache_syntax_error.js
+ !/dom/security/test/cors/file_CrossSiteXHR_server.sjs
+ !/dom/notification/test/mochitest/MockServices.js
+ !/dom/notification/test/mochitest/NotificationTest.js
+ blocking_install_event_worker.js
+ sw_bad_mime_type.js
+ sw_bad_mime_type.js^headers^
+ error_reporting_helpers.js
+ fetch.js
+ hello.html
+ create_another_sharedWorker.html
+ sharedWorker_fetch.js
+ async_waituntil_worker.js
+ lazy_worker.js
+ nofetch_handler_worker.js
+ service_worker.js
+ service_worker_client.html
+ utils.js
+ sw_storage_not_allow.js
+ update_worker.sjs
+ self_update_worker.sjs
+ !/dom/events/test/event_leak_utils.js
+ onmessageerror_worker.js
+
+[test_abrupt_completion.html]
+skip-if =
+ os == 'linux' #Bug 1615164
+ win10_2004 # Bug 1615164
+[test_bug1151916.html]
+[test_bug1240436.html]
+[test_bug1408734.html]
+[test_claim.html]
+[test_claim_oninstall.html]
+[test_controller.html]
+[test_cross_origin_url_after_redirect.html]
+[test_devtools_bypass_serviceworker.html]
+[test_empty_serviceworker.html]
+[test_error_reporting.html]
+skip-if = serviceworker_e10s
+[test_escapedSlashes.html]
+[test_eval_allowed.html]
+[test_event_listener_leaks.html]
+skip-if = (os == "win" && processor == "aarch64") #bug 1535784
+[test_fetch_event.html]
+skip-if = debug # Bug 1262224
+[test_fetch_event_with_thirdpartypref.html]
+skip-if = debug # Bug 1262224
+[test_fetch_integrity.html]
+skip-if = serviceworker_e10s
+support-files = console_monitor.js
+[test_file_blob_response.html]
+[test_file_blob_upload.html]
+[test_force_refresh.html]
+[test_gzip_redirect.html]
+[test_hsts_upgrade_intercept.html]
+skip-if =
+ win10_2004 && !debug # Bug 1717091
+ win11_2009 && !debug # Bug 1797751
+ os == "linux" && bits == 64 && debug # Bug 1749068
+ apple_catalina && !debug # Bug 1717091
+scheme = https
+[test_imagecache.html]
+[test_imagecache_max_age.html]
+skip-if =
+ os == 'linux' && bits == 64 && !debug && asan && os_version == '18.04' # Bug 1585668
+[test_importscript.html]
+[test_install_event.html]
+[test_install_event_gc.html]
+skip-if = xorigin # JavaScript error: http://mochi.xorigin-test:8888/tests/SimpleTest/TestRunner.js, line 157: SecurityError: Permission denied to access property "wrappedJSObject" on cross-origin object
+[test_installation_simple.html]
+[test_match_all.html]
+[test_match_all_advanced.html]
+[test_match_all_client_id.html]
+skip-if = toolkit == 'android' && !is_fennec
+[test_match_all_client_properties.html]
+skip-if = toolkit == 'android' && !is_fennec
+[test_navigationPreload_disable_crash.html]
+scheme = https
+skip-if =
+ os == "linux" && bits == 64 && debug # Bug 1749068
+[test_navigator.html]
+[test_not_intercept_plugin.html]
+skip-if = serviceworker_e10s # leaks InterceptedHttpChannel and others things
+[test_notification_constructor_error.html]
+[test_notification_get.html]
+skip-if = xorigin # Bug 1792790
+[test_notification_openWindow.html]
+skip-if =
+ toolkit == 'android' && !is_fennec # Bug 1620052
+ xorigin # JavaScript error: http://mochi.xorigin-test:8888/tests/SimpleTest/TestRunner.js, line 157: SecurityError: Permission denied to access property "wrappedJSObject" on cross-origin object
+support-files = notification_openWindow_worker.js file_notification_openWindow.html
+tags = openwindow
+[test_notificationclick.html]
+skip-if = xorigin # Bug 1792790
+[test_notificationclick_focus.html]
+skip-if = xorigin # Bug 1792790
+[test_notificationclick-otherwindow.html]
+skip-if = xorigin # Bug 1792790
+[test_notificationclose.html]
+skip-if = xorigin # Bug 1792790
+[test_onmessageerror.html]
+skip-if = xorigin # Hangs with no error log
+[test_opaque_intercept.html]
+[test_origin_after_redirect.html]
+[test_origin_after_redirect_cached.html]
+[test_origin_after_redirect_to_https.html]
+[test_origin_after_redirect_to_https_cached.html]
+[test_post_message.html]
+[test_post_message_advanced.html]
+[test_post_message_source.html]
+[test_register_base.html]
+[test_register_https_in_http.html]
+[test_sandbox_intercept.html]
+[test_scopes.html]
+[test_script_loader_intercepted_js_cache.html]
+skip-if = serviceworker_e10s
+[test_sanitize.html]
+[test_serviceworker.html]
+[test_service_worker_allowed.html]
+[test_serviceworker_header.html]
+[test_serviceworker_interfaces.html]
+[test_serviceworker_not_sharedworker.html]
+[test_skip_waiting.html]
+[test_streamfilter.html]
+[test_strict_mode_warning.html]
+[test_third_party_iframes.html]
+support-files =
+ window_party_iframes.html
+[test_unregister.html]
+[test_unresolved_fetch_interception.html]
+skip-if = verify
+ serviceworker_e10s
+[test_workerUnregister.html]
+[test_workerUpdate.html]
+[test_workerupdatefoundevent.html]
+[test_xslt.html]
+[test_async_waituntil.html]
+[test_worker_reference_gc_timeout.html]
+[test_nofetch_handler.html]
+[test_bad_script_cache.html]
+[test_file_upload.html]
+skip-if = toolkit == 'android' #Bug 1430182
+support-files = script_file_upload.js sw_file_upload.js server_file_upload.sjs
+[test_self_update_worker.html]
+skip-if = serviceworker_e10s
+ (toolkit == 'android' && !is_fennec)
diff --git a/dom/serviceworkers/test/mochitest-dFPI.ini b/dom/serviceworkers/test/mochitest-dFPI.ini
new file mode 100644
index 0000000000..37d6171837
--- /dev/null
+++ b/dom/serviceworkers/test/mochitest-dFPI.ini
@@ -0,0 +1,11 @@
+[DEFAULT]
+# Enable dFPI(cookieBehavior 5) for service worker tests.
+prefs =
+ network.cookie.cookieBehavior=5
+tags = serviceworker-dfpi
+# We disable service workers for third-party contexts when dFPI is enabled. So,
+# we disable xorigin tests for dFPI.
+skip-if = xorigin
+dupe-manifest = true
+
+[include:mochitest-common.ini]
diff --git a/dom/serviceworkers/test/mochitest.ini b/dom/serviceworkers/test/mochitest.ini
new file mode 100644
index 0000000000..49b6c91201
--- /dev/null
+++ b/dom/serviceworkers/test/mochitest.ini
@@ -0,0 +1,35 @@
+[DEFAULT]
+# Mochitests are executed in iframes. Several ServiceWorker tests use iframes
+# too. The result is that we have nested iframes. CookieBehavior 4
+# (BEHAVIOR_REJECT_TRACKER) doesn't grant storage access permission to nested
+# iframes because trackers could use them to follow users across sites. Let's
+# use cookieBehavior 0 (BEHAVIOR_ACCEPT) here.
+prefs =
+ network.cookie.cookieBehavior=0
+dupe-manifest = true
+tags = condprof
+
+# Following tests are not working currently when dFPI is enabled. So, we put
+# these tests here instead of mochitest-common.ini so that these tests won't run
+# when dFPI is enabled.
+[test_cookie_fetch.html]
+[test_csp_upgrade-insecure_intercept.html]
+[test_eventsource_intercept.html]
+[test_https_fetch.html]
+skip-if = condprof #: timed out
+[test_https_fetch_cloned_response.html]
+[test_https_origin_after_redirect.html]
+[test_https_origin_after_redirect_cached.html]
+skip-if = condprof #: timed out
+[test_https_synth_fetch_from_cached_sw.html]
+[test_importscript_mixedcontent.html]
+tags = mcb
+[test_openWindow.html]
+skip-if =
+ toolkit == 'android' && !is_fennec # Bug 1620052
+ xorigin # Bug 1792790
+ condprof #: timed out
+tags = openwindow
+[test_sanitize_domain.html]
+
+[include:mochitest-common.ini]
diff --git a/dom/serviceworkers/test/navigationPreload_page.html b/dom/serviceworkers/test/navigationPreload_page.html
new file mode 100644
index 0000000000..39d4a79378
--- /dev/null
+++ b/dom/serviceworkers/test/navigationPreload_page.html
@@ -0,0 +1 @@
+NavigationPreload
diff --git a/dom/serviceworkers/test/network_with_utils.html b/dom/serviceworkers/test/network_with_utils.html
new file mode 100644
index 0000000000..63f6b0e796
--- /dev/null
+++ b/dom/serviceworkers/test/network_with_utils.html
@@ -0,0 +1,14 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <script src="utils.js" type="text/javascript"></script>
+</head>
+<body>
+NETWORK
+</body>
+</html>
diff --git a/dom/serviceworkers/test/nofetch_handler_worker.js b/dom/serviceworkers/test/nofetch_handler_worker.js
new file mode 100644
index 0000000000..77ba0c734a
--- /dev/null
+++ b/dom/serviceworkers/test/nofetch_handler_worker.js
@@ -0,0 +1,14 @@
+function handleFetch(event) {
+ event.respondWith(new Response("intercepted"));
+}
+
+self.oninstall = function(event) {
+ addEventListener("fetch", handleFetch);
+ self.onfetch = handleFetch;
+};
+
+// Bug 1325101. Make sure adding event listeners for other events
+// doesn't set the fetch flag.
+addEventListener("push", function() {});
+addEventListener("message", function() {});
+addEventListener("non-sw-event", function() {});
diff --git a/dom/serviceworkers/test/notification/listener.html b/dom/serviceworkers/test/notification/listener.html
new file mode 100644
index 0000000000..1c6e282ece
--- /dev/null
+++ b/dom/serviceworkers/test/notification/listener.html
@@ -0,0 +1,27 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1114554 - proxy to forward messages from SW to test</title>
+<script class="testbody" type="text/javascript">
+ var testWindow = parent;
+ if (opener) {
+ testWindow = opener;
+ }
+
+ navigator.serviceWorker.onmessage = function(msg) {
+ // worker message;
+ testWindow.postMessage(msg.data, "*");
+ if (msg.data.type == 'finish') {
+ window.close();
+ }
+ };
+</script>
+
+</head>
+<body>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/notification/register.html b/dom/serviceworkers/test/notification/register.html
new file mode 100644
index 0000000000..b7df73bede
--- /dev/null
+++ b/dom/serviceworkers/test/notification/register.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<script>
+ function done() {
+ parent.callback();
+ }
+
+ navigator.serviceWorker.ready.then(done);
+ navigator.serviceWorker.register("../notification_get_sw.js", {scope: "."}).catch(function(e) {
+ dump("Registration failure " + e.message + "\n");
+ });
+</script>
diff --git a/dom/serviceworkers/test/notification/unregister.html b/dom/serviceworkers/test/notification/unregister.html
new file mode 100644
index 0000000000..d5a141f830
--- /dev/null
+++ b/dom/serviceworkers/test/notification/unregister.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<script>
+ navigator.serviceWorker.getRegistration(".").then(function(registration) {
+ registration.unregister().then(function(success) {
+ if (success) {
+ window.parent.callback();
+ }
+ }, function(e) {
+ dump("Unregistering the SW failed with " + e + "\n");
+ });
+ });
+</script>
diff --git a/dom/serviceworkers/test/notification_alt/register.html b/dom/serviceworkers/test/notification_alt/register.html
new file mode 100644
index 0000000000..b7df73bede
--- /dev/null
+++ b/dom/serviceworkers/test/notification_alt/register.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<script>
+ function done() {
+ parent.callback();
+ }
+
+ navigator.serviceWorker.ready.then(done);
+ navigator.serviceWorker.register("../notification_get_sw.js", {scope: "."}).catch(function(e) {
+ dump("Registration failure " + e.message + "\n");
+ });
+</script>
diff --git a/dom/serviceworkers/test/notification_alt/unregister.html b/dom/serviceworkers/test/notification_alt/unregister.html
new file mode 100644
index 0000000000..d5a141f830
--- /dev/null
+++ b/dom/serviceworkers/test/notification_alt/unregister.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<script>
+ navigator.serviceWorker.getRegistration(".").then(function(registration) {
+ registration.unregister().then(function(success) {
+ if (success) {
+ window.parent.callback();
+ }
+ }, function(e) {
+ dump("Unregistering the SW failed with " + e + "\n");
+ });
+ });
+</script>
diff --git a/dom/serviceworkers/test/notification_constructor_error.js b/dom/serviceworkers/test/notification_constructor_error.js
new file mode 100644
index 0000000000..644dba480e
--- /dev/null
+++ b/dom/serviceworkers/test/notification_constructor_error.js
@@ -0,0 +1 @@
+new Notification("Hi there");
diff --git a/dom/serviceworkers/test/notification_get_sw.js b/dom/serviceworkers/test/notification_get_sw.js
new file mode 100644
index 0000000000..9b7c24f496
--- /dev/null
+++ b/dom/serviceworkers/test/notification_get_sw.js
@@ -0,0 +1,55 @@
+function postAll(data) {
+ self.clients.matchAll().then(function(clients) {
+ if (!clients.length) {
+ dump(
+ "***************** NO CLIENTS FOUND! Test messages are being lost *******************\n"
+ );
+ }
+ clients.forEach(function(client) {
+ client.postMessage(data);
+ });
+ });
+}
+
+function ok(a, msg) {
+ postAll({ type: "status", status: !!a, msg: a + ": " + msg });
+}
+
+function is(a, b, msg) {
+ postAll({
+ type: "status",
+ status: a === b,
+ msg: a + " === " + b + ": " + msg,
+ });
+}
+
+function done() {
+ postAll({ type: "finish" });
+}
+
+onmessage = function(e) {
+ dump("%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% MESSAGE " + e.data + "\n");
+ var start;
+ if (e.data == "create") {
+ start = registration.showNotification("This is a title");
+ } else {
+ start = Promise.resolve();
+ }
+
+ start.then(function() {
+ dump("CALLING getNotification\n");
+ registration.getNotifications().then(function(notifications) {
+ dump("RECD getNotification\n");
+ is(notifications.length, 1, "There should be one stored notification");
+ var notification = notifications[0];
+ if (!notification) {
+ done();
+ return;
+ }
+ ok(notification instanceof Notification, "Should be a Notification");
+ is(notification.title, "This is a title", "Title should match");
+ notification.close();
+ done();
+ });
+ });
+};
diff --git a/dom/serviceworkers/test/notification_openWindow_worker.js b/dom/serviceworkers/test/notification_openWindow_worker.js
new file mode 100644
index 0000000000..174199b14a
--- /dev/null
+++ b/dom/serviceworkers/test/notification_openWindow_worker.js
@@ -0,0 +1,25 @@
+const gRoot = "http://mochi.test:8888/tests/dom/serviceworkers/test/";
+const gTestURL = gRoot + "test_notification_openWindow.html";
+const gClientURL = gRoot + "file_notification_openWindow.html";
+
+onmessage = function(event) {
+ if (event.data !== "DONE") {
+ dump(`ERROR: received unexpected message: ${JSON.stringify(event.data)}\n`);
+ }
+
+ event.waitUntil(
+ clients.matchAll({ includeUncontrolled: true }).then(cl => {
+ for (let client of cl) {
+ // The |gClientURL| window closes itself after posting the DONE message,
+ // so we don't need to send it anything here.
+ if (client.url === gTestURL) {
+ client.postMessage("DONE");
+ }
+ }
+ })
+ );
+};
+
+onnotificationclick = function(event) {
+ clients.openWindow(gClientURL);
+};
diff --git a/dom/serviceworkers/test/notificationclick-otherwindow.html b/dom/serviceworkers/test/notificationclick-otherwindow.html
new file mode 100644
index 0000000000..f64e82aabd
--- /dev/null
+++ b/dom/serviceworkers/test/notificationclick-otherwindow.html
@@ -0,0 +1,30 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1114554 - controlled page</title>
+<script class="testbody" type="text/javascript">
+ var testWindow = parent;
+ if (opener) {
+ testWindow = opener;
+ }
+
+ navigator.serviceWorker.ready.then(function(swr) {
+ var ifr = document.createElement("iframe");
+ document.documentElement.appendChild(ifr);
+ ifr.contentWindow.ServiceWorkerRegistration.prototype.showNotification
+ .call(swr, "Hi there. The ServiceWorker should receive a click event for this.", { data: { complex: ["jsval", 5] }});
+ });
+
+ navigator.serviceWorker.onmessage = function(msg) {
+ testWindow.callback(msg.data.result);
+ };
+</script>
+
+</head>
+<body>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/notificationclick.html b/dom/serviceworkers/test/notificationclick.html
new file mode 100644
index 0000000000..448764a1cb
--- /dev/null
+++ b/dom/serviceworkers/test/notificationclick.html
@@ -0,0 +1,27 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1114554 - controlled page</title>
+<script class="testbody" type="text/javascript">
+ var testWindow = parent;
+ if (opener) {
+ testWindow = opener;
+ }
+
+ navigator.serviceWorker.ready.then(function(swr) {
+ swr.showNotification("Hi there. The ServiceWorker should receive a click event for this.", { data: { complex: ["jsval", 5] }});
+ });
+
+ navigator.serviceWorker.onmessage = function(msg) {
+ testWindow.callback(msg.data.result);
+ };
+</script>
+
+</head>
+<body>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/notificationclick.js b/dom/serviceworkers/test/notificationclick.js
new file mode 100644
index 0000000000..ba7a7cb14a
--- /dev/null
+++ b/dom/serviceworkers/test/notificationclick.js
@@ -0,0 +1,23 @@
+// Any copyright is dedicated to the Public Domain.
+// http://creativecommons.org/publicdomain/zero/1.0/
+//
+onnotificationclick = function(e) {
+ self.clients.matchAll().then(function(clients) {
+ if (clients.length === 0) {
+ dump(
+ "********************* CLIENTS LIST EMPTY! Test will timeout! ***********************\n"
+ );
+ return;
+ }
+
+ clients.forEach(function(client) {
+ client.postMessage({
+ result:
+ e.notification.data &&
+ e.notification.data.complex &&
+ e.notification.data.complex[0] == "jsval" &&
+ e.notification.data.complex[1] == 5,
+ });
+ });
+ });
+};
diff --git a/dom/serviceworkers/test/notificationclick_focus.html b/dom/serviceworkers/test/notificationclick_focus.html
new file mode 100644
index 0000000000..0152d397f3
--- /dev/null
+++ b/dom/serviceworkers/test/notificationclick_focus.html
@@ -0,0 +1,28 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1144660 - controlled page</title>
+<script class="testbody" type="text/javascript">
+ var testWindow = parent;
+ if (opener) {
+ testWindow = opener;
+ }
+
+ navigator.serviceWorker.ready.then(function(swr) {
+ swr.showNotification("Hi there. The ServiceWorker should receive a click event for this.");
+ });
+
+ navigator.serviceWorker.onmessage = function(msg) {
+ dump("GOT Message " + JSON.stringify(msg.data) + "\n");
+ testWindow.callback(msg.data.ok);
+ };
+</script>
+
+</head>
+<body>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/notificationclick_focus.js b/dom/serviceworkers/test/notificationclick_focus.js
new file mode 100644
index 0000000000..4c88340041
--- /dev/null
+++ b/dom/serviceworkers/test/notificationclick_focus.js
@@ -0,0 +1,49 @@
+// Any copyright is dedicated to the Public Domain.
+// http://creativecommons.org/publicdomain/zero/1.0/
+//
+
+function promisifyTimerFocus(client, delay) {
+ return new Promise(function(resolve, reject) {
+ setTimeout(function() {
+ client.focus().then(resolve, reject);
+ }, delay);
+ });
+}
+
+onnotificationclick = function(e) {
+ e.waitUntil(
+ self.clients.matchAll().then(function(clients) {
+ if (clients.length === 0) {
+ dump(
+ "********************* CLIENTS LIST EMPTY! Test will timeout! ***********************\n"
+ );
+ return Promise.resolve();
+ }
+
+ var immediatePromise = clients[0].focus();
+ var withinTimeout = promisifyTimerFocus(clients[0], 100);
+
+ var afterTimeout = promisifyTimerFocus(clients[0], 2000).then(
+ function() {
+ throw "Should have failed!";
+ },
+ function() {
+ return Promise.resolve();
+ }
+ );
+
+ return Promise.all([immediatePromise, withinTimeout, afterTimeout])
+ .then(function() {
+ clients.forEach(function(client) {
+ client.postMessage({ ok: true });
+ });
+ })
+ .catch(function(ex) {
+ dump("Error " + ex + "\n");
+ clients.forEach(function(client) {
+ client.postMessage({ ok: false });
+ });
+ });
+ })
+ );
+};
diff --git a/dom/serviceworkers/test/notificationclose.html b/dom/serviceworkers/test/notificationclose.html
new file mode 100644
index 0000000000..f18801122e
--- /dev/null
+++ b/dom/serviceworkers/test/notificationclose.html
@@ -0,0 +1,37 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1265841 - controlled page</title>
+<script class="testbody" type="text/javascript">
+ var testWindow = parent;
+ if (opener) {
+ testWindow = opener;
+ }
+
+ navigator.serviceWorker.ready.then(function(swr) {
+ return swr.showNotification(
+ "Hi there. The ServiceWorker should receive a close event for this.",
+ { data: { complex: ["jsval", 5] }}).then(function() {
+ return swr;
+ });
+ }).then(function(swr) {
+ return swr.getNotifications();
+ }).then(function(notifications) {
+ notifications.forEach(function(notification) {
+ notification.close();
+ });
+ });
+
+ navigator.serviceWorker.onmessage = function(msg) {
+ testWindow.callback(msg.data);
+ };
+</script>
+
+</head>
+<body>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/notificationclose.js b/dom/serviceworkers/test/notificationclose.js
new file mode 100644
index 0000000000..75271e604e
--- /dev/null
+++ b/dom/serviceworkers/test/notificationclose.js
@@ -0,0 +1,31 @@
+// Any copyright is dedicated to the Public Domain.
+// http://creativecommons.org/publicdomain/zero/1.0/
+//
+onnotificationclose = function(e) {
+ e.waitUntil(
+ (async function() {
+ let windowOpened = true;
+ await clients.openWindow("hello.html").catch(err => {
+ windowOpened = false;
+ });
+
+ self.clients.matchAll().then(function(clients) {
+ if (clients.length === 0) {
+ dump("*** CLIENTS LIST EMPTY! Test will timeout! ***\n");
+ return;
+ }
+
+ clients.forEach(function(client) {
+ client.postMessage({
+ result:
+ e.notification.data &&
+ e.notification.data.complex &&
+ e.notification.data.complex[0] == "jsval" &&
+ e.notification.data.complex[1] == 5,
+ windowOpened,
+ });
+ });
+ });
+ })()
+ );
+};
diff --git a/dom/serviceworkers/test/notify_loaded.js b/dom/serviceworkers/test/notify_loaded.js
new file mode 100644
index 0000000000..3bf001abd6
--- /dev/null
+++ b/dom/serviceworkers/test/notify_loaded.js
@@ -0,0 +1 @@
+parent.postMessage("SCRIPT_LOADED", "*");
diff --git a/dom/serviceworkers/test/onmessageerror_worker.js b/dom/serviceworkers/test/onmessageerror_worker.js
new file mode 100644
index 0000000000..15426128a6
--- /dev/null
+++ b/dom/serviceworkers/test/onmessageerror_worker.js
@@ -0,0 +1,54 @@
+async function getSwContainer() {
+ const clients = await self.clients.matchAll({
+ type: "window",
+ includeUncontrolled: true,
+ });
+
+ for (let client of clients) {
+ if (client.url.endsWith("test_onmessageerror.html")) {
+ return client;
+ }
+ }
+}
+
+self.addEventListener("message", async e => {
+ const config = e.data;
+ const swContainer = await getSwContainer();
+
+ if (config == "send-bad-message") {
+ const serializable = true;
+ const deserializable = false;
+
+ swContainer.postMessage(
+ new StructuredCloneTester(serializable, deserializable)
+ );
+
+ return;
+ }
+
+ if (!config.serializable) {
+ swContainer.postMessage({
+ result: "Error",
+ reason: "Service Worker received an unserializable object",
+ });
+
+ return;
+ }
+
+ if (!config.deserializable) {
+ swContainer.postMessage({
+ result: "Error",
+ reason:
+ "Service Worker received (and deserialized) an un-deserializable object",
+ });
+
+ return;
+ }
+
+ swContainer.postMessage({ received: "message" });
+});
+
+self.addEventListener("messageerror", async () => {
+ const swContainer = await getSwContainer();
+ swContainer.postMessage({ received: "messageerror" });
+});
diff --git a/dom/serviceworkers/test/opaque_intercept_worker.js b/dom/serviceworkers/test/opaque_intercept_worker.js
new file mode 100644
index 0000000000..5dbf6fbc28
--- /dev/null
+++ b/dom/serviceworkers/test/opaque_intercept_worker.js
@@ -0,0 +1,40 @@
+var name = "opaqueInterceptCache";
+
+// Cross origin request to ensure that an opaque response is used
+var prefix = "http://example.com/tests/dom/serviceworkers/test/";
+
+var testReady = new Promise(resolve => {
+ self.addEventListener(
+ "message",
+ m => {
+ resolve();
+ },
+ { once: true }
+ );
+});
+
+self.addEventListener("install", function(event) {
+ var request = new Request(prefix + "notify_loaded.js", { mode: "no-cors" });
+ event.waitUntil(
+ Promise.all([caches.open(name), fetch(request), testReady]).then(function(
+ results
+ ) {
+ var cache = results[0];
+ var response = results[1];
+ return cache.put("./sw_clients/does_not_exist.js", response);
+ })
+ );
+});
+
+self.addEventListener("fetch", function(event) {
+ event.respondWith(
+ caches
+ .open(name)
+ .then(function(cache) {
+ return cache.match(event.request);
+ })
+ .then(function(response) {
+ return response || fetch(event.request);
+ })
+ );
+});
diff --git a/dom/serviceworkers/test/openWindow_worker.js b/dom/serviceworkers/test/openWindow_worker.js
new file mode 100644
index 0000000000..eb440da6e5
--- /dev/null
+++ b/dom/serviceworkers/test/openWindow_worker.js
@@ -0,0 +1,178 @@
+// the worker won't shut down between events because we increased
+// the timeout values.
+var client;
+var window_count = 0;
+var expected_window_count = 9;
+var isolated_window_count = 0;
+var expected_isolated_window_count = 2;
+var resolve_got_all_windows = null;
+var got_all_windows = new Promise(function(res, rej) {
+ resolve_got_all_windows = res;
+});
+
+// |expected_window_count| needs to be updated for every new call that's
+// expected to actually open a new window regardless of what |clients.openWindow|
+// returns.
+function testForUrl(url, throwType, clientProperties, resultsArray) {
+ return clients
+ .openWindow(url)
+ .then(function(e) {
+ if (throwType != null) {
+ resultsArray.push({
+ result: false,
+ message: "openWindow should throw " + throwType,
+ });
+ } else if (clientProperties) {
+ resultsArray.push({
+ result: e instanceof WindowClient,
+ message: `openWindow should resolve to a WindowClient for url ${url}, got ${e}`,
+ });
+ resultsArray.push({
+ result: e.url == clientProperties.url,
+ message: "Client url should be " + clientProperties.url,
+ });
+ // Add more properties
+ } else {
+ resultsArray.push({
+ result: e == null,
+ message: "Open window should resolve to null. Got: " + e,
+ });
+ }
+ })
+ .catch(function(err) {
+ if (throwType == null) {
+ resultsArray.push({
+ result: false,
+ message: "Unexpected throw: " + err,
+ });
+ } else {
+ resultsArray.push({
+ result: err.toString().includes(throwType),
+ message: "openWindow should throw: " + err,
+ });
+ }
+ });
+}
+
+onmessage = function(event) {
+ if (event.data == "testNoPopup") {
+ client = event.source;
+
+ var results = [];
+ var promises = [];
+ promises.push(testForUrl("about:blank", "TypeError", null, results));
+ promises.push(
+ testForUrl("http://example.com", "InvalidAccessError", null, results)
+ );
+ promises.push(
+ testForUrl("_._*`InvalidURL", "InvalidAccessError", null, results)
+ );
+ event.waitUntil(
+ Promise.all(promises).then(function(e) {
+ client.postMessage(results);
+ })
+ );
+ }
+
+ if (event.data == "NEW_WINDOW" || event.data == "NEW_ISOLATED_WINDOW") {
+ window_count += 1;
+ if (event.data == "NEW_ISOLATED_WINDOW") {
+ isolated_window_count += 1;
+ }
+ if (window_count == expected_window_count) {
+ resolve_got_all_windows();
+ }
+ }
+
+ if (event.data == "CHECK_NUMBER_OF_WINDOWS") {
+ event.waitUntil(
+ got_all_windows
+ .then(function() {
+ return clients.matchAll();
+ })
+ .then(function(cl) {
+ event.source.postMessage([
+ {
+ result: cl.length == expected_window_count,
+ message: `The number of windows is correct. ${cl.length} == ${expected_window_count}`,
+ },
+ {
+ result: isolated_window_count == expected_isolated_window_count,
+ message: `The number of isolated windows is correct. ${isolated_window_count} == ${expected_isolated_window_count}`,
+ },
+ ]);
+ for (i = 0; i < cl.length; i++) {
+ cl[i].postMessage("CLOSE");
+ }
+ })
+ );
+ }
+};
+
+onnotificationclick = function(e) {
+ var results = [];
+ var promises = [];
+
+ var redirect =
+ "http://mochi.test:8888/tests/dom/serviceworkers/test/redirect.sjs?";
+ var redirect_xorigin =
+ "http://example.com/tests/dom/serviceworkers/test/redirect.sjs?";
+ var same_origin =
+ "http://mochi.test:8888/tests/dom/serviceworkers/test/open_window/client.sjs";
+ var different_origin =
+ "http://example.com/tests/dom/serviceworkers/test/open_window/client.sjs";
+
+ promises.push(testForUrl("about:blank", "TypeError", null, results));
+ promises.push(testForUrl(different_origin, null, null, results));
+ promises.push(testForUrl(same_origin, null, { url: same_origin }, results));
+ promises.push(
+ testForUrl("open_window/client.sjs", null, { url: same_origin }, results)
+ );
+
+ // redirect tests
+ promises.push(
+ testForUrl(
+ redirect + "open_window/client.sjs",
+ null,
+ { url: same_origin },
+ results
+ )
+ );
+ promises.push(testForUrl(redirect + different_origin, null, null, results));
+
+ promises.push(
+ testForUrl(redirect_xorigin + "open_window/client.sjs", null, null, results)
+ );
+ promises.push(
+ testForUrl(
+ redirect_xorigin + same_origin,
+ null,
+ { url: same_origin },
+ results
+ )
+ );
+
+ // coop+coep tests
+ promises.push(
+ testForUrl(
+ same_origin + "?crossOriginIsolated=true",
+ null,
+ { url: same_origin + "?crossOriginIsolated=true" },
+ results
+ )
+ );
+ promises.push(
+ testForUrl(
+ different_origin + "?crossOriginIsolated=true",
+ null,
+ null,
+ results
+ )
+ );
+
+ e.waitUntil(
+ Promise.all(promises).then(function() {
+ client.postMessage(results);
+ })
+ );
+};
diff --git a/dom/serviceworkers/test/open_window/client.sjs b/dom/serviceworkers/test/open_window/client.sjs
new file mode 100644
index 0000000000..236a4a1226
--- /dev/null
+++ b/dom/serviceworkers/test/open_window/client.sjs
@@ -0,0 +1,69 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const RESPONSE = `
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1172870 - page opened by ServiceWorkerClients.OpenWindow</title>
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+<h1>client.sjs</h1>
+<script class="testbody" type="text/javascript">
+
+ window.onload = function() {
+ if (document.domain === "mochi.test") {
+ navigator.serviceWorker.ready.then(function(result) {
+ navigator.serviceWorker.onmessage = function(event) {
+ if (event.data !== "CLOSE") {
+ dump("ERROR: unexepected reply from the service worker.\\n");
+ }
+ if (parent) {
+ parent.postMessage("CLOSE", "*");
+ }
+ window.close();
+ }
+
+ let message = window.crossOriginIsolated ? "NEW_ISOLATED_WINDOW" : "NEW_WINDOW";
+ navigator.serviceWorker.controller.postMessage(message);
+ })
+ } else {
+ window.onmessage = function(event) {
+ if (event.data !== "CLOSE") {
+ dump("ERROR: unexepected reply from the iframe.\\n");
+ }
+ window.close();
+ }
+
+ var iframe = document.createElement('iframe');
+ iframe.src = "http://mochi.test:8888/tests/dom/serviceworkers/test/open_window/client.sjs";
+ document.body.appendChild(iframe);
+ }
+ }
+
+</script>
+</pre>
+</body>
+</html>
+`;
+
+function handleRequest(request, response) {
+ Components.utils.importGlobalProperties(["URLSearchParams"]);
+ let query = new URLSearchParams(request.queryString);
+
+ // If the request has been marked to be isolated with COOP+COEP, set the appropriate headers.
+ if (query.get("crossOriginIsolated") == "true") {
+ response.setHeader("Cross-Origin-Opener-Policy", "same-origin", false);
+ }
+
+ // Always set the COEP and CORP headers, so that this document can be framed
+ // by a document which has also set COEP to require-corp.
+ response.setHeader("Cross-Origin-Embedder-Policy", "require-corp", false);
+ response.setHeader("Cross-Origin-Resource-Policy", "cross-origin", false);
+
+ response.setHeader("Content-Type", "text/html", false);
+ response.write(RESPONSE);
+}
diff --git a/dom/serviceworkers/test/page_post_controlled.html b/dom/serviceworkers/test/page_post_controlled.html
new file mode 100644
index 0000000000..27694c0027
--- /dev/null
+++ b/dom/serviceworkers/test/page_post_controlled.html
@@ -0,0 +1,27 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+</head>
+<body>
+<script type="text/javascript">
+ window.parent.postMessage({
+ controlled: !!navigator.serviceWorker.controller
+ }, "*");
+
+ addEventListener("message", e => {
+ if (e.data == "create nested iframe") {
+ const iframe = document.createElement('iframe');
+ document.body.appendChild(iframe);
+ iframe.src = location.href;
+ } else {
+ window.parent.postMessage(e.data, "*");
+ }
+ });
+</script>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/parse_error_worker.js b/dom/serviceworkers/test/parse_error_worker.js
new file mode 100644
index 0000000000..b6a8ef0a1a
--- /dev/null
+++ b/dom/serviceworkers/test/parse_error_worker.js
@@ -0,0 +1,2 @@
+// intentional parse error.
+var foo = {;
diff --git a/dom/serviceworkers/test/redirect.sjs b/dom/serviceworkers/test/redirect.sjs
new file mode 100644
index 0000000000..43fec90b5a
--- /dev/null
+++ b/dom/serviceworkers/test/redirect.sjs
@@ -0,0 +1,4 @@
+function handleRequest(request, response) {
+ response.setStatusLine(request.httpVersion, 301, "Moved Permanently");
+ response.setHeader("Location", request.queryString, false);
+}
diff --git a/dom/serviceworkers/test/redirect_post.sjs b/dom/serviceworkers/test/redirect_post.sjs
new file mode 100644
index 0000000000..3b15f397b1
--- /dev/null
+++ b/dom/serviceworkers/test/redirect_post.sjs
@@ -0,0 +1,39 @@
+const CC = Components.Constructor;
+const BinaryInputStream = CC(
+ "@mozilla.org/binaryinputstream;1",
+ "nsIBinaryInputStream",
+ "setInputStream"
+);
+
+function handleRequest(request, response) {
+ var query = {};
+ request.queryString.split("&").forEach(function(val) {
+ var [name, value] = val.split("=");
+ query[name] = unescape(value);
+ });
+
+ var bodyStream = new BinaryInputStream(request.bodyInputStream);
+ var bodyBytes = [];
+ while ((bodyAvail = bodyStream.available()) > 0) {
+ Array.prototype.push.apply(bodyBytes, bodyStream.readByteArray(bodyAvail));
+ }
+
+ var body = decodeURIComponent(
+ escape(String.fromCharCode.apply(null, bodyBytes))
+ );
+
+ var currentHop = query.hop ? parseInt(query.hop) : 0;
+
+ var obj = JSON.parse(body);
+ if (currentHop < obj.hops) {
+ var newURL =
+ "/tests/dom/serviceworkers/test/redirect_post.sjs?hop=" +
+ (1 + currentHop);
+ response.setStatusLine(null, 307, "redirect");
+ response.setHeader("Location", newURL);
+ return;
+ }
+
+ response.setHeader("Content-Type", "application/json");
+ response.write(body);
+}
diff --git a/dom/serviceworkers/test/redirect_serviceworker.sjs b/dom/serviceworkers/test/redirect_serviceworker.sjs
new file mode 100644
index 0000000000..858e6d4824
--- /dev/null
+++ b/dom/serviceworkers/test/redirect_serviceworker.sjs
@@ -0,0 +1,7 @@
+function handleRequest(request, response) {
+ response.setStatusLine("1.1", 302, "Found");
+ response.setHeader(
+ "Location",
+ "http://mochi.test:8888/tests/dom/serviceworkers/test/worker.js"
+ );
+}
diff --git a/dom/serviceworkers/test/register_https.html b/dom/serviceworkers/test/register_https.html
new file mode 100644
index 0000000000..572c7ce6b8
--- /dev/null
+++ b/dom/serviceworkers/test/register_https.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<script>
+function ok(condition, message) {
+ parent.postMessage({type: "ok", status: condition, msg: message}, "*");
+}
+
+function done() {
+ parent.postMessage({type: "done"}, "*");
+}
+
+ok(location.protocol == "https:", "We should be loaded from HTTPS");
+ok(!window.isSecureContext, "Should not be secure context");
+ok(!("serviceWorker" in navigator), "ServiceWorkerContainer not availalble in insecure context");
+done();
+</script>
diff --git a/dom/serviceworkers/test/sanitize/example_check_and_unregister.html b/dom/serviceworkers/test/sanitize/example_check_and_unregister.html
new file mode 100644
index 0000000000..8553e442d6
--- /dev/null
+++ b/dom/serviceworkers/test/sanitize/example_check_and_unregister.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<script>
+ function done(exists) {
+ parent.postMessage(exists, '*');
+ }
+
+ function fail() {
+ parent.postMessage("FAIL", '*');
+ }
+
+ navigator.serviceWorker.getRegistration(".").then(function(reg) {
+ if (reg) {
+ reg.unregister().then(done.bind(undefined, true), fail);
+ } else {
+ dump("getRegistration() returned undefined registration\n");
+ done(false);
+ }
+ }, function(e) {
+ dump("getRegistration() failed\n");
+ fail();
+ });
+</script>
diff --git a/dom/serviceworkers/test/sanitize/frame.html b/dom/serviceworkers/test/sanitize/frame.html
new file mode 100644
index 0000000000..b4bf7a1ff1
--- /dev/null
+++ b/dom/serviceworkers/test/sanitize/frame.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<script>
+ fetch("intercept-this").then(function(r) {
+ if (!r.ok) {
+ return "FAIL";
+ }
+ return r.text();
+ }).then(function(body) {
+ parent.postMessage(body, '*');
+ });
+</script>
diff --git a/dom/serviceworkers/test/sanitize/register.html b/dom/serviceworkers/test/sanitize/register.html
new file mode 100644
index 0000000000..4ae74bec11
--- /dev/null
+++ b/dom/serviceworkers/test/sanitize/register.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<script>
+ function done() {
+ parent.postMessage('', '*');
+ }
+
+ navigator.serviceWorker.ready.then(done);
+ navigator.serviceWorker.register("../sanitize_worker.js", {scope: "."});
+</script>
diff --git a/dom/serviceworkers/test/sanitize_worker.js b/dom/serviceworkers/test/sanitize_worker.js
new file mode 100644
index 0000000000..efd25a3856
--- /dev/null
+++ b/dom/serviceworkers/test/sanitize_worker.js
@@ -0,0 +1,5 @@
+onfetch = function(e) {
+ if (e.request.url.includes("intercept-this")) {
+ e.respondWith(new Response("intercepted"));
+ }
+};
diff --git a/dom/serviceworkers/test/scope/scope_worker.js b/dom/serviceworkers/test/scope/scope_worker.js
new file mode 100644
index 0000000000..4164e7a244
--- /dev/null
+++ b/dom/serviceworkers/test/scope/scope_worker.js
@@ -0,0 +1,2 @@
+// This worker is used to test if calling register() without a scope argument
+// leads to scope being relative to service worker script.
diff --git a/dom/serviceworkers/test/script_file_upload.js b/dom/serviceworkers/test/script_file_upload.js
new file mode 100644
index 0000000000..ff839744b2
--- /dev/null
+++ b/dom/serviceworkers/test/script_file_upload.js
@@ -0,0 +1,15 @@
+/* eslint-env mozilla/chrome-script */
+
+Cu.importGlobalProperties(["File"]);
+
+addMessageListener("file.open", function(e) {
+ var testFile = Cc["@mozilla.org/file/directory_service;1"]
+ .getService(Ci.nsIDirectoryService)
+ .QueryInterface(Ci.nsIProperties)
+ .get("ProfD", Ci.nsIFile);
+ testFile.append("prefs.js");
+
+ File.createFromNsIFile(testFile).then(function(file) {
+ sendAsyncMessage("file.opened", { file });
+ });
+});
diff --git a/dom/serviceworkers/test/self_update_worker.sjs b/dom/serviceworkers/test/self_update_worker.sjs
new file mode 100644
index 0000000000..8081b20afd
--- /dev/null
+++ b/dom/serviceworkers/test/self_update_worker.sjs
@@ -0,0 +1,42 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const WORKER_BODY = `
+onactivate = function(event) {
+ let promise = clients.matchAll({includeUncontrolled: true}).then(function(clients) {
+ for (i = 0; i < clients.length; i++) {
+ clients[i].postMessage({version: version});
+ }
+ }).then(function() {
+ return self.registration.update();
+ });
+ event.waitUntil(promise);
+};
+`;
+
+function handleRequest(request, response) {
+ if (request.queryString == "clearcounter") {
+ setState("count", "1");
+ response.write("ok");
+ return;
+ }
+
+ let count = getState("count");
+ if (count === "") {
+ count = 1;
+ } else {
+ count = parseInt(count);
+ }
+
+ let worker = "var version = " + count + ";\n";
+ worker = worker + WORKER_BODY;
+
+ // This header is necessary for making this script able to be loaded.
+ response.setHeader("Content-Type", "application/javascript");
+
+ // If this is the first request, return the first source.
+ response.write(worker);
+ setState("count", "" + (count + 1));
+}
diff --git a/dom/serviceworkers/test/server_file_upload.sjs b/dom/serviceworkers/test/server_file_upload.sjs
new file mode 100644
index 0000000000..a2f960af94
--- /dev/null
+++ b/dom/serviceworkers/test/server_file_upload.sjs
@@ -0,0 +1,22 @@
+const CC = Components.Constructor;
+const BinaryInputStream = CC(
+ "@mozilla.org/binaryinputstream;1",
+ "nsIBinaryInputStream",
+ "setInputStream"
+);
+const BinaryOutputStream = CC(
+ "@mozilla.org/binaryoutputstream;1",
+ "nsIBinaryOutputStream",
+ "setOutputStream"
+);
+
+function handleRequest(request, response) {
+ var bodyStream = new BinaryInputStream(request.bodyInputStream);
+ var bodyBytes = [];
+ while ((bodyAvail = bodyStream.available()) > 0) {
+ Array.prototype.push.apply(bodyBytes, bodyStream.readByteArray(bodyAvail));
+ }
+
+ var bos = new BinaryOutputStream(response.bodyOutputStream);
+ bos.writeByteArray(bodyBytes, bodyBytes.length);
+}
diff --git a/dom/serviceworkers/test/service_worker.js b/dom/serviceworkers/test/service_worker.js
new file mode 100644
index 0000000000..432b943d8b
--- /dev/null
+++ b/dom/serviceworkers/test/service_worker.js
@@ -0,0 +1,9 @@
+onmessage = function(e) {
+ self.clients.matchAll().then(function(res) {
+ if (!res.length) {
+ dump("Error: no clients are currently controlled.\n");
+ return;
+ }
+ res[0].postMessage(indexedDB ? { available: true } : { available: false });
+ });
+};
diff --git a/dom/serviceworkers/test/service_worker_client.html b/dom/serviceworkers/test/service_worker_client.html
new file mode 100644
index 0000000000..c1c98eaabb
--- /dev/null
+++ b/dom/serviceworkers/test/service_worker_client.html
@@ -0,0 +1,28 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>controlled page</title>
+<script class="testbody" type="text/javascript">
+ if (!parent) {
+ info("service_worker_client.html should not be launched directly!");
+ }
+
+ window.onload = function() {
+ navigator.serviceWorker.onmessage = function(msg) {
+ // Forward messages coming from the service worker to the test page.
+ parent.postMessage(msg.data, "*");
+ };
+ navigator.serviceWorker.ready.then(function(swr) {
+ parent.postMessage("READY", "*");
+ });
+ }
+</script>
+
+</head>
+<body>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/serviceworker.html b/dom/serviceworkers/test/serviceworker.html
new file mode 100644
index 0000000000..11edd001a2
--- /dev/null
+++ b/dom/serviceworkers/test/serviceworker.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <script>
+ navigator.serviceWorker.register("worker.js");
+ </script>
+ </head>
+ <body>
+ This is a test page.
+ </body>
+<html>
diff --git a/dom/serviceworkers/test/serviceworker_not_sharedworker.js b/dom/serviceworkers/test/serviceworker_not_sharedworker.js
new file mode 100644
index 0000000000..ef89fb01b4
--- /dev/null
+++ b/dom/serviceworkers/test/serviceworker_not_sharedworker.js
@@ -0,0 +1,20 @@
+function OnMessage(e) {
+ if (e.data.msg == "whoareyou") {
+ if ("ServiceWorker" in self) {
+ self.clients.matchAll().then(function(clients) {
+ clients[0].postMessage({ result: "serviceworker" });
+ });
+ } else {
+ port.postMessage({ result: "sharedworker" });
+ }
+ }
+}
+
+var port;
+onconnect = function(e) {
+ port = e.ports[0];
+ port.onmessage = OnMessage;
+ port.start();
+};
+
+onmessage = OnMessage;
diff --git a/dom/serviceworkers/test/serviceworker_wrapper.js b/dom/serviceworkers/test/serviceworker_wrapper.js
new file mode 100644
index 0000000000..c9716c381f
--- /dev/null
+++ b/dom/serviceworkers/test/serviceworker_wrapper.js
@@ -0,0 +1,92 @@
+// Any copyright is dedicated to the Public Domain.
+// http://creativecommons.org/publicdomain/zero/1.0/
+//
+// ServiceWorker equivalent of worker_wrapper.js.
+
+let client;
+
+function ok(a, msg) {
+ dump("OK: " + !!a + " => " + a + ": " + msg + "\n");
+ client.postMessage({ type: "status", status: !!a, msg: a + ": " + msg });
+}
+
+function is(a, b, msg) {
+ dump("IS: " + (a === b) + " => " + a + " | " + b + ": " + msg + "\n");
+ client.postMessage({
+ type: "status",
+ status: a === b,
+ msg: a + " === " + b + ": " + msg,
+ });
+}
+
+function workerTestArrayEquals(a, b) {
+ if (!Array.isArray(a) || !Array.isArray(b) || a.length != b.length) {
+ return false;
+ }
+ for (var i = 0, n = a.length; i < n; ++i) {
+ if (a[i] !== b[i]) {
+ return false;
+ }
+ }
+ return true;
+}
+
+function workerTestDone() {
+ client.postMessage({ type: "finish" });
+}
+
+function workerTestGetHelperData(cb) {
+ addEventListener("message", function workerTestGetHelperDataCB(e) {
+ if (e.data.type !== "returnHelperData") {
+ return;
+ }
+ removeEventListener("message", workerTestGetHelperDataCB);
+ cb(e.data.result);
+ });
+ client.postMessage({
+ type: "getHelperData",
+ });
+}
+
+function workerTestGetStorageManager(cb) {
+ addEventListener("message", function workerTestGetStorageManagerCB(e) {
+ if (e.data.type !== "returnStorageManager") {
+ return;
+ }
+ removeEventListener("message", workerTestGetStorageManagerCB);
+ cb(e.data.result);
+ });
+ client.postMessage({
+ type: "getStorageManager",
+ });
+}
+
+let completeInstall;
+
+addEventListener("message", function workerWrapperOnMessage(e) {
+ removeEventListener("message", workerWrapperOnMessage);
+ var data = e.data;
+ self.clients.matchAll({ includeUncontrolled: true }).then(function(clients) {
+ for (var i = 0; i < clients.length; ++i) {
+ if (clients[i].url.includes("message_receiver.html")) {
+ client = clients[i];
+ break;
+ }
+ }
+ try {
+ importScripts(data.script);
+ } catch (ex) {
+ client.postMessage({
+ type: "status",
+ status: false,
+ msg:
+ "worker failed to import " + data.script + "; error: " + ex.message,
+ });
+ }
+ completeInstall();
+ });
+});
+
+addEventListener("install", e => {
+ e.waitUntil(new Promise(resolve => (completeInstall = resolve)));
+});
diff --git a/dom/serviceworkers/test/serviceworkerinfo_iframe.html b/dom/serviceworkers/test/serviceworkerinfo_iframe.html
new file mode 100644
index 0000000000..24103d1757
--- /dev/null
+++ b/dom/serviceworkers/test/serviceworkerinfo_iframe.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <script>
+ window.onmessage = function (event) {
+ if (event.data !== "register") {
+ return;
+ }
+ var promise = navigator.serviceWorker.register("worker.js",
+ { updateViaCache: 'all' });
+ window.onmessage = function (e) {
+ if (e.data !== "unregister") {
+ return;
+ }
+ promise.then(function (registration) {
+ registration.unregister();
+ });
+ window.onmessage = null;
+ };
+ };
+ </script>
+ </head>
+ <body>
+ This is a test page.
+ </body>
+<html>
diff --git a/dom/serviceworkers/test/serviceworkermanager_iframe.html b/dom/serviceworkers/test/serviceworkermanager_iframe.html
new file mode 100644
index 0000000000..4ea21010cb
--- /dev/null
+++ b/dom/serviceworkers/test/serviceworkermanager_iframe.html
@@ -0,0 +1,34 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <script>
+ window.onmessage = function (event) {
+ if (event.data !== "register") {
+ return;
+ }
+ var promise = navigator.serviceWorker.register("worker.js");
+ window.onmessage = function (event1) {
+ if (event1.data !== "register") {
+ return;
+ }
+ promise = promise.then(function (registration) {
+ return navigator.serviceWorker.register("worker2.js");
+ });
+ window.onmessage = function (event2) {
+ if (event2.data !== "unregister") {
+ return;
+ }
+ promise.then(function (registration) {
+ registration.unregister();
+ });
+ window.onmessage = null;
+ };
+ };
+ };
+ </script>
+ </head>
+ <body>
+ This is a test page.
+ </body>
+<html>
diff --git a/dom/serviceworkers/test/serviceworkerregistrationinfo_iframe.html b/dom/serviceworkers/test/serviceworkerregistrationinfo_iframe.html
new file mode 100644
index 0000000000..8f382cf0dc
--- /dev/null
+++ b/dom/serviceworkers/test/serviceworkerregistrationinfo_iframe.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <script>
+ var reg;
+ window.onmessage = function (event) {
+ if (event.data !== "register") {
+ return;
+ }
+ var promise = navigator.serviceWorker.register("worker.js");
+ window.onmessage = function (e) {
+ if (e.data === "register") {
+ promise.then(function() {
+ return navigator.serviceWorker.register("worker2.js")
+ .then(function(registration) {
+ reg = registration;
+ });
+ });
+ } else if (e.data === "unregister") {
+ reg.unregister();
+ }
+ };
+ };
+ </script>
+ </head>
+ <body>
+ This is a test page.
+ </body>
+<html>
diff --git a/dom/serviceworkers/test/sharedWorker_fetch.js b/dom/serviceworkers/test/sharedWorker_fetch.js
new file mode 100644
index 0000000000..b0a72674d4
--- /dev/null
+++ b/dom/serviceworkers/test/sharedWorker_fetch.js
@@ -0,0 +1,30 @@
+var clients = new Array();
+clients.length = 0;
+
+var broadcast = function(message) {
+ var length = clients.length;
+ for (var i = 0; i < length; i++) {
+ port = clients[i];
+ port.postMessage(message);
+ }
+};
+
+onconnect = function(e) {
+ clients.push(e.ports[0]);
+ if (clients.length == 1) {
+ clients[0].postMessage("Connected");
+ } else if (clients.length == 2) {
+ broadcast("BothConnected");
+ clients[0].onmessage = function(msg) {
+ if (msg.data == "StartFetchWithWrongIntegrity") {
+ // The fetch will succeed because the integrity value is invalid and we
+ // are looking for the console message regarding the bad integrity value.
+ fetch("SharedWorker_SRIFailed.html", { integrity: "abc" }).then(
+ function() {
+ clients[0].postMessage("SRI_failed");
+ }
+ );
+ }
+ };
+ }
+};
diff --git a/dom/serviceworkers/test/simple_fetch_worker.js b/dom/serviceworkers/test/simple_fetch_worker.js
new file mode 100644
index 0000000000..d5b1d8d9a7
--- /dev/null
+++ b/dom/serviceworkers/test/simple_fetch_worker.js
@@ -0,0 +1,18 @@
+// A simple worker script that forward intercepted url to the controlled window.
+
+function responseMsg(msg) {
+ self.clients
+ .matchAll({
+ includeUncontrolled: true,
+ type: "window",
+ })
+ .then(clients => {
+ if (clients && clients.length) {
+ clients[0].postMessage(msg);
+ }
+ });
+}
+
+onfetch = function(e) {
+ responseMsg(e.request.url);
+};
diff --git a/dom/serviceworkers/test/simpleregister/index.html b/dom/serviceworkers/test/simpleregister/index.html
new file mode 100644
index 0000000000..99e4fe3f23
--- /dev/null
+++ b/dom/serviceworkers/test/simpleregister/index.html
@@ -0,0 +1,51 @@
+<html>
+ <head></head>
+ <body>
+ <script type="text/javascript">
+ var expectedEvents = 2;
+ function eventReceived() {
+ window.parent.postMessage({ type: "check", status: expectedEvents > 0, msg: "updatefound received" }, "*");
+
+ if (--expectedEvents) {
+ window.parent.postMessage({ type: "finish" }, "*");
+ }
+ }
+
+ navigator.serviceWorker.getRegistrations().then(function(a) {
+ window.parent.postMessage({ type: "check", status: Array.isArray(a),
+ msg: "getRegistrations returns an array" }, "*");
+ window.parent.postMessage({ type: "check", status: !!a.length,
+ msg: "getRegistrations returns an array with 1 item" }, "*");
+ for (var i = 0; i < a.length; ++i) {
+ window.parent.postMessage({ type: "check", status: a[i] instanceof ServiceWorkerRegistration,
+ msg: "getRegistrations returns an array of ServiceWorkerRegistration objects" }, "*");
+ if (a[i].scope.match(/simpleregister\//)) {
+ a[i].onupdatefound = function(e) {
+ eventReceived();
+ }
+ }
+ }
+ });
+
+ navigator.serviceWorker.getRegistration('http://mochi.test:8888/tests/dom/serviceworkers/test/simpleregister/')
+ .then(function(a) {
+ window.parent.postMessage({ type: "check", status: a instanceof ServiceWorkerRegistration,
+ msg: "getRegistration returns a ServiceWorkerRegistration" }, "*");
+ a.onupdatefound = function(e) {
+ eventReceived();
+ }
+ });
+
+ navigator.serviceWorker.getRegistration('http://www.something_else.net/')
+ .then(function(a) {
+ window.parent.postMessage({ type: "check", status: false,
+ msg: "getRegistration should throw for security error!" }, "*");
+ }, function(a) {
+ window.parent.postMessage({ type: "check", status: true,
+ msg: "getRegistration should throw for security error!" }, "*");
+ });
+
+ window.parent.postMessage({ type: "ready" }, "*");
+ </script>
+ </body>
+</html>
diff --git a/dom/serviceworkers/test/simpleregister/ready.html b/dom/serviceworkers/test/simpleregister/ready.html
new file mode 100644
index 0000000000..6bc163e5f4
--- /dev/null
+++ b/dom/serviceworkers/test/simpleregister/ready.html
@@ -0,0 +1,14 @@
+<html>
+ <head></head>
+ <body>
+ <script type="text/javascript">
+
+ window.addEventListener('message', function(evt) {
+ navigator.serviceWorker.ready.then(function() {
+ evt.ports[0].postMessage("WOW!");
+ });
+ });
+
+ </script>
+ </body>
+</html>
diff --git a/dom/serviceworkers/test/skip_waiting_installed_worker.js b/dom/serviceworkers/test/skip_waiting_installed_worker.js
new file mode 100644
index 0000000000..a142576b9d
--- /dev/null
+++ b/dom/serviceworkers/test/skip_waiting_installed_worker.js
@@ -0,0 +1,6 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+self.addEventListener("install", evt => {
+ evt.waitUntil(self.skipWaiting());
+});
diff --git a/dom/serviceworkers/test/skip_waiting_scope/index.html b/dom/serviceworkers/test/skip_waiting_scope/index.html
new file mode 100644
index 0000000000..2b480d8707
--- /dev/null
+++ b/dom/serviceworkers/test/skip_waiting_scope/index.html
@@ -0,0 +1,33 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1131352 - Add ServiceWorkerGlobalScope skipWaiting()</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+<script class="testbody" type="text/javascript">
+
+ if (!parent) {
+ info("skip_waiting_scope/index.html shouldn't be launched directly!");
+ }
+
+ navigator.serviceWorker.oncontrollerchange = function() {
+ parent.postMessage({
+ event: "controllerchange",
+ controllerScriptURL: navigator.serviceWorker.controller &&
+ navigator.serviceWorker.controller.scriptURL
+ }, "*");
+ }
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/source_message_posting_worker.js b/dom/serviceworkers/test/source_message_posting_worker.js
new file mode 100644
index 0000000000..e48c005294
--- /dev/null
+++ b/dom/serviceworkers/test/source_message_posting_worker.js
@@ -0,0 +1,16 @@
+onmessage = function(e) {
+ if (!e.source) {
+ dump("ERROR: message doesn't have a source.");
+ }
+
+ if (!(e instanceof ExtendableMessageEvent)) {
+ e.source.postMessage("ERROR. event is not an extendable message event.");
+ }
+
+ // The client should be a window client
+ if (e.source instanceof WindowClient) {
+ e.source.postMessage(e.data);
+ } else {
+ e.source.postMessage("ERROR. source is not a window client.");
+ }
+};
diff --git a/dom/serviceworkers/test/storage_recovery_worker.sjs b/dom/serviceworkers/test/storage_recovery_worker.sjs
new file mode 100644
index 0000000000..9c9ce6a8d7
--- /dev/null
+++ b/dom/serviceworkers/test/storage_recovery_worker.sjs
@@ -0,0 +1,23 @@
+const BASE_URI = "http://mochi.test:8888/browser/dom/serviceworkers/test/";
+
+function handleRequest(request, response) {
+ let redirect = getState("redirect");
+ setState("redirect", "false");
+
+ if (request.queryString.includes("set-redirect")) {
+ setState("redirect", "true");
+ } else if (request.queryString.includes("clear-redirect")) {
+ setState("redirect", "false");
+ }
+
+ response.setHeader("Cache-Control", "no-store");
+
+ if (redirect === "true") {
+ response.setStatusLine(request.httpVersion, 307, "Moved Temporarily");
+ response.setHeader("Location", BASE_URI + "empty.js");
+ return;
+ }
+
+ response.setHeader("Content-Type", "application/javascript");
+ response.write("");
+}
diff --git a/dom/serviceworkers/test/streamfilter_server.sjs b/dom/serviceworkers/test/streamfilter_server.sjs
new file mode 100644
index 0000000000..0e5a62d1dd
--- /dev/null
+++ b/dom/serviceworkers/test/streamfilter_server.sjs
@@ -0,0 +1,9 @@
+Components.utils.importGlobalProperties(["URLSearchParams"]);
+
+function handleRequest(request, response) {
+ const searchParams = new URLSearchParams(request.queryString);
+
+ if (searchParams.get("syntheticResponse") === "0") {
+ response.write(String(searchParams));
+ }
+}
diff --git a/dom/serviceworkers/test/streamfilter_worker.js b/dom/serviceworkers/test/streamfilter_worker.js
new file mode 100644
index 0000000000..03a0f0a933
--- /dev/null
+++ b/dom/serviceworkers/test/streamfilter_worker.js
@@ -0,0 +1,9 @@
+onactivate = e => e.waitUntil(clients.claim());
+
+onfetch = e => {
+ const searchParams = new URL(e.request.url).searchParams;
+
+ if (searchParams.get("syntheticResponse") === "1") {
+ e.respondWith(new Response(String(searchParams)));
+ }
+};
diff --git a/dom/serviceworkers/test/strict_mode_warning.js b/dom/serviceworkers/test/strict_mode_warning.js
new file mode 100644
index 0000000000..38418de3d8
--- /dev/null
+++ b/dom/serviceworkers/test/strict_mode_warning.js
@@ -0,0 +1,4 @@
+function f() {
+ return 1;
+ return 2;
+}
diff --git a/dom/serviceworkers/test/sw_bad_mime_type.js b/dom/serviceworkers/test/sw_bad_mime_type.js
new file mode 100644
index 0000000000..f371807db9
--- /dev/null
+++ b/dom/serviceworkers/test/sw_bad_mime_type.js
@@ -0,0 +1 @@
+// I need some contents.
diff --git a/dom/serviceworkers/test/sw_bad_mime_type.js^headers^ b/dom/serviceworkers/test/sw_bad_mime_type.js^headers^
new file mode 100644
index 0000000000..a1f9e38d90
--- /dev/null
+++ b/dom/serviceworkers/test/sw_bad_mime_type.js^headers^
@@ -0,0 +1 @@
+Content-Type: text/plain
diff --git a/dom/serviceworkers/test/sw_clients/file_blob_upload_frame.html b/dom/serviceworkers/test/sw_clients/file_blob_upload_frame.html
new file mode 100644
index 0000000000..ff9803622a
--- /dev/null
+++ b/dom/serviceworkers/test/sw_clients/file_blob_upload_frame.html
@@ -0,0 +1,76 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>test file blob upload with SW interception</title>
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+<script class="testbody" type="text/javascript">
+
+if (!parent) {
+ dump("sw_clients/file_blob_upload_frame.html shouldn't be launched directly!");
+}
+
+function makeFileBlob(obj) {
+ return new Promise(function(resolve, reject) {
+
+ var request = indexedDB.open(window.location.pathname, 1);
+ request.onerror = reject;
+ request.onupgradeneeded = function(evt) {
+ var db = evt.target.result;
+ db.onerror = reject;
+
+ var objectStore = db.createObjectStore('test', { autoIncrement: true });
+ var index = objectStore.createIndex('test', 'index');
+ };
+
+ request.onsuccess = function(evt) {
+ var db = evt.target.result;
+ db.onerror = reject;
+
+ var blob = new Blob([JSON.stringify(obj)],
+ { type: 'application/json' });
+ var data = { blob, index: 5 };
+
+ objectStore = db.transaction('test', 'readwrite').objectStore('test');
+ objectStore.add(data).onsuccess = function(evt1) {
+ var key = evt1.target.result;
+ objectStore = db.transaction('test').objectStore('test');
+ objectStore.get(key).onsuccess = function(evt2) {
+ resolve(evt2.target.result.blob);
+ };
+ };
+ };
+ });
+}
+
+navigator.serviceWorker.ready.then(function() {
+ parent.postMessage({ status: 'READY' }, '*');
+});
+
+var URL = '/tests/dom/serviceworkers/test/redirect_post.sjs';
+
+addEventListener('message', function(evt) {
+ if (evt.data.type = 'TEST') {
+ makeFileBlob(evt.data.body).then(function(blob) {
+ return fetch(URL, { method: 'POST', body: blob });
+ }).then(function(response) {
+ return response.json();
+ }).then(function(result) {
+ parent.postMessage({ status: 'OK', result }, '*');
+ }).catch(function(e) {
+ parent.postMessage({ status: 'ERROR', result: e.toString() }, '*');
+ });
+ }
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/sw_clients/navigator.html b/dom/serviceworkers/test/sw_clients/navigator.html
new file mode 100644
index 0000000000..16a4fe9189
--- /dev/null
+++ b/dom/serviceworkers/test/sw_clients/navigator.html
@@ -0,0 +1,34 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 982726 - test match_all not crashing</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+<script class="testbody" type="text/javascript">
+
+ if (!parent) {
+ dump("sw_clients/navigator.html shouldn't be launched directly!\n");
+ }
+
+ window.addEventListener("message", function(event) {
+ if (event.data.type === "NAVIGATE") {
+ window.location = event.data.url;
+ }
+ });
+
+ navigator.serviceWorker.ready.then(function() {
+ parent.postMessage("NAVIGATOR_READY", "*");
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/sw_clients/refresher.html b/dom/serviceworkers/test/sw_clients/refresher.html
new file mode 100644
index 0000000000..b3c6e00152
--- /dev/null
+++ b/dom/serviceworkers/test/sw_clients/refresher.html
@@ -0,0 +1,38 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 982726 - test match_all not crashing</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <!-- some tests will intercept this bogus script request -->
+ <script type="text/javascript" src="does_not_exist.js"></script>
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+<script class="testbody" type="text/javascript">
+
+ if (!parent) {
+ dump("sw_clients/simple.html shouldn't be launched directly!");
+ }
+
+ window.addEventListener("message", function(event) {
+ if (event.data === "REFRESH") {
+ window.location.reload();
+ } else if (event.data === "FORCE_REFRESH") {
+ window.location.reload(true);
+ }
+ });
+
+ navigator.serviceWorker.ready.then(function() {
+ parent.postMessage("READY", "*");
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/sw_clients/refresher_cached.html b/dom/serviceworkers/test/sw_clients/refresher_cached.html
new file mode 100644
index 0000000000..4a91e46e99
--- /dev/null
+++ b/dom/serviceworkers/test/sw_clients/refresher_cached.html
@@ -0,0 +1,37 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 982726 - test match_all not crashing</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+<script class="testbody" type="text/javascript">
+
+ if (!parent) {
+ info("sw_clients/simple.html shouldn't be launched directly!");
+ }
+
+ window.addEventListener("message", function(event) {
+ if (event.data === "REFRESH") {
+ window.location.reload();
+ } else if (event.data === "FORCE_REFRESH") {
+ window.location.reload(true);
+ }
+ });
+
+ navigator.serviceWorker.ready.then(function() {
+ parent.postMessage("READY_CACHED", "*");
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/sw_clients/refresher_cached_compressed.html b/dom/serviceworkers/test/sw_clients/refresher_cached_compressed.html
new file mode 100644
index 0000000000..6b6a328211
--- /dev/null
+++ b/dom/serviceworkers/test/sw_clients/refresher_cached_compressed.html
Binary files differ
diff --git a/dom/serviceworkers/test/sw_clients/refresher_cached_compressed.html^headers^ b/dom/serviceworkers/test/sw_clients/refresher_cached_compressed.html^headers^
new file mode 100644
index 0000000000..4204d8601d
--- /dev/null
+++ b/dom/serviceworkers/test/sw_clients/refresher_cached_compressed.html^headers^
@@ -0,0 +1,2 @@
+Content-Type: text/html
+Content-Encoding: gzip
diff --git a/dom/serviceworkers/test/sw_clients/refresher_compressed.html b/dom/serviceworkers/test/sw_clients/refresher_compressed.html
new file mode 100644
index 0000000000..e0861a5180
--- /dev/null
+++ b/dom/serviceworkers/test/sw_clients/refresher_compressed.html
Binary files differ
diff --git a/dom/serviceworkers/test/sw_clients/refresher_compressed.html^headers^ b/dom/serviceworkers/test/sw_clients/refresher_compressed.html^headers^
new file mode 100644
index 0000000000..4204d8601d
--- /dev/null
+++ b/dom/serviceworkers/test/sw_clients/refresher_compressed.html^headers^
@@ -0,0 +1,2 @@
+Content-Type: text/html
+Content-Encoding: gzip
diff --git a/dom/serviceworkers/test/sw_clients/service_worker_controlled.html b/dom/serviceworkers/test/sw_clients/service_worker_controlled.html
new file mode 100644
index 0000000000..e0d7bce573
--- /dev/null
+++ b/dom/serviceworkers/test/sw_clients/service_worker_controlled.html
@@ -0,0 +1,38 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>controlled page</title>
+ <!--
+ Paged controlled by a service worker for testing matchAll().
+ See bug 982726, 1058311.
+ -->
+<script class="testbody" type="text/javascript">
+ function fail(msg) {
+ info("service_worker_controlled.html: " + msg);
+ opener.postMessage("FAIL", "*");
+ }
+
+ if (!parent) {
+ info("service_worker_controlled.html should not be launched directly!");
+ }
+
+ window.onload = function() {
+ navigator.serviceWorker.ready.then(function(swr) {
+ parent.postMessage("READY", "*");
+ });
+ }
+
+ navigator.serviceWorker.onmessage = function(msg) {
+ // forward message to the test page.
+ parent.postMessage(msg.data, "*");
+ };
+</script>
+
+</head>
+<body>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/sw_clients/simple.html b/dom/serviceworkers/test/sw_clients/simple.html
new file mode 100644
index 0000000000..bbe6782e2a
--- /dev/null
+++ b/dom/serviceworkers/test/sw_clients/simple.html
@@ -0,0 +1,29 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 982726 - test match_all not crashing</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+<script class="testbody" type="text/javascript">
+
+ if (!parent) {
+ info("sw_clients/simple.html shouldn't be launched directly!");
+ }
+
+ navigator.serviceWorker.ready.then(function() {
+ parent.postMessage("READY", "*");
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/sw_file_upload.js b/dom/serviceworkers/test/sw_file_upload.js
new file mode 100644
index 0000000000..c35269924b
--- /dev/null
+++ b/dom/serviceworkers/test/sw_file_upload.js
@@ -0,0 +1,16 @@
+self.skipWaiting();
+
+addEventListener("fetch", event => {
+ const url = new URL(event.request.url);
+ const params = new URLSearchParams(url.search);
+
+ if (params.get("clone") === "1") {
+ event.respondWith(fetch(event.request.clone()));
+ } else {
+ event.respondWith(fetch(event.request));
+ }
+});
+
+addEventListener("activate", function(event) {
+ event.waitUntil(clients.claim());
+});
diff --git a/dom/serviceworkers/test/sw_respondwith_serviceworker.js b/dom/serviceworkers/test/sw_respondwith_serviceworker.js
new file mode 100644
index 0000000000..6ddbc3d5c1
--- /dev/null
+++ b/dom/serviceworkers/test/sw_respondwith_serviceworker.js
@@ -0,0 +1,24 @@
+const SERVICEWORKER_DOC = `<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <script src="utils.js" type="text/javascript"></script>
+</head>
+<body>
+SERVICEWORKER
+</body>
+</html>
+`;
+
+const SERVICEWORKER_RESPONSE = new Response(SERVICEWORKER_DOC, {
+ headers: { "content-type": "text/html" },
+});
+
+addEventListener("fetch", event => {
+ // Allow utils.js which we explicitly include to be loaded by resetting
+ // interception.
+ if (event.request.url.endsWith("/utils.js")) {
+ return;
+ }
+ event.respondWith(SERVICEWORKER_RESPONSE.clone());
+});
diff --git a/dom/serviceworkers/test/sw_storage_not_allow.js b/dom/serviceworkers/test/sw_storage_not_allow.js
new file mode 100644
index 0000000000..c0bd782326
--- /dev/null
+++ b/dom/serviceworkers/test/sw_storage_not_allow.js
@@ -0,0 +1,33 @@
+let clientId;
+addEventListener("fetch", function(event) {
+ event.respondWith(
+ (async function() {
+ if (event.request.url.includes("getClients")) {
+ // Expected to fail since the storage access is not allowed.
+ try {
+ await self.clients.matchAll();
+ } catch (e) {
+ // expected failure
+ }
+ } else if (event.request.url.includes("getClient-stage1")) {
+ let clients = await self.clients.matchAll();
+ clientId = clients[0].id;
+ } else if (event.request.url.includes("getClient-stage2")) {
+ // Expected to fail since the storage access is not allowed.
+ try {
+ await self.clients.get(clientId);
+ } catch (e) {
+ // expected failure
+ }
+ }
+
+ // Pass through the network request once our various Clients API
+ // promises have completed.
+ return await fetch(event.request);
+ })()
+ );
+});
+
+addEventListener("activate", function(event) {
+ event.waitUntil(clients.claim());
+});
diff --git a/dom/serviceworkers/test/sw_with_navigationPreload.js b/dom/serviceworkers/test/sw_with_navigationPreload.js
new file mode 100644
index 0000000000..75e06787cd
--- /dev/null
+++ b/dom/serviceworkers/test/sw_with_navigationPreload.js
@@ -0,0 +1,28 @@
+addEventListener("activate", event => {
+ event.waitUntil(self.registration.navigationPreload.enable());
+});
+
+async function post_to_page(data) {
+ let cs = await self.clients.matchAll();
+ for (const client of cs) {
+ client.postMessage(data);
+ }
+}
+
+addEventListener("fetch", event => {
+ if (event.request.url.includes("navigationPreload_page.html")) {
+ event.respondWith(
+ new Response("<!DOCTYPE html>", {
+ headers: { "Content-Type": "text/html; charset=utf-8" },
+ })
+ );
+
+ event.waitUntil(
+ (async function() {
+ let preloadResponse = await event.preloadResponse;
+ let text = await preloadResponse.text();
+ await post_to_page(text);
+ })()
+ );
+ }
+});
diff --git a/dom/serviceworkers/test/swa/worker_scope_different.js b/dom/serviceworkers/test/swa/worker_scope_different.js
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/dom/serviceworkers/test/swa/worker_scope_different.js
diff --git a/dom/serviceworkers/test/swa/worker_scope_different.js^headers^ b/dom/serviceworkers/test/swa/worker_scope_different.js^headers^
new file mode 100644
index 0000000000..e85a7f09de
--- /dev/null
+++ b/dom/serviceworkers/test/swa/worker_scope_different.js^headers^
@@ -0,0 +1 @@
+Service-Worker-Allowed: different/path
diff --git a/dom/serviceworkers/test/swa/worker_scope_different2.js b/dom/serviceworkers/test/swa/worker_scope_different2.js
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/dom/serviceworkers/test/swa/worker_scope_different2.js
diff --git a/dom/serviceworkers/test/swa/worker_scope_different2.js^headers^ b/dom/serviceworkers/test/swa/worker_scope_different2.js^headers^
new file mode 100644
index 0000000000..e37307d666
--- /dev/null
+++ b/dom/serviceworkers/test/swa/worker_scope_different2.js^headers^
@@ -0,0 +1 @@
+Service-Worker-Allowed: /different/path
diff --git a/dom/serviceworkers/test/swa/worker_scope_precise.js b/dom/serviceworkers/test/swa/worker_scope_precise.js
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/dom/serviceworkers/test/swa/worker_scope_precise.js
diff --git a/dom/serviceworkers/test/swa/worker_scope_precise.js^headers^ b/dom/serviceworkers/test/swa/worker_scope_precise.js^headers^
new file mode 100644
index 0000000000..7488cafbb0
--- /dev/null
+++ b/dom/serviceworkers/test/swa/worker_scope_precise.js^headers^
@@ -0,0 +1 @@
+Service-Worker-Allowed: /tests/dom/serviceworkers/test/swa
diff --git a/dom/serviceworkers/test/swa/worker_scope_too_deep.js b/dom/serviceworkers/test/swa/worker_scope_too_deep.js
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/dom/serviceworkers/test/swa/worker_scope_too_deep.js
diff --git a/dom/serviceworkers/test/swa/worker_scope_too_deep.js^headers^ b/dom/serviceworkers/test/swa/worker_scope_too_deep.js^headers^
new file mode 100644
index 0000000000..9a66c3d153
--- /dev/null
+++ b/dom/serviceworkers/test/swa/worker_scope_too_deep.js^headers^
@@ -0,0 +1 @@
+Service-Worker-Allowed: /tests/dom/serviceworkers/test/swa/deep/way/too/specific
diff --git a/dom/serviceworkers/test/swa/worker_scope_too_narrow.js b/dom/serviceworkers/test/swa/worker_scope_too_narrow.js
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/dom/serviceworkers/test/swa/worker_scope_too_narrow.js
diff --git a/dom/serviceworkers/test/swa/worker_scope_too_narrow.js^headers^ b/dom/serviceworkers/test/swa/worker_scope_too_narrow.js^headers^
new file mode 100644
index 0000000000..407361a3c7
--- /dev/null
+++ b/dom/serviceworkers/test/swa/worker_scope_too_narrow.js^headers^
@@ -0,0 +1 @@
+Service-Worker-Allowed: /tests/dom/serviceworkers
diff --git a/dom/serviceworkers/test/test_abrupt_completion.html b/dom/serviceworkers/test/test_abrupt_completion.html
new file mode 100644
index 0000000000..bbf9e965f0
--- /dev/null
+++ b/dom/serviceworkers/test/test_abrupt_completion.html
@@ -0,0 +1,144 @@
+<!doctype html>
+<meta charset=utf-8>
+<title></title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script>
+
+// Tests a _registered_ ServiceWorker whose script evaluation results in an
+// "abrupt completion", e.g. threw an uncaught exception. Such a ServiceWorker's
+// first script evaluation must result in a "normal completion", however, for
+// the Update algorithm to not abort in its step 18 when registering:
+//
+// 18. If runResult is failure or an abrupt completion, then: [...]
+
+const script = "./abrupt_completion_worker.js";
+const scope = "./empty.html";
+const expectedMessage = "handler-before-throw";
+let registration = null;
+
+// Should only be called once registration.active is non-null. Uses
+// implementation details by zero-ing the "idle timeout"s and then sending an
+// event to the ServiceWorker, which should immediately cause its termination.
+// The idle timeouts are restored after the ServiceWorker is terminated.
+async function startAndStopServiceWorker() {
+ SpecialPowers.registerObservers("service-worker-shutdown");
+
+ const spTopic = "specialpowers-service-worker-shutdown";
+
+ const origIdleTimeout =
+ SpecialPowers.getIntPref("dom.serviceWorkers.idle_timeout");
+
+ const origIdleExtendedTimeout =
+ SpecialPowers.getIntPref("dom.serviceWorkers.idle_extended_timeout");
+
+ await new Promise(resolve => {
+ const observer = {
+ async observe(subject, topic, data) {
+ if (topic !== spTopic) {
+ return;
+ }
+
+ SpecialPowers.removeObserver(observer, spTopic);
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.serviceWorkers.idle_timeout", origIdleTimeout],
+ ["dom.serviceWorkers.idle_extended_timeout", origIdleExtendedTimeout]
+ ]
+ });
+
+ resolve();
+ },
+ };
+
+ // Speed things up.
+ SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.serviceWorkers.idle_timeout", 0],
+ ["dom.serviceWorkers.idle_extended_timeout", 0]
+ ]
+ }).then(() => {
+ SpecialPowers.addObserver(observer, spTopic);
+
+ registration.active.postMessage("");
+ });
+ });
+}
+
+// eslint-disable-next-line mozilla/no-addtask-setup
+add_task(async function setup() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true]
+ ]
+ });
+
+ registration = await navigator.serviceWorker.register(script, { scope });
+ SimpleTest.registerCleanupFunction(async function unregisterRegistration() {
+ await registration.unregister();
+ });
+
+ await new Promise(resolve => {
+ const serviceWorker = registration.installing;
+
+ serviceWorker.onstatechange = () => {
+ if (serviceWorker.state === "activated") {
+ resolve();
+ }
+ };
+ });
+
+ ok(registration.active instanceof ServiceWorker, "ServiceWorker is activated");
+});
+
+// We expect that the restarted SW that experiences an abrupt completion at
+// startup after adding its message handler 1) will be active in order to
+// respond to our postMessage and 2) will respond with the global value set
+// prior to the importScripts call that throws (and not the global value that
+// would have been assigned after the importScripts call if it didn't throw).
+add_task(async function testMessageHandler() {
+ await startAndStopServiceWorker();
+
+ await new Promise(resolve => {
+ navigator.serviceWorker.onmessage = e => {
+ is(e.data, expectedMessage, "Correct message handler");
+ resolve();
+ };
+ registration.active.postMessage("");
+ });
+});
+
+// We expect that the restarted SW that experiences an abrupt completion at
+// startup before adding its "fetch" listener will 1) successfully dispatch the
+// event and 2) it will not be handled (respondWith() will not be called) so
+// interception will be reset and the response will contain the contents of
+// empty.html. Before the fix in bug 1603484 the SW would fail to properly start
+// up and the fetch event would result in a NetworkError, breaking the
+// controlled page.
+add_task(async function testFetchHandler() {
+ await startAndStopServiceWorker();
+
+ const iframe = document.createElement("iframe");
+ SimpleTest.registerCleanupFunction(function removeIframe() {
+ iframe.remove();
+ });
+
+ await new Promise(resolve => {
+ iframe.src = scope;
+ iframe.onload = resolve;
+ document.body.appendChild(iframe);
+ });
+
+ const response = await iframe.contentWindow.fetch(scope);
+
+ // NetworkError will have a status of 0, which is not "ok", and this is
+ // a stronger guarantee that should be true instead of just checking if there
+ // isn't a NetworkError.
+ ok(response.ok, "Fetch succeeded and didn't result in a NetworkError");
+
+ const text = await response.text();
+ is(text, "", "Correct response text");
+});
+
+</script>
diff --git a/dom/serviceworkers/test/test_async_waituntil.html b/dom/serviceworkers/test/test_async_waituntil.html
new file mode 100644
index 0000000000..8c15eb2b11
--- /dev/null
+++ b/dom/serviceworkers/test/test_async_waituntil.html
@@ -0,0 +1,91 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+ Test that:
+ 1. waitUntil() waits for each individual promise separately, even if
+ one of them was rejected.
+ 2. waitUntil() can be called asynchronously as long as there is still
+ a pending extension promise.
+ -->
+<head>
+ <title>Test for Bug 1263304</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="error_reporting_helpers.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <meta http-equiv="Content-type" content="text/html;charset=UTF-8">
+</head>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1263304">Mozilla Bug 1263304</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+
+<script src="utils.js"></script>
+<script class="testbody" type="text/javascript">
+add_task(function setupPrefs() {
+ return SpecialPowers.pushPrefEnv({"set": [
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ ]});
+});
+
+function wait_for_message(expected_message) {
+ return new Promise(function(resolve, reject) {
+ navigator.serviceWorker.onmessage = function(event) {
+ navigator.serviceWorker.onmessage = null;
+ ok(event.data === expected_message, "Received expected message event: " + event.data);
+ resolve();
+ }
+ });
+}
+
+add_task(async function async_wait_until() {
+ var worker;
+ let registration = await navigator.serviceWorker.register(
+ "async_waituntil_worker.js", { scope: "./"} )
+ .then(function(reg) {
+ worker = reg.installing;
+ return waitForState(worker, 'activated', reg);
+ });
+
+ // The service worker will claim us when it becomes active.
+ ok(navigator.serviceWorker.controller, "Controlled");
+
+ // This will make the service worker die immediately if there are no pending
+ // waitUntil promises to keep it alive.
+ await SpecialPowers.pushPrefEnv({"set": [
+ ["dom.serviceWorkers.idle_timeout", 0],
+ ["dom.serviceWorkers.idle_extended_timeout", 299999]]});
+
+ // The service worker will wait on two promises, one of which
+ // will be rejected. We check whether the SW is killed using
+ // the value of a global variable.
+ let waitForStart = wait_for_message("Started");
+ worker.postMessage("Start");
+ await waitForStart;
+
+ await new Promise((res, rej) => {
+ setTimeout(res, 0);
+ });
+
+ let waitResult = wait_for_message("Success");
+ worker.postMessage("Result");
+ await waitResult;
+
+ // Test the behaviour of calling waitUntil asynchronously. The important
+ // part is that we receive the message event.
+ let waitForMessage = wait_for_message("Done");
+ await fetch("doesnt_exist.html").then(() => {
+ ok(true, "Fetch was successful.");
+ });
+ await waitForMessage;
+
+ await SpecialPowers.popPrefEnv();
+ await SpecialPowers.popPrefEnv();
+ await registration.unregister();
+});
+</script>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/test_bad_script_cache.html b/dom/serviceworkers/test/test_bad_script_cache.html
new file mode 100644
index 0000000000..7919802678
--- /dev/null
+++ b/dom/serviceworkers/test/test_bad_script_cache.html
@@ -0,0 +1,96 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test updating a service worker with a bad script cache.</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<script src='utils.js'></script>
+<script class="testbody" type="text/javascript">
+
+async function deleteCaches(cacheStorage) {
+ let keyList = await cacheStorage.keys();
+ let promiseList = [];
+ keyList.forEach(key => {
+ promiseList.push(cacheStorage.delete(key));
+ });
+ return await Promise.all(keyList);
+}
+
+function waitForUpdate(reg) {
+ return new Promise(resolve => {
+ reg.addEventListener('updatefound', resolve, { once: true });
+ });
+}
+
+async function runTest() {
+ let reg;
+ try {
+ const script = 'update_worker.sjs';
+ const scope = 'bad-script-cache';
+
+ reg = await navigator.serviceWorker.register(script, { scope });
+ await waitForState(reg.installing, 'activated');
+
+ // Verify the service worker script cache has the worker script stored.
+ let chromeCaches = SpecialPowers.createChromeCache('chrome', window.origin);
+ let scriptURL = new URL(script, window.location.href);
+ let response = await chromeCaches.match(scriptURL.href);
+ is(response.url, scriptURL.href, 'worker script should be stored');
+
+ // Force delete the service worker script out from under the service worker.
+ // Note: Prefs are set to kill the SW thread immediately on idle.
+ await deleteCaches(chromeCaches);
+
+ // Verify the service script cache no longer knows about the worker script.
+ response = await chromeCaches.match(scriptURL.href);
+ is(response, undefined, 'worker script should not be stored');
+
+ // Force an update and wait for it to fire an update event.
+ reg.update();
+ await waitForUpdate(reg);
+ await waitForState(reg.installing, 'activated');
+
+ // Verify that the script cache knows about the worker script again.
+ response = await chromeCaches.match(scriptURL.href);
+ is(response.url, scriptURL.href, 'worker script should be stored');
+ } catch (e) {
+ ok(false, e);
+ }
+ if (reg) {
+ await reg.unregister();
+ }
+
+ // If this test is run on windows and the process shuts down immediately after, then
+ // we may fail to remove some of the Cache API body files. This is because the GC
+ // runs late causing Cache API to cleanup after shutdown begins. It seems something
+ // during shutdown scans these files and conflicts with removing the file on windows.
+ //
+ // To avoid this we perform an explict GC here to ensure that Cache API can cleanup
+ // earlier.
+ await new Promise(resolve => SpecialPowers.exactGC(resolve));
+
+ SimpleTest.finish();
+}
+
+SimpleTest.waitForExplicitFinish();
+SpecialPowers.pushPrefEnv({"set": [
+ // standard prefs
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ ["dom.caches.enabled", true],
+
+ // immediately kill the service worker thread when idle
+ ["dom.serviceWorkers.idle_timeout", 0],
+
+]}, runTest);
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/test_bug1151916.html b/dom/serviceworkers/test/test_bug1151916.html
new file mode 100644
index 0000000000..b541129ccb
--- /dev/null
+++ b/dom/serviceworkers/test/test_bug1151916.html
@@ -0,0 +1,104 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1151916 - Test principal is set on cached serviceworkers</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+<!--
+ If the principal is not set, accessing self.caches in the worker will crash.
+-->
+</head>
+<body>
+<p id="display"></p>
+<div id="content"></div>
+<pre id="test"></pre>
+<script class="testbody" type="text/javascript">
+
+ var frame;
+
+ function listenForMessage() {
+ var p = new Promise(function(resolve, reject) {
+ window.onmessage = function(e) {
+ if (e.data.status == "failed") {
+ ok(false, "iframe had error " + e.data.message);
+ reject(e.data.message);
+ } else if (e.data.status == "success") {
+ ok(true, "iframe step success " + e.data.message);
+ resolve(e.data.message);
+ } else {
+ ok(false, "Unexpected message " + e.data);
+ reject();
+ }
+ }
+ });
+
+ return p;
+ }
+
+ // We have the iframe register for its own scope so that this page is not
+ // holding any references when we GC.
+ function register() {
+ var p = listenForMessage();
+
+ frame = document.createElement("iframe");
+ document.body.appendChild(frame);
+ frame.src = "bug1151916_driver.html";
+
+ return p;
+ }
+
+ function unloadFrame() {
+ frame.src = "about:blank";
+ frame.remove();
+ frame = null;
+ }
+
+ function gc() {
+ return new Promise(function(resolve) {
+ SpecialPowers.exactGC(resolve);
+ });
+ }
+
+ function testCaches() {
+ var p = listenForMessage();
+
+ frame = document.createElement("iframe");
+ document.body.appendChild(frame);
+ frame.src = "bug1151916_driver.html";
+
+ return p;
+ }
+
+ function unregister() {
+ return navigator.serviceWorker.getRegistration("./bug1151916_driver.html").then(function(reg) {
+ ok(reg instanceof ServiceWorkerRegistration, "Must have valid registration.");
+ return reg.unregister();
+ });
+ }
+
+ function runTest() {
+ register()
+ .then(unloadFrame)
+ .then(gc)
+ .then(testCaches)
+ .then(unregister)
+ .catch(function(e) {
+ ok(false, "Some test failed with error " + e);
+ }).then(SimpleTest.finish);
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ SpecialPowers.pushPrefEnv({"set": [
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ ["dom.caches.enabled", true],
+ ]}, runTest);
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/test_bug1240436.html b/dom/serviceworkers/test/test_bug1240436.html
new file mode 100644
index 0000000000..8b76ada6a8
--- /dev/null
+++ b/dom/serviceworkers/test/test_bug1240436.html
@@ -0,0 +1,34 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for encoding of service workers</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+<script class="testbody" type="text/javascript">
+ SimpleTest.waitForExplicitFinish();
+
+ function runTest() {
+ navigator.serviceWorker.register("bug1240436_worker.js")
+ .then(reg => reg.unregister())
+ .then(() => ok(true, "service worker register script succeed"))
+ .catch(err => ok(false, "service worker register script faled " + err))
+ .then(() => SimpleTest.finish());
+ }
+
+ SpecialPowers.pushPrefEnv({"set": [
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true]
+ ]}, runTest);
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/test_bug1408734.html b/dom/serviceworkers/test/test_bug1408734.html
new file mode 100644
index 0000000000..27559e695f
--- /dev/null
+++ b/dom/serviceworkers/test/test_bug1408734.html
@@ -0,0 +1,52 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1408734</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="error_reporting_helpers.js"></script>
+ <script src="utils.js"></script>
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+<script class="testbody" type="text/javascript">
+
+// setup prefs
+add_task(() => {
+ return SpecialPowers.pushPrefEnv({"set": [
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ ]});
+});
+
+// test for bug 1408734
+add_task(async () => {
+ // register a service worker
+ let registration = await navigator.serviceWorker.register("fetch.js",
+ {scope: "./"});
+ // wait for service worker be activated
+ await waitForState(registration.installing, "activated");
+
+ // get the ServiceWorkerRegistration we just register through GetRegistration
+ registration = await navigator.serviceWorker.getRegistration("./");
+ ok(registration, "should get the registration under scope './'");
+
+ // call unregister()
+ await registration.unregister();
+
+ // access registration.updateViaCache to trigger the bug
+ // we really care that we don't crash. In the future we will fix
+ is(registration.updateViaCache, "imports",
+ "registration.updateViaCache should work after unregister()");
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/test_claim.html b/dom/serviceworkers/test/test_claim.html
new file mode 100644
index 0000000000..e72f1173e8
--- /dev/null
+++ b/dom/serviceworkers/test/test_claim.html
@@ -0,0 +1,171 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1130684 - Test service worker clients claim onactivate </title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+<script class="testbody" type="text/javascript">
+ var registration_1;
+ var registration_2;
+ var client;
+
+ function register_1() {
+ return navigator.serviceWorker.register("claim_worker_1.js",
+ { scope: "./" })
+ .then((swr) => registration_1 = swr);
+ }
+
+ function register_2() {
+ return navigator.serviceWorker.register("claim_worker_2.js",
+ { scope: "./claim_clients/client.html" })
+ .then((swr) => registration_2 = swr);
+ }
+
+ function unregister(reg) {
+ return reg.unregister().then(function(result) {
+ ok(result, "Unregister should return true.");
+ });
+ }
+
+ function createClient() {
+ var p = new Promise(function(res, rej) {
+ window.onmessage = function(e) {
+ if (e.data === "READY") {
+ res();
+ }
+ }
+ });
+
+ content = document.getElementById("content");
+ ok(content, "parent exists.");
+
+ client = document.createElement("iframe");
+ client.setAttribute('src', "claim_clients/client.html");
+ content.appendChild(client);
+
+ return p;
+ }
+
+ function testController() {
+ ok(navigator.serviceWorker.controller.scriptURL.match("claim_worker_1"),
+ "Controlling service worker has the correct url.");
+ }
+
+ function testClientWasClaimed(expected) {
+ var resolveClientMessage, resolveClientControllerChange;
+ var messageFromClient = new Promise(function(res, rej) {
+ resolveClientMessage = res;
+ });
+ var controllerChangeFromClient = new Promise(function(res, rej) {
+ resolveClientControllerChange = res;
+ });
+ window.onmessage = function(e) {
+ if (!e.data.event) {
+ ok(false, "Unknown message received: " + e.data);
+ }
+
+ if (e.data.event === "controllerchange") {
+ ok(e.data.controller,
+ "Client was claimed and received controllerchange event.");
+ resolveClientControllerChange();
+ }
+
+ if (e.data.event === "message") {
+ ok(e.data.data.resolve_value === undefined,
+ "Claim should resolve with undefined.");
+ ok(e.data.data.message === expected.message,
+ "Client received message from claiming worker.");
+ ok(e.data.data.match_count_before === expected.match_count_before,
+ "MatchAll clients count before claim should be " + expected.match_count_before);
+ ok(e.data.data.match_count_after === expected.match_count_after,
+ "MatchAll clients count after claim should be " + expected.match_count_after);
+ resolveClientMessage();
+ }
+ }
+
+ return Promise.all([messageFromClient, controllerChangeFromClient])
+ .then(() => window.onmessage = null);
+ }
+
+ function testClaimFirstWorker() {
+ // wait for the worker to control us
+ var controllerChange = new Promise(function(res, rej) {
+ navigator.serviceWorker.oncontrollerchange = function(e) {
+ ok(true, "controller changed event received.");
+ res();
+ };
+ });
+
+ var messageFromWorker = new Promise(function(res, rej) {
+ navigator.serviceWorker.onmessage = function(e) {
+ ok(e.data.resolve_value === undefined,
+ "Claim should resolve with undefined.");
+ ok(e.data.message === "claim_worker_1",
+ "Received message from claiming worker.");
+ ok(e.data.match_count_before === 0,
+ "Worker doesn't control any client before claim.");
+ ok(e.data.match_count_after === 2, "Worker should claim 2 clients.");
+ res();
+ }
+ });
+
+ var clientClaim = testClientWasClaimed({
+ message: "claim_worker_1",
+ match_count_before: 0,
+ match_count_after: 2
+ });
+
+ return Promise.all([controllerChange, messageFromWorker, clientClaim])
+ .then(testController);
+ }
+
+ function testClaimSecondWorker() {
+ navigator.serviceWorker.oncontrollerchange = function(e) {
+ ok(false, "Claim_worker_2 shouldn't claim this window.");
+ }
+
+ navigator.serviceWorker.onmessage = function(e) {
+ ok(false, "Claim_worker_2 shouldn't claim this window.");
+ }
+
+ var clientClaim = testClientWasClaimed({
+ message: "claim_worker_2",
+ match_count_before: 0,
+ match_count_after: 1
+ });
+
+ return clientClaim.then(testController);
+ }
+
+ function runTest() {
+ createClient()
+ .then(register_1)
+ .then(testClaimFirstWorker)
+ .then(register_2)
+ .then(testClaimSecondWorker)
+ .then(function() { return unregister(registration_1); })
+ .then(function() { return unregister(registration_2); })
+ .catch(function(e) {
+ ok(false, "Some test failed with error " + e);
+ }).then(SimpleTest.finish);
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ SpecialPowers.pushPrefEnv({"set": [
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true]
+ ]}, runTest);
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/test_claim_oninstall.html b/dom/serviceworkers/test/test_claim_oninstall.html
new file mode 100644
index 0000000000..54933405ce
--- /dev/null
+++ b/dom/serviceworkers/test/test_claim_oninstall.html
@@ -0,0 +1,77 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1130684 - Test service worker clients.claim oninstall</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+<script class="testbody" type="text/javascript">
+ var registration;
+
+ function register() {
+ return navigator.serviceWorker.register("claim_oninstall_worker.js",
+ { scope: "./" })
+ .then((swr) => registration = swr);
+ }
+
+
+ function unregister() {
+ return registration.unregister().then(function(result) {
+ ok(result, "Unregister should return true.");
+ });
+ }
+
+ function testClaim() {
+ ok(registration.installing, "Worker should be in installing state");
+
+ navigator.serviceWorker.oncontrollerchange = function() {
+ ok(false, "Claim should not succeed when the worker is not active.");
+ }
+
+ var p = new Promise(function(res, rej) {
+ var worker = registration.installing;
+ worker.onstatechange = function(e) {
+ if (worker.state === 'installed') {
+ is(worker, registration.waiting, "Worker should be in waiting state");
+ } else if (worker.state === 'activated') {
+ // The worker will become active only if claim will reject inside the
+ // install handler.
+ is(worker, registration.active,
+ "Claim should reject if the worker is not active");
+ ok(navigator.serviceWorker.controller === null, "Client is not controlled.");
+ e.target.onstatechange = null;
+ res();
+ }
+ }
+ });
+
+ return p;
+ }
+
+ function runTest() {
+ register()
+ .then(testClaim)
+ .then(unregister)
+ .catch(function(e) {
+ ok(false, "Some test failed with error " + e);
+ }).then(SimpleTest.finish);
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ SpecialPowers.pushPrefEnv({"set": [
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true]
+ ]}, runTest);
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/test_controller.html b/dom/serviceworkers/test/test_controller.html
new file mode 100644
index 0000000000..c0e220a36e
--- /dev/null
+++ b/dom/serviceworkers/test/test_controller.html
@@ -0,0 +1,83 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1002570 - test controller instance.</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+<script src="utils.js"></script>
+<script class="testbody" type="text/javascript">
+
+ var content;
+ var iframe;
+ var registration;
+
+ function simpleRegister() {
+ // We use the control scope for the less specific registration. The window will register a worker on controller/
+ return navigator.serviceWorker.register("worker.js", { scope: "./control" })
+ .then(swr => waitForState(swr.installing, 'activated', swr))
+ .then(swr => registration = swr);
+ }
+
+ function unregister() {
+ return registration.unregister().then(function(result) {
+ ok(result, "Unregister should return true.");
+ }, function(e) {
+ dump("Unregistering the SW failed: " + e + "\n");
+ });
+ }
+
+ function testController() {
+ var p = new Promise(function(resolve, reject) {
+ window.onmessage = function(e) {
+ if (e.data.status == "ok") {
+ ok(e.data.result, e.data.message);
+ } else if (e.data.status == "done") {
+ window.onmessage = null;
+ content.removeChild(iframe);
+ resolve();
+ }
+ }
+ });
+
+ content = document.getElementById("content");
+ ok(content, "Parent exists.");
+
+ iframe = document.createElement("iframe");
+ iframe.setAttribute('src', "controller/index.html");
+ content.appendChild(iframe);
+
+ return p;
+ }
+
+ // This document just flips the prefs and opens the iframe for the actual test.
+ function runTest() {
+ simpleRegister()
+ .then(testController)
+ .then(unregister)
+ .then(function() {
+ SimpleTest.finish();
+ }).catch(function(e) {
+ ok(false, "Some test failed with error " + e);
+ SimpleTest.finish();
+ });
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ SpecialPowers.pushPrefEnv({"set": [
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true]
+ ]}, runTest);
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/test_cookie_fetch.html b/dom/serviceworkers/test/test_cookie_fetch.html
new file mode 100644
index 0000000000..8c4324c759
--- /dev/null
+++ b/dom/serviceworkers/test/test_cookie_fetch.html
@@ -0,0 +1,64 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1331680 - test access to cookies in the documents synthesized from service worker responses</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+<iframe></iframe>
+</div>
+<pre id="test"></pre>
+<script class="testbody" type="text/javascript">
+
+ var iframe;
+ function runTest() {
+ iframe = document.querySelector("iframe");
+ iframe.src = "https://example.com/tests/dom/serviceworkers/test/fetch/cookie/register.html";
+ window.onmessage = function(e) {
+ if (e.data.status == "ok") {
+ ok(e.data.result, e.data.message);
+ } else if (e.data.status == "registrationdone") {
+ // Remove the iframe and recreate a new one to ensure that any traces
+ // of the cookies have been removed from the child process.
+ iframe.remove();
+ iframe = document.createElement("iframe");
+ document.getElementById("content").appendChild(iframe);
+
+ iframe.src = "https://example.com/tests/dom/serviceworkers/test/fetch/cookie/synth.html";
+ } else if (e.data.status == "done") {
+ // Note, we can't do an exact is() comparison here since other
+ // tests can leave cookies on the domain.
+ ok(e.data.cookie.includes("foo=bar"),
+ "The synthesized document has access to its cookies");
+
+ iframe.src = "https://example.com/tests/dom/serviceworkers/test/fetch/cookie/unregister.html";
+ } else if (e.data.status == "unregistrationdone") {
+ window.onmessage = null;
+ ok(true, "Test finished successfully");
+ SpecialPowers.clearUserPref("network.cookie.sameSite.laxByDefault");
+ SimpleTest.finish();
+ }
+ };
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ onload = function() {
+ SpecialPowers.pushPrefEnv({"set": [
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ // Bug 1617611: Fix all the tests broken by "cookies SameSite=lax by default"
+ ["network.cookie.sameSite.laxByDefault", false],
+ ]}, runTest);
+ };
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/test_cross_origin_url_after_redirect.html b/dom/serviceworkers/test/test_cross_origin_url_after_redirect.html
new file mode 100644
index 0000000000..bfd4f700be
--- /dev/null
+++ b/dom/serviceworkers/test/test_cross_origin_url_after_redirect.html
@@ -0,0 +1,50 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test access to a cross origin Request.url property from a service worker for a redirected intercepted iframe</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+<iframe></iframe>
+</div>
+<pre id="test"></pre>
+<script class="testbody" type="text/javascript">
+
+ var iframe;
+ function runTest() {
+ iframe = document.querySelector("iframe");
+ iframe.src = "/tests/dom/serviceworkers/test/fetch/requesturl/register.html";
+ window.onmessage = function(e) {
+ if (e.data.status == "ok") {
+ ok(e.data.result, e.data.message);
+ } else if (e.data.status == "registrationdone") {
+ iframe.src = "/tests/dom/serviceworkers/test/fetch/requesturl/index.html";
+ } else if (e.data.status == "done") {
+ iframe.src = "/tests/dom/serviceworkers/test/fetch/requesturl/unregister.html";
+ } else if (e.data.status == "unregistrationdone") {
+ window.onmessage = null;
+ ok(true, "Test finished successfully");
+ SimpleTest.finish();
+ }
+ };
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ onload = function() {
+ SpecialPowers.pushPrefEnv({"set": [
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ ]}, runTest);
+ };
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/test_csp_upgrade-insecure_intercept.html b/dom/serviceworkers/test/test_csp_upgrade-insecure_intercept.html
new file mode 100644
index 0000000000..b5ddbb97b6
--- /dev/null
+++ b/dom/serviceworkers/test/test_csp_upgrade-insecure_intercept.html
@@ -0,0 +1,55 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test that a CSP upgraded request can be intercepted by a service worker</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content">
+<iframe></iframe>
+</div>
+<pre id="test"></pre>
+<script class="testbody" type="text/javascript">
+
+ var iframe;
+ function runTest() {
+ iframe = document.querySelector("iframe");
+ iframe.src = "https://example.com/tests/dom/serviceworkers/test/fetch/upgrade-insecure/register.html";
+ window.onmessage = function(e) {
+ if (e.data.status == "ok") {
+ ok(e.data.result, e.data.message);
+ } else if (e.data.status == "registrationdone") {
+ iframe.src = "https://example.com/tests/dom/serviceworkers/test/fetch/upgrade-insecure/embedder.html";
+ } else if (e.data.status == "protocol") {
+ is(e.data.data, "https:", "Correct protocol expected");
+ } else if (e.data.status == "image") {
+ is(e.data.data, 40, "The image request was upgraded before interception");
+ iframe.src = "https://example.com/tests/dom/serviceworkers/test/fetch/upgrade-insecure/unregister.html";
+ } else if (e.data.status == "unregistrationdone") {
+ window.onmessage = null;
+ SimpleTest.finish();
+ }
+ };
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ onload = function() {
+ SpecialPowers.pushPrefEnv({"set": [
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ // This is needed so that we can test upgrading a non-secure load inside an https iframe.
+ ["security.mixed_content.block_active_content", false],
+ ["security.mixed_content.block_display_content", false],
+ ]}, runTest);
+ };
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/test_devtools_bypass_serviceworker.html b/dom/serviceworkers/test/test_devtools_bypass_serviceworker.html
new file mode 100644
index 0000000000..09c05d557a
--- /dev/null
+++ b/dom/serviceworkers/test/test_devtools_bypass_serviceworker.html
@@ -0,0 +1,107 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title> Verify devtools can utilize nsIChannel::LOAD_BYPASS_SERVICE_WORKER to bypass the service worker </title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="error_reporting_helpers.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+ <meta http-equiv="Content-type" content="text/html;charset=UTF-8">
+</head>
+<body>
+<div id="content" style="display: none"></div>
+<script src="utils.js"></script>
+<script type="text/javascript">
+"use strict";
+
+async function testBypassSW () {
+ let Ci = SpecialPowers.Ci;
+
+ // Bypass SW imitates the "Disable Cache" option in dev-tools.
+ // Note: if we put the setter/getter into dev-tools, we should take care of
+ // the implementation of enabling/disabling cache since it just overwrite the
+ // defaultLoadFlags of docShell.
+ function setBypassServiceWorker(aDocShell, aBypass) {
+ if (aBypass) {
+ aDocShell.defaultLoadFlags |= Ci.nsIChannel.LOAD_BYPASS_SERVICE_WORKER;
+ return;
+ }
+
+ aDocShell.defaultLoadFlags &= ~Ci.nsIChannel.LOAD_BYPASS_SERVICE_WORKER;
+ }
+
+ function getBypassServiceWorker(aDocShell) {
+ return !!(aDocShell.defaultLoadFlags &
+ Ci.nsIChannel.LOAD_BYPASS_SERVICE_WORKER);
+ }
+
+ async function fetchFakeDocAndCheckIfIntercepted(aWindow) {
+ const fakeDoc = "fake.html";
+
+ // Note: The fetching document doesn't exist, so the expected status of the
+ // repsonse is 404 unless the request is hijacked.
+ let response = await aWindow.fetch(fakeDoc);
+ if (response.status === 404) {
+ return false;
+ } else if (!response.ok) {
+ throw(response.statusText);
+ }
+
+ let text = await response.text();
+ if (text.includes("Hello")) {
+ // Intercepted
+ return true;
+ }
+
+ throw("Unexpected error");
+ return;
+ }
+
+ let docShell = SpecialPowers.wrap(window).docShell;
+
+ info("Test 1: Enable bypass service worker for the docShell");
+
+ setBypassServiceWorker(docShell, true);
+ ok(getBypassServiceWorker(docShell),
+ "The loadFlags in docShell does bypass the serviceWorker by default");
+
+ let intercepted = await fetchFakeDocAndCheckIfIntercepted(window);
+ ok(!intercepted,
+ "The fetched document wasn't intercepted by the serviceWorker");
+
+ info("Test 2: Disable the bypass service worker for the docShell");
+
+ setBypassServiceWorker(docShell, false);
+ ok(!getBypassServiceWorker(docShell),
+ "The loadFlags in docShell doesn't bypass the serviceWorker by default");
+
+ intercepted = await fetchFakeDocAndCheckIfIntercepted(window);
+ ok(intercepted,
+ "The fetched document was intercepted by the serviceWorker");
+}
+
+// (This doesn't really need to be its own task, but it allows the actual test
+// case to be self-contained.)
+add_task(function setupPrefs() {
+ return SpecialPowers.pushPrefEnv({"set": [
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ ]});
+});
+
+add_task(async function test_bypassServiceWorker() {
+ const swURL = "fetch.js";
+ let registration = await navigator.serviceWorker.register(swURL);
+ await waitForState(registration.installing, 'activated');
+
+ try {
+ await testBypassSW();
+ } catch (e) {
+ ok(false, "Reason:" + e);
+ }
+
+ await registration.unregister();
+});
+
+</script>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/test_devtools_track_serviceworker_time.html b/dom/serviceworkers/test/test_devtools_track_serviceworker_time.html
new file mode 100644
index 0000000000..ac27ebcd33
--- /dev/null
+++ b/dom/serviceworkers/test/test_devtools_track_serviceworker_time.html
@@ -0,0 +1,236 @@
+<html>
+<head>
+ <title>Bug 1251238 - track service worker install time</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"?>
+</head>
+<iframe id="iframe"></iframe>
+<body>
+
+<script type="text/javascript">
+
+const State = {
+ BYTECHECK: -1,
+ PARSED: Ci.nsIServiceWorkerInfo.STATE_PARSED,
+ INSTALLING: Ci.nsIServiceWorkerInfo.STATE_INSTALLING,
+ INSTALLED: Ci.nsIServiceWorkerInfo.STATE_INSTALLED,
+ ACTIVATING: Ci.nsIServiceWorkerInfo.STATE_ACTIVATING,
+ ACTIVATED: Ci.nsIServiceWorkerInfo.STATE_ACTIVATED,
+ REDUNDANT: Ci.nsIServiceWorkerInfo.STATE_REDUNDANT
+};
+let swm = Cc["@mozilla.org/serviceworkers/manager;1"].
+ getService(Ci.nsIServiceWorkerManager);
+
+let EXAMPLE_URL = "https://example.com/chrome/dom/serviceworkers/test/";
+
+let swrlistener = null;
+let registrationInfo = null;
+
+// Use it to keep the sw after unregistration.
+let astrayServiceWorkerInfo = null;
+
+let expectedResults = [
+ {
+ // Speacial state for verifying update since we will do the byte-check
+ // first.
+ state: State.BYTECHECK, installedTimeRecorded: false,
+ activatedTimeRecorded: false, redundantTimeRecorded: false
+ },
+ {
+ state: State.PARSED, installedTimeRecorded: false,
+ activatedTimeRecorded: false, redundantTimeRecorded: false
+ },
+ {
+ state: State.INSTALLING, installedTimeRecorded: false,
+ activatedTimeRecorded: false, redundantTimeRecorded: false
+ },
+ {
+ state: State.INSTALLED, installedTimeRecorded: true,
+ activatedTimeRecorded: false, redundantTimeRecorded: false
+ },
+ {
+ state: State.ACTIVATING, installedTimeRecorded: true,
+ activatedTimeRecorded: false, redundantTimeRecorded: false
+ },
+ {
+ state: State.ACTIVATED, installedTimeRecorded: true,
+ activatedTimeRecorded: true, redundantTimeRecorded: false
+ },
+
+ // When first being marked as unregistered (but the worker can remain
+ // actively controlling pages)
+ {
+ state: State.ACTIVATED, installedTimeRecorded: true,
+ activatedTimeRecorded: true, redundantTimeRecorded: false
+ },
+ // When cleared (when idle)
+ {
+ state: State.REDUNDANT, installedTimeRecorded: true,
+ activatedTimeRecorded: true, redundantTimeRecorded: true
+ },
+];
+
+function waitForRegister(aScope, aCallback) {
+ return new Promise(function (aResolve) {
+ let listener = {
+ onRegister (aRegistration) {
+ if (aRegistration.scope !== aScope) {
+ return;
+ }
+ swm.removeListener(listener);
+ registrationInfo = aRegistration;
+ aResolve();
+ }
+ };
+ swm.addListener(listener);
+ });
+}
+
+function waitForUnregister(aScope) {
+ return new Promise(function (aResolve) {
+ let listener = {
+ onUnregister (aRegistration) {
+ if (aRegistration.scope !== aScope) {
+ return;
+ }
+ swm.removeListener(listener);
+ aResolve();
+ }
+ };
+ swm.addListener(listener);
+ });
+}
+
+function register() {
+ info("Register a ServiceWorker in the iframe");
+
+ let iframe = document.querySelector("iframe");
+ iframe.src = EXAMPLE_URL + "serviceworkerinfo_iframe.html";
+
+ let promise = new Promise(function(aResolve) {
+ iframe.onload = aResolve;
+ });
+
+ return promise.then(function() {
+ iframe.contentWindow.postMessage("register", "*");
+ return waitForRegister(EXAMPLE_URL);
+ })
+}
+
+function verifyServiceWorkTime(aSWRInfo, resolve) {
+ let expectedResult = expectedResults.shift();
+ ok(!!expectedResult, "We should be able to get test from expectedResults");
+
+ info("Check the ServiceWorker time in its state is " + expectedResult.state);
+
+ // Get serviceWorkerInfo from swrInfo or get the astray one which we hold.
+ let swInfo = aSWRInfo.evaluatingWorker ||
+ aSWRInfo.installingWorker ||
+ aSWRInfo.waitingWorker ||
+ aSWRInfo.activeWorker ||
+ astrayServiceWorkerInfo;
+
+ ok(!!aSWRInfo.lastUpdateTime,
+ "We should do the byte-check and update the update timeStamp");
+
+ if (!swInfo) {
+ is(expectedResult.state, State.BYTECHECK,
+ "We shouldn't get sw when we are notified for first time updating");
+ return;
+ }
+
+ ok(!!swInfo);
+
+ is(expectedResult.state, swInfo.state,
+ "The service worker's state should be " + swInfo.state + ", but got " +
+ expectedResult.state);
+
+ is(expectedResult.installedTimeRecorded, !!swInfo.installedTime,
+ "InstalledTime should be recorded when their state is greater than " +
+ "INSTALLING");
+
+ is(expectedResult.activatedTimeRecorded, !!swInfo.activatedTime,
+ "ActivatedTime should be recorded when their state is greater than " +
+ "ACTIVATING");
+
+ is(expectedResult.redundantTimeRecorded, !!swInfo.redundantTime,
+ "RedundantTime should be recorded when their state is REDUNDANT");
+
+ // We need to hold sw to avoid losing it since we'll unregister the swr later.
+ if (expectedResult.state === State.ACTIVATED) {
+ astrayServiceWorkerInfo = aSWRInfo.activeWorker;
+
+ // Resolve the promise for testServiceWorkerInfo after sw is activated.
+ resolve();
+ }
+}
+
+function testServiceWorkerInfo() {
+ info("Listen onChange event and verify service worker's information");
+
+ let promise_resolve;
+ let promise = new Promise(aResolve => promise_resolve = aResolve);
+
+ swrlistener = {
+ onChange: () => {
+ verifyServiceWorkTime(registrationInfo, promise_resolve);
+ }
+ };
+
+ registrationInfo.addListener(swrlistener);
+
+ return promise;
+}
+
+async function testHttpCacheUpdateTime() {
+ let iframe = document.querySelector("iframe");
+ let reg = await iframe.contentWindow.navigator.serviceWorker.getRegistration();
+ let lastUpdateTime = registrationInfo.lastUpdateTime;
+ await reg.update();
+ is(lastUpdateTime, registrationInfo.lastUpdateTime,
+ "The update time should not change when SW script is read from http cache.");
+}
+
+function unregister() {
+ info("Unregister the ServiceWorker");
+
+ let iframe = document.querySelector("iframe");
+ iframe.contentWindow.postMessage("unregister", "*");
+ return waitForUnregister(EXAMPLE_URL);
+}
+
+function cleanAll() {
+ return new Promise((aResolve, aReject) => {
+ is(expectedResults.length, 0, "All the tests should be tested");
+
+ registrationInfo.removeListener(swrlistener);
+
+ swm = null;
+ swrlistener = null;
+ registrationInfo = null;
+ astrayServiceWorkerInfo = null;
+ aResolve();
+ })
+}
+
+function runTest() {
+ return Promise.resolve()
+ .then(register)
+ .then(testServiceWorkerInfo)
+ .then(testHttpCacheUpdateTime)
+ .then(unregister)
+ .catch(aError => ok(false, "Some test failed with error " + aError))
+ .then(cleanAll)
+ .then(SimpleTest.finish);
+}
+
+SimpleTest.waitForExplicitFinish();
+SpecialPowers.pushPrefEnv({"set": [
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true]
+]}, runTest);
+
+</script>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/test_empty_serviceworker.html b/dom/serviceworkers/test/test_empty_serviceworker.html
new file mode 100644
index 0000000000..00b77939f8
--- /dev/null
+++ b/dom/serviceworkers/test/test_empty_serviceworker.html
@@ -0,0 +1,46 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test that registering an empty service worker works</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test"></pre>
+<script class="testbody" type="text/javascript">
+
+ function runTest() {
+ navigator.serviceWorker.ready.then(done);
+ navigator.serviceWorker.register("empty.js", {scope: "."});
+ }
+
+ function done(registration) {
+ ok(registration.waiting || registration.active, "registration worked");
+ registration.unregister().then(function(success) {
+ ok(success, "unregister worked");
+ SimpleTest.finish();
+ }, function(e) {
+ dump("Unregistering the SW failed with " + e + "\n");
+ SimpleTest.finish();
+ });
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ onload = function() {
+ SpecialPowers.pushPrefEnv({"set": [
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true]
+ ]}, runTest);
+ };
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/test_error_reporting.html b/dom/serviceworkers/test/test_error_reporting.html
new file mode 100644
index 0000000000..7c2d56fb9e
--- /dev/null
+++ b/dom/serviceworkers/test/test_error_reporting.html
@@ -0,0 +1,241 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test Error Reporting of Service Worker Failures</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="error_reporting_helpers.js"></script>
+ <script src="utils.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+ <meta http-equiv="Content-type" content="text/html;charset=UTF-8">
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+/**
+ * Test that a bunch of service worker coding errors and failure modes that
+ * might otherwise be hard to diagnose are surfaced as console error messages.
+ * The driving use-case is minimizing cursing from a developer looking at a
+ * document in Firefox testing a page that involves service workers.
+ *
+ * This test assumes that errors will be reported via
+ * ServiceWorkerManager::ReportToAllClients and that that method is reliable and
+ * tested via some other file.
+ **/
+
+add_task(function setupPrefs() {
+ return SpecialPowers.pushPrefEnv({"set": [
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ ["dom.caches.testing.enabled", true],
+ ]});
+});
+
+/**
+ * Ensure an error is logged during the initial registration of a SW when a 404
+ * is received.
+ */
+add_task(async function register_404() {
+ // Start monitoring for the error
+ let expectedMessage = expect_console_message(
+ "ServiceWorkerRegisterNetworkError",
+ [make_absolute_url("network_error/"), "404", make_absolute_url("404.js")]);
+
+ // Register, generating the 404 error. This will reject with a TypeError
+ // which we need to consume so it doesn't get thrown at our generator.
+ await navigator.serviceWorker.register("404.js", { scope: "network_error/" })
+ .then(
+ () => { ok(false, "should have rejected"); },
+ (e) => { ok(e.name === "TypeError", "404 failed as expected"); });
+
+ await wait_for_expected_message(expectedMessage);
+});
+
+/**
+ * Ensure an error is logged when the service worker is being served with a
+ * MIME type of text/plain rather than a JS type.
+ */
+add_task(async function register_bad_mime_type() {
+ let expectedMessage = expect_console_message(
+ "ServiceWorkerRegisterMimeTypeError2",
+ [make_absolute_url("bad_mime_type/"), "text/plain",
+ make_absolute_url("sw_bad_mime_type.js")]);
+
+ // consume the expected rejection so it doesn't get thrown at us.
+ await navigator.serviceWorker.register("sw_bad_mime_type.js", { scope: "bad_mime_type/" })
+ .then(
+ () => { ok(false, "should have rejected"); },
+ (e) => { ok(e.name === "SecurityError", "bad MIME type failed as expected"); });
+
+ await wait_for_expected_message(expectedMessage);
+});
+
+async function notAllowStorageAccess() {
+ throw new Error("Storage permissions should be used when bug 1774860 overhauls this test.");
+}
+
+async function allowStorageAccess() {
+ throw new Error("Storage permissions should be used when bug 1774860 overhauls this test.");
+}
+
+/**
+ * Ensure an error is logged when the storage is not allowed and the content
+ * script is trying to register a service worker.
+ */
+add_task(async function register_storage_error() {
+ let expectedMessage = expect_console_message(
+ "ServiceWorkerRegisterStorageError",
+ [make_absolute_url("storage_not_allow/")]);
+
+ await notAllowStorageAccess();
+
+ // consume the expected rejection so it doesn't get thrown at us.
+ await navigator.serviceWorker.register("sw_storage_not_allow.js",
+ { scope: "storage_not_allow/" })
+ .then(
+ () => { ok(false, "should have rejected"); },
+ (e) => { ok(e.name === "SecurityError",
+ "storage access failed as expected."); });
+
+ await wait_for_expected_message(expectedMessage);
+
+ await allowStorageAccess();
+});
+
+/**
+ * Ensure an error is logged when the storage is not allowed and the content
+ * script is trying to get the service worker registration.
+ */
+add_task(async function get_registration_storage_error() {
+ let expectedMessage =
+ expect_console_message("ServiceWorkerGetRegistrationStorageError", []);
+
+ await notAllowStorageAccess();
+
+ // consume the expected rejection so it doesn't get thrown at us.
+ await navigator.serviceWorker.getRegistration()
+ .then(
+ () => { ok(false, "should have rejected"); },
+ (e) => { ok(e.name === "SecurityError",
+ "storage access failed as expected."); });
+
+ await wait_for_expected_message(expectedMessage);
+
+ await allowStorageAccess();
+});
+
+/**
+ * Ensure an error is logged when the storage is not allowed and the content
+ * script is trying to get the service worker registrations.
+ */
+add_task(async function get_registrations_storage_error() {
+ let expectedMessage =
+ expect_console_message("ServiceWorkerGetRegistrationStorageError", []);
+
+ await notAllowStorageAccess();
+
+ // consume the expected rejection so it doesn't get thrown at us.
+ await navigator.serviceWorker.getRegistrations()
+ .then(
+ () => { ok(false, "should have rejected"); },
+ (e) => { ok(e.name === "SecurityError",
+ "storage access failed as expected."); });
+
+ await wait_for_expected_message(expectedMessage);
+
+ await allowStorageAccess();
+});
+
+/**
+ * Ensure an error is logged when the storage is not allowed and the content
+ * script is trying to post a message to the service worker.
+ */
+add_task(async function postMessage_storage_error() {
+ let expectedMessage = expect_console_message(
+ "ServiceWorkerPostMessageStorageError",
+ [make_absolute_url("storage_not_allow/")]);
+
+ let registration;
+ // consume the expected rejection so it doesn't get thrown at us.
+ await navigator.serviceWorker.register("sw_storage_not_allow.js",
+ { scope: "storage_not_allow/" })
+ .then(reg => { registration = reg; })
+ .then(() => notAllowStorageAccess())
+ .then(() => registration.installing ||
+ registration.waiting ||
+ registration.active)
+ .then(worker => worker.postMessage('ha'))
+ .then(
+ () => { ok(false, "should have rejected"); },
+ (e) => { ok(e.name === "SecurityError",
+ "storage access failed as expected."); });
+
+ await wait_for_expected_message(expectedMessage);
+
+ await registration.unregister();
+ await allowStorageAccess();
+});
+
+/**
+ * Ensure an error is logged when the storage is not allowed and the service
+ * worker is trying to get its client.
+ */
+add_task(async function get_client_storage_error() {
+ let expectedMessage =
+ expect_console_message("ServiceWorkerGetClientStorageError", []);
+
+ await SpecialPowers.pushPrefEnv({"set": [
+ // Make the test pass the IsOriginPotentiallyTrustworthy.
+ ["dom.securecontext.allowlist", "mochi.test"]
+ ]});
+
+ let registration;
+ // consume the expected rejection so it doesn't get thrown at us.
+ await navigator.serviceWorker.register("sw_storage_not_allow.js",
+ { scope: "test_error_reporting.html" })
+ .then(reg => {
+ registration = reg;
+ return waitForState(registration.installing, "activated");
+ })
+ // Get the client's ID in the stage 1
+ .then(() => fetch("getClient-stage1"))
+ .then(() => notAllowStorageAccess())
+ // Trigger the clients.get() in the stage 2
+ .then(() => fetch("getClient-stage2"))
+ .catch(e => ok(false, "fail due to:" + e));
+
+ await wait_for_expected_message(expectedMessage);
+
+ await registration.unregister();
+ await allowStorageAccess();
+});
+
+/**
+ * Ensure an error is logged when the storage is not allowed and the service
+ * worker is trying to get its clients.
+ */
+add_task(async function get_clients_storage_error() {
+ let expectedMessage =
+ expect_console_message("ServiceWorkerGetClientStorageError", []);
+
+ let registration;
+ // consume the expected rejection so it doesn't get thrown at us.
+ await navigator.serviceWorker.register("sw_storage_not_allow.js",
+ { scope: "test_error_reporting.html" })
+ .then(reg => {
+ registration = reg;
+ return waitForState(registration.installing, "activated");
+ })
+ .then(() => notAllowStorageAccess())
+ .then(() => fetch("getClients"))
+ .catch(e => ok(false, "fail due to:" + e));
+
+ await wait_for_expected_message(expectedMessage);
+
+ await registration.unregister();
+ await allowStorageAccess();
+});
+</script>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/test_escapedSlashes.html b/dom/serviceworkers/test/test_escapedSlashes.html
new file mode 100644
index 0000000000..001c660242
--- /dev/null
+++ b/dom/serviceworkers/test/test_escapedSlashes.html
@@ -0,0 +1,102 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for escaped slashes in navigator.serviceWorker.register</title>
+ <script type="text/javascript" src="http://mochi.test:8888/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="http://mochi.test:8888/tests/SimpleTest/test.css" />
+ <base href="https://mozilla.org/">
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test"></pre>
+<script class="testbody" type="text/javascript">
+
+var tests = [
+ { status: true,
+ scriptURL: "a.js?foo%2fbar",
+ scopeURL: null },
+ { status: false,
+ scriptURL: "foo%2fbar",
+ scopeURL: null },
+ { status: true,
+ scriptURL: "a.js?foo%2Fbar",
+ scopeURL: null },
+ { status: false,
+ scriptURL: "foo%2Fbar",
+ scopeURL: null },
+ { status: true,
+ scriptURL: "a.js?foo%5cbar",
+ scopeURL: null },
+ { status: false,
+ scriptURL: "foo%5cbar",
+ scopeURL: null },
+ { status: true,
+ scriptURL: "a.js?foo%2Cbar",
+ scopeURL: null },
+ { status: false,
+ scriptURL: "foo%5Cbar",
+ scopeURL: null },
+ { status: true,
+ scriptURL: "ok.js",
+ scopeURL: "/scope?foo%2fbar"},
+ { status: false,
+ scriptURL: "ok.js",
+ scopeURL: "/foo%2fbar"},
+ { status: true,
+ scriptURL: "ok.js",
+ scopeURL: "/scope?foo%2Fbar"},
+ { status: false,
+ scriptURL: "ok.js",
+ scopeURL: "foo%2Fbar"},
+ { status: true,
+ scriptURL: "ok.js",
+ scopeURL: "/scope?foo%5cbar"},
+ { status: false,
+ scriptURL: "ok.js",
+ scopeURL: "foo%5cbar"},
+ { status: true,
+ scriptURL: "ok.js",
+ scopeURL: "/scope?foo%5Cbar"},
+ { status: false,
+ scriptURL: "ok.js",
+ scopeURL: "foo%5Cbar"},
+];
+
+function runTest() {
+ if (!tests.length) {
+ SimpleTest.finish();
+ return;
+ }
+
+ var test = tests.shift();
+ navigator.serviceWorker.register(test.scriptURL, test.scopeURL)
+ .then(reg => {
+ ok(false, "Register should fail");
+ }, err => {
+ if (!test.status) {
+ is(err.name, "TypeError", "Registration should fail with TypeError");
+ } else {
+ ok(test.status, "Register should fail");
+ }
+ })
+ .then(runTest);
+}
+
+SimpleTest.waitForExplicitFinish();
+onload = function() {
+ SpecialPowers.pushPrefEnv({"set": [
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ ["dom.serviceWorkers.enabled", true],
+ ]}, runTest);
+};
+
+</script>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/test_eval_allowed.html b/dom/serviceworkers/test/test_eval_allowed.html
new file mode 100644
index 0000000000..5d6d7a7d9c
--- /dev/null
+++ b/dom/serviceworkers/test/test_eval_allowed.html
@@ -0,0 +1,51 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1160458 - CSP activated by default in Service Workers</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+<script class="testbody" type="text/javascript">
+ function register() {
+ return navigator.serviceWorker.register("eval_worker.js");
+ }
+
+ function runTest() {
+ try {
+ eval("1");
+ ok(false, "should throw");
+ }
+ catch (ex) {
+ ok(true, "did throw");
+ }
+ register()
+ .then(function(swr) {
+ ok(true, "eval restriction didn't get inherited");
+ swr.unregister()
+ .then(function() {
+ SimpleTest.finish();
+ });
+ }).catch(function(e) {
+ ok(false, "Some test failed with error " + e);
+ SimpleTest.finish();
+ });
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ SpecialPowers.pushPrefEnv({"set": [
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ ]}, runTest);
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/test_eval_allowed.html^headers^ b/dom/serviceworkers/test/test_eval_allowed.html^headers^
new file mode 100644
index 0000000000..51ffaa71dd
--- /dev/null
+++ b/dom/serviceworkers/test/test_eval_allowed.html^headers^
@@ -0,0 +1 @@
+Content-Security-Policy: "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self'"
diff --git a/dom/serviceworkers/test/test_event_listener_leaks.html b/dom/serviceworkers/test/test_event_listener_leaks.html
new file mode 100644
index 0000000000..33ffeb44c4
--- /dev/null
+++ b/dom/serviceworkers/test/test_event_listener_leaks.html
@@ -0,0 +1,63 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1447871 - Test some service worker leak conditions</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="utils.js"></script>
+ <script type="text/javascript" src="/tests/dom/events/test/event_leak_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<script class="testbody" type="text/javascript">
+
+const scope = new URL("empty.html?leak_tests", location).href;
+const script = new URL("empty.js", location).href;
+
+// Manipulate service worker DOM objects in the frame's context.
+// Its important here that we create a listener callback from
+// the DOM objects back to the frame's global in order to
+// exercise the leak condition.
+async function useServiceWorker(contentWindow) {
+ contentWindow.navigator.serviceWorker.oncontrollerchange = _ => {
+ contentWindow.controlledChangeCount += 1;
+ };
+ let reg = await contentWindow.navigator.serviceWorker.getRegistration(scope);
+ reg.onupdatefound = _ => {
+ contentWindow.updateCount += 1;
+ };
+ reg.active.onstatechange = _ => {
+ contentWindow.stateChangeCount += 1;
+ };
+}
+
+async function runTest() {
+ await SpecialPowers.pushPrefEnv({"set": [
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true]
+ ]});
+
+ let reg = await navigator.serviceWorker.register(script, { scope });
+ await waitForState(reg.installing, "activated");
+
+ try {
+ await checkForEventListenerLeaks("ServiceWorker", useServiceWorker);
+ } catch (e) {
+ ok(false, e);
+ } finally {
+ await reg.unregister();
+ SimpleTest.finish();
+ }
+}
+
+SimpleTest.waitForExplicitFinish();
+addEventListener("load", runTest, { once: true });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/test_eventsource_intercept.html b/dom/serviceworkers/test/test_eventsource_intercept.html
new file mode 100644
index 0000000000..b76c0d1d1a
--- /dev/null
+++ b/dom/serviceworkers/test/test_eventsource_intercept.html
@@ -0,0 +1,103 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1182103 - Test EventSource scenarios with fetch interception</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+<script class="testbody" type="text/javascript">
+
+ function testFrame(src) {
+ return new Promise(function(resolve, reject) {
+ var iframe = document.createElement("iframe");
+ iframe.src = src;
+ window.onmessage = function(e) {
+ if (e.data.status == "callback") {
+ switch(e.data.data) {
+ case "ok":
+ ok(e.data.condition, e.data.message);
+ break;
+ case "ready":
+ iframe.contentWindow.postMessage({status: "callback", data: "eventsource"}, "*");
+ break;
+ case "done":
+ window.onmessage = null;
+ iframe.src = "about:blank";
+ document.body.removeChild(iframe);
+ iframe = null;
+ resolve();
+ break;
+ default:
+ ok(false, "Something went wrong");
+ break;
+ }
+ } else {
+ ok(false, "Something went wrong");
+ }
+ };
+ document.body.appendChild(iframe);
+ });
+ }
+
+ function runTest() {
+ Promise.resolve()
+ .then(() => {
+ info("Going to intercept and test opaque responses");
+ return testFrame("eventsource/eventsource_register_worker.html" +
+ "?script=eventsource_opaque_response_intercept_worker.js");
+ })
+ .then(() => {
+ return testFrame("eventsource/eventsource_opaque_response.html");
+ })
+ .then(() => {
+ info("Going to intercept and test cors responses");
+ return testFrame("eventsource/eventsource_register_worker.html" +
+ "?script=eventsource_cors_response_intercept_worker.js");
+ })
+ .then(() => {
+ return testFrame("eventsource/eventsource_cors_response.html");
+ })
+ .then(() => {
+ info("Going to intercept and test synthetic responses");
+ return testFrame("eventsource/eventsource_register_worker.html" +
+ "?script=eventsource_synthetic_response_intercept_worker.js");
+ })
+ .then(() => {
+ return testFrame("eventsource/eventsource_synthetic_response.html");
+ })
+ .then(() => {
+ info("Going to intercept and test mixed content cors responses");
+ return testFrame("https://example.com/tests/dom/serviceworkers/test/" +
+ "eventsource/eventsource_register_worker.html" +
+ "?script=eventsource_mixed_content_cors_response_intercept_worker.js");
+ })
+ .then(() => {
+ return testFrame("https://example.com/tests/dom/serviceworkers/test/" +
+ "eventsource/eventsource_mixed_content_cors_response.html");
+ })
+ .then(SimpleTest.finish)
+ .catch(function(e) {
+ ok(false, "Some test failed with error " + e);
+ SimpleTest.finish();
+ });
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ SpecialPowers.pushPrefEnv({"set": [
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ ["dom.caches.enabled", true],
+ ]}, runTest);
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/test_fetch_event.html b/dom/serviceworkers/test/test_fetch_event.html
new file mode 100644
index 0000000000..5227f6ae34
--- /dev/null
+++ b/dom/serviceworkers/test/test_fetch_event.html
@@ -0,0 +1,75 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 94048 - test install event.</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+<script src="utils.js"></script>
+<script class="testbody" type="text/javascript">
+ SimpleTest.requestCompleteLog();
+
+ var registration;
+ function simpleRegister() {
+ return navigator.serviceWorker.register("fetch_event_worker.js", { scope: "./fetch" })
+ .then(swr => {
+ registration = swr;
+ return waitForState(swr.installing, 'activated');
+ });
+ }
+
+ function unregister() {
+ return registration.unregister().then(function(success) {
+ ok(success, "Service worker should be unregistered successfully");
+ }, function(e) {
+ dump("SW unregistration error: " + e + "\n");
+ });
+ }
+
+ function testController() {
+ var p = new Promise(function(resolve, reject) {
+ window.onmessage = function(e) {
+ if (e.data.status == "ok") {
+ ok(e.data.result, e.data.message);
+ } else if (e.data.status == "done") {
+ window.onmessage = null;
+ w.close();
+ resolve();
+ }
+ }
+ });
+
+ var w = window.open("fetch/index.html");
+ return p;
+ }
+
+ function runTest() {
+ simpleRegister()
+ .then(testController)
+ .then(unregister)
+ .then(function() {
+ SimpleTest.finish();
+ }).catch(function(e) {
+ ok(false, "Some test failed with error " + e);
+ SimpleTest.finish();
+ });
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ SpecialPowers.pushPrefEnv({"set": [
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ ]}, runTest);
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/test_fetch_event_with_thirdpartypref.html b/dom/serviceworkers/test/test_fetch_event_with_thirdpartypref.html
new file mode 100644
index 0000000000..53552e03c3
--- /dev/null
+++ b/dom/serviceworkers/test/test_fetch_event_with_thirdpartypref.html
@@ -0,0 +1,90 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 94048 - test install event.</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+<script src="utils.js"></script>
+<script class="testbody" type="text/javascript">
+
+ // NOTE: This is just test_fetch_event.html but with an alternate cookie
+ // mode preference set to make sure that setting the preference does
+ // not break interception as observed in bug 1336364.
+ // TODO: Refactor this test so it doesn't duplicate so much code logic.
+
+ SimpleTest.requestCompleteLog();
+
+ var registration;
+ function simpleRegister() {
+ return navigator.serviceWorker.register("fetch_event_worker.js", { scope: "./fetch" })
+ .then(swr => {
+ registration = swr;
+ return waitForState(swr.installing, 'activated');
+ });
+ }
+
+ function unregister() {
+ return registration.unregister().then(function(success) {
+ ok(success, "Service worker should be unregistered successfully");
+ }, function(e) {
+ dump("SW unregistration error: " + e + "\n");
+ });
+ }
+
+ function testController() {
+ var p = new Promise(function(resolve, reject) {
+ var reloaded = false;
+ window.onmessage = function(e) {
+ if (e.data.status == "ok") {
+ ok(e.data.result, e.data.message);
+ } else if (e.data.status == "done") {
+ if (reloaded) {
+ window.onmessage = null;
+ w.close();
+ resolve();
+ } else {
+ w.location.reload();
+ reloaded = true;
+ }
+ }
+ }
+ });
+
+ var w = window.open("fetch/index.html");
+ return p;
+ }
+
+ function runTest() {
+ simpleRegister()
+ .then(testController)
+ .then(unregister)
+ .then(function() {
+ SimpleTest.finish();
+ }).catch(function(e) {
+ ok(false, "Some test failed with error " + e);
+ SimpleTest.finish();
+ });
+ }
+
+ const COOKIE_BEHAVIOR_REJECTFOREIGN = 1;
+
+ SimpleTest.waitForExplicitFinish();
+ SpecialPowers.pushPrefEnv({"set": [
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ ["network.cookie.cookieBehavior", COOKIE_BEHAVIOR_REJECTFOREIGN],
+ ]}, runTest);
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/test_fetch_integrity.html b/dom/serviceworkers/test/test_fetch_integrity.html
new file mode 100644
index 0000000000..35879d5749
--- /dev/null
+++ b/dom/serviceworkers/test/test_fetch_integrity.html
@@ -0,0 +1,228 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title> Test fetch.integrity on console report for serviceWorker and sharedWorker </title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="error_reporting_helpers.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+ <meta http-equiv="Content-type" content="text/html;charset=UTF-8">
+</head>
+<body>
+<div id="content" style="display: none"></div>
+<script src="utils.js"></script>
+<script type="text/javascript">
+"use strict";
+
+let security_localizer =
+ stringBundleService.createBundle("chrome://global/locale/security/security.properties");
+
+let consoleScript;
+let monitorCallbacks = [];
+
+function registerConsoleMonitor() {
+ return new Promise(resolve => {
+ var url = SimpleTest.getTestFileURL("console_monitor.js");
+ consoleScript = SpecialPowers.loadChromeScript(url);
+
+ consoleScript.addMessageListener("ready", resolve);
+ consoleScript.addMessageListener("monitor", function(msg) {
+ for (let i = 0; i < monitorCallbacks.length;) {
+ if (monitorCallbacks[i](msg)) {
+ ++i;
+ } else {
+ monitorCallbacks.splice(i, 1);
+ }
+ }
+ });
+ consoleScript.sendAsyncMessage("load", {});
+ });
+}
+
+function unregisterConsoleMonitor() {
+ return new Promise(resolve => {
+ consoleScript.addMessageListener("unloaded", () => {
+ consoleScript.destroy();
+ resolve();
+ });
+ consoleScript.sendAsyncMessage("unload", {});
+ });
+}
+
+function registerConsoleMonitorCallback(callback) {
+ monitorCallbacks.push(callback);
+}
+
+function waitForMessages() {
+ let messages = [];
+
+ // process repeated paired arguments of: msgId, args
+ for (let i = 0; i < arguments.length; i += 3) {
+ let msgId = arguments[i];
+ let args = arguments[i + 1];
+ messages.push(security_localizer.formatStringFromName(msgId, args));
+ }
+
+ return new Promise(resolve => {
+ registerConsoleMonitorCallback(msg => {
+ for (let i = 0; i < messages.length; ++i) {
+ if (messages[i] == msg.errorMessage) {
+ messages.splice(i, 1);
+ break;
+ }
+ }
+
+ if (!messages.length) {
+ resolve();
+ return false;
+ }
+
+ return true;
+ });
+ });
+}
+
+function expect_security_console_message(/* msgId, args, ... */) {
+ let expectations = [];
+ // process repeated paired arguments of: msgId, args
+ for (let i = 0; i < arguments.length; i += 3) {
+ let msgId = arguments[i];
+ let args = arguments[i + 1];
+ let filename = arguments[i + 2];
+ expectations.push({
+ errorMessage: security_localizer.formatStringFromName(msgId, args),
+ sourceName: filename,
+ });
+ }
+ return new Promise(resolve => {
+ SimpleTest.monitorConsole(resolve, expectations);
+ });
+}
+
+// (This doesn't really need to be its own task, but it allows the actual test
+// case to be self-contained.)
+add_task(function setupPrefs() {
+ return SpecialPowers.pushPrefEnv({"set": [
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ ["browser.newtab.preload", false],
+ ]});
+});
+
+add_task(async function test_integrity_serviceWorker() {
+ var filename = make_absolute_url("fetch.js");
+ var filename2 = make_absolute_url("fake.html");
+
+ let registration = await navigator.serviceWorker.register("fetch.js",
+ { scope: "./" });
+ await waitForState(registration.installing, "activated");
+
+ info("Test for mNavigationInterceptions.")
+ // The client_win will reload to another URL after opening filename2.
+ let client_win = window.open(filename2);
+
+ let expectedMessage = expect_security_console_message(
+ "MalformedIntegrityHash",
+ ["abc"],
+ filename,
+ "NoValidMetadata",
+ [""],
+ filename,
+ );
+ let expectedMessage2 = expect_security_console_message(
+ "MalformedIntegrityHash",
+ ["abc"],
+ filename,
+ "NoValidMetadata",
+ [""],
+ filename,
+ );
+
+ info("Test for mControlledDocuments and report error message to console.");
+ // The fetch will succeed because the integrity value is invalid and we are
+ // looking for the console message regarding the bad integrity value.
+ await fetch("fail.html");
+
+ await wait_for_expected_message(expectedMessage);
+
+ await wait_for_expected_message(expectedMessage2);
+
+ await registration.unregister();
+ client_win.close();
+});
+
+add_task(async function test_integrity_sharedWorker() {
+ var filename = make_absolute_url("sharedWorker_fetch.js");
+
+ await registerConsoleMonitor();
+
+ info("Attach main window to a SharedWorker.");
+ let sharedWorker = new SharedWorker(filename);
+ let waitForConnected = new Promise((resolve) => {
+ sharedWorker.port.onmessage = function (e) {
+ if (e.data == "Connected") {
+ resolve();
+ } else {
+ reject();
+ }
+ }
+ });
+ await waitForConnected;
+
+ info("Attch another window to the same SharedWorker.");
+ // Open another window and its also managed by the shared worker.
+ let client_win = window.open("create_another_sharedWorker.html");
+ let waitForBothConnected = new Promise((resolve) => {
+ sharedWorker.port.onmessage = function (e) {
+ if (e.data == "BothConnected") {
+ resolve();
+ } else {
+ reject();
+ }
+ }
+ });
+ await waitForBothConnected;
+
+ let expectedMessage = waitForMessages(
+ "MalformedIntegrityHash",
+ ["abc"],
+ filename,
+ "NoValidMetadata",
+ [""],
+ filename,
+ );
+
+ let expectedMessage2 = waitForMessages(
+ "MalformedIntegrityHash",
+ ["abc"],
+ filename,
+ "NoValidMetadata",
+ [""],
+ filename,
+ );
+
+ info("Start to fetch a URL with wrong integrity.")
+ sharedWorker.port.start();
+ sharedWorker.port.postMessage("StartFetchWithWrongIntegrity");
+
+ let waitForSRIFailed = new Promise((resolve) => {
+ sharedWorker.port.onmessage = function (e) {
+ if (e.data == "SRI_failed") {
+ resolve();
+ } else {
+ reject();
+ }
+ }
+ });
+ await waitForSRIFailed;
+
+ await expectedMessage;
+ await expectedMessage2;
+
+ client_win.close();
+
+ await unregisterConsoleMonitor();
+});
+
+</script>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/test_file_blob_response.html b/dom/serviceworkers/test/test_file_blob_response.html
new file mode 100644
index 0000000000..3aa72c3dda
--- /dev/null
+++ b/dom/serviceworkers/test/test_file_blob_response.html
@@ -0,0 +1,78 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1253777 - Test interception using file blob response body</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+<script src="utils.js"></script>
+<script class="testbody" type="text/javascript">
+ var registration;
+ var scope = './file_blob_response/';
+ function start() {
+ return navigator.serviceWorker.register("file_blob_response_worker.js",
+ { scope })
+ .then(function(swr) {
+ registration = swr;
+ return new waitForState(swr.installing, 'activated');
+ });
+ }
+
+ function unregister() {
+ return registration.unregister().then(function(result) {
+ ok(result, "Unregister should return true.");
+ }, function(e) {
+ ok(false, "Unregistering the SW failed with " + e + "\n");
+ });
+ }
+
+ function withFrame(url) {
+ return new Promise(function(resolve, reject) {
+ var content = document.getElementById("content");
+ ok(content, "Parent exists.");
+
+ var frame = document.createElement("iframe");
+ frame.setAttribute('src', url);
+ content.appendChild(frame);
+
+ frame.addEventListener('load', function(evt) {
+ resolve(frame);
+ }, {once: true});
+ });
+ }
+
+ function runTest() {
+ start()
+ .then(function() {
+ return withFrame(scope + 'dummy.txt');
+ })
+ .then(function(frame) {
+ var result = JSON.parse(frame.contentWindow.document.body.textContent);
+ frame.remove();
+ is(result.value, 'success');
+ })
+ .catch(function(e) {
+ ok(false, "Some test failed with error " + e);
+ })
+ .then(unregister)
+ .then(SimpleTest.finish);
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ SpecialPowers.pushPrefEnv({"set": [
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true]
+ ]}, runTest);
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/test_file_blob_upload.html b/dom/serviceworkers/test/test_file_blob_upload.html
new file mode 100644
index 0000000000..e60e65badd
--- /dev/null
+++ b/dom/serviceworkers/test/test_file_blob_upload.html
@@ -0,0 +1,146 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1203680 - Test interception of file blob uploads</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+<script src="utils.js"></script>
+<script class="testbody" type="text/javascript">
+ var registration;
+ var iframe;
+ function start() {
+ return navigator.serviceWorker.register("empty.js",
+ { scope: "./sw_clients/" })
+ .then((swr) => {
+ registration = swr
+ return waitForState(swr.installing, 'activated', swr);
+ });
+ }
+
+ function unregister() {
+ if (iframe) {
+ iframe.remove();
+ iframe = null;
+ }
+
+ return registration.unregister().then(function(result) {
+ ok(result, "Unregister should return true.");
+ }, function(e) {
+ ok(false, "Unregistering the SW failed with " + e + "\n");
+ });
+ }
+
+ function withFrame() {
+ var content = document.getElementById("content");
+ ok(content, "Parent exists.");
+
+ iframe = document.createElement("iframe");
+ iframe.setAttribute('src', "sw_clients/file_blob_upload_frame.html");
+ content.appendChild(iframe);
+
+ return new Promise(function(resolve, reject) {
+ window.addEventListener('message', function(evt) {
+ if (evt.data.status === 'READY') {
+ resolve();
+ } else {
+ reject(evt.data.result);
+ }
+ }, {once: true});
+ });
+ }
+
+ function postBlob(body) {
+ return new Promise(function(resolve, reject) {
+ window.addEventListener('message', function(evt) {
+ if (evt.data.status === 'OK') {
+ is(JSON.stringify(body), JSON.stringify(evt.data.result),
+ 'body echoed back correctly');
+ resolve();
+ } else {
+ reject(evt.data.result);
+ }
+ }, {once: true});
+
+ iframe.contentWindow.postMessage({ type: 'TEST', body }, '*');
+ });
+ }
+
+ function generateMessage(length) {
+
+ var lorem =
+ 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis egestas '
+ 'vehicula tortor eget ultrices. Sed et luctus est. Nunc eu orci ligula. '
+ 'In vel ornare eros, eget lacinia diam. Praesent vel metus mattis, '
+ 'cursus nulla sit amet, rhoncus diam. Aliquam nulla tortor, aliquet et '
+ 'viverra non, dignissim vel tellus. Praesent sed ex in dolor aliquet '
+ 'aliquet. In at facilisis sem, et aliquet eros. Maecenas feugiat nisl '
+ 'quis elit blandit posuere. Duis viverra odio sed eros consectetur, '
+ 'viverra mattis ligula volutpat.';
+
+ var result = '';
+
+ while (result.length < length) {
+ var remaining = length - result.length;
+ if (remaining < lorem.length) {
+ result += lorem.slice(0, remaining);
+ } else {
+ result += lorem;
+ }
+ }
+
+ return result;
+ }
+
+ var smallBody = generateMessage(64);
+ var mediumBody = generateMessage(1024);
+
+ // TODO: Test large bodies over the default pipe size. Currently stalls
+ // due to bug 1134372.
+ //var largeBody = generateMessage(100 * 1024);
+
+ function runTest() {
+ start()
+ .then(withFrame)
+ .then(function() {
+ return postBlob({ hops: 0, message: smallBody });
+ })
+ .then(function() {
+ return postBlob({ hops: 1, message: smallBody });
+ })
+ .then(function() {
+ return postBlob({ hops: 10, message: smallBody });
+ })
+ .then(function() {
+ return postBlob({ hops: 0, message: mediumBody });
+ })
+ .then(function() {
+ return postBlob({ hops: 1, message: mediumBody });
+ })
+ .then(function() {
+ return postBlob({ hops: 10, message: mediumBody });
+ })
+ .then(unregister)
+ .catch(function(e) {
+ ok(false, "Some test failed with error " + e);
+ }).then(SimpleTest.finish);
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ SpecialPowers.pushPrefEnv({"set": [
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true]
+ ]}, runTest);
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/test_file_upload.html b/dom/serviceworkers/test/test_file_upload.html
new file mode 100644
index 0000000000..0c502686af
--- /dev/null
+++ b/dom/serviceworkers/test/test_file_upload.html
@@ -0,0 +1,68 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1424701 - Test for service worker + file upload</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<input id="input" type="file">
+<script class="testbody" type="text/javascript">
+
+function GetFormData(file) {
+ const formData = new FormData();
+ formData.append('file', file);
+ return formData;
+}
+
+async function onOpened(message) {
+ let input = document.getElementById("input");
+ SpecialPowers.wrap(input).mozSetFileArray([message.file]);
+ script.destroy();
+
+ let reg = await navigator.serviceWorker.register('sw_file_upload.js',
+ {scope: "." });
+ let serviceWorker = reg.installing || reg.waiting || reg.active;
+ await waitForState(serviceWorker, 'activated');
+
+ let res = await fetch('server_file_upload.sjs?clone=0', {
+ method: 'POST',
+ body: input.files[0],
+ });
+
+ let data = await res.clone().text();
+ ok(data.length, "We have data for an uncloned request!");
+
+ res = await fetch('server_file_upload.sjs?clone=1', {
+ method: 'POST',
+ // Make sure the underlying stream is a file stream
+ body: GetFormData(input.files[0]),
+ });
+
+ data = await res.clone().text();
+ ok(data.length, "We have data for a file-stream-backed cloned request!");
+
+ await reg.unregister();
+ SimpleTest.finish();
+}
+
+let url = SimpleTest.getTestFileURL("script_file_upload.js");
+let script = SpecialPowers.loadChromeScript(url);
+
+SimpleTest.waitForExplicitFinish();
+SpecialPowers.pushPrefEnv({"set": [
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true]
+]}).then(() => {
+ script.addMessageListener("file.opened", onOpened);
+ script.sendAsyncMessage("file.open");
+});
+</script>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/test_force_refresh.html b/dom/serviceworkers/test/test_force_refresh.html
new file mode 100644
index 0000000000..69da7b7de3
--- /dev/null
+++ b/dom/serviceworkers/test/test_force_refresh.html
@@ -0,0 +1,105 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 982726 - Test service worker post message </title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+<script class="testbody" type="text/javascript">
+ /**
+ *
+ */
+ let iframe;
+ let registration;
+
+ function start() {
+ return new Promise(resolve => {
+ const content = document.getElementById("content");
+ ok(content, "Parent exists.");
+
+ iframe = document.createElement("iframe");
+ iframe.setAttribute("src", "sw_clients/refresher_compressed.html");
+
+ /*
+ * The initial iframe must be the _uncached_ version, which means its
+ * load must happen before the Service Worker's `activate` event.
+ * Rather than `waitUntil`-ing the Service Worker's `install` event
+ * until the load finishes (more concurrency, but involves coordinating
+ * `postMessage`s), just ensure the load finishes before registering
+ * the Service Worker (which is simpler).
+ */
+ iframe.onload = resolve;
+
+ content.appendChild(iframe);
+ }).then(async () => {
+ /*
+ * There's no need _here_ to explicitly wait for this Service Worker to be
+ * "activated"; this test will progress when the "READY"/"READY_CACHED"
+ * messages are received from the iframe, and the iframe will only send
+ * those messages once the Service Worker is "activated" (by chaining on
+ * its `navigator.serviceWorker.ready` promise).
+ */
+ registration = await navigator.serviceWorker.register(
+ "force_refresh_worker.js", { scope: "./sw_clients/" });
+ });
+ }
+
+ function unregister() {
+ return registration.unregister().then(function(result) {
+ ok(result, "Unregister should return true.");
+ }, function(e) {
+ dump("Unregistering the SW failed with " + e + "\n");
+ });
+ }
+
+ function testForceRefresh(swr) {
+ return new Promise(function(res, rej) {
+ var count = 0;
+ var cachedCount = 0;
+ window.onmessage = function(e) {
+ if (e.data === "READY") {
+ count += 1;
+ if (count == 2) {
+ is(cachedCount, 1, "should have received cached message before " +
+ "second non-cached message");
+ res();
+ }
+ iframe.contentWindow.postMessage("REFRESH", "*");
+ } else if (e.data === "READY_CACHED") {
+ cachedCount += 1;
+ is(count, 1, "should have received non-cached message before " +
+ "cached message");
+ iframe.contentWindow.postMessage("FORCE_REFRESH", "*");
+ }
+ }
+ }).then(() => document.getElementById("content").removeChild(iframe));
+ }
+
+ function runTest() {
+ start()
+ .then(testForceRefresh)
+ .then(unregister)
+ .catch(function(e) {
+ ok(false, "Some test failed with error " + e);
+ }).then(SimpleTest.finish);
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ SpecialPowers.pushPrefEnv({"set": [
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ ["dom.caches.enabled", true],
+ ]}, runTest);
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/test_gzip_redirect.html b/dom/serviceworkers/test/test_gzip_redirect.html
new file mode 100644
index 0000000000..8119303ae7
--- /dev/null
+++ b/dom/serviceworkers/test/test_gzip_redirect.html
@@ -0,0 +1,88 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 982726 - Test service worker post message </title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+<script src="utils.js"></script>
+<script class="testbody" type="text/javascript">
+ var registration;
+ function start() {
+ return navigator.serviceWorker.register("gzip_redirect_worker.js",
+ { scope: "./sw_clients/" })
+ .then((swr) => {
+ registration = swr;
+ return waitForState(swr.installing, 'activated', swr);
+ });
+ }
+
+ function unregister() {
+ return registration.unregister().then(function(result) {
+ ok(result, "Unregister should return true.");
+ }, function(e) {
+ dump("Unregistering the SW failed with " + e + "\n");
+ });
+ }
+
+
+ function testGzipRedirect(swr) {
+ var p = new Promise(function(res, rej) {
+ var navigatorReady = false;
+ var finalReady = false;
+
+ window.onmessage = function(e) {
+ if (e.data === "NAVIGATOR_READY") {
+ ok(!navigatorReady, "should only get navigator ready message once");
+ ok(!finalReady, "should get navigator ready before final redirect ready message");
+ navigatorReady = true;
+ iframe.contentWindow.postMessage({
+ type: "NAVIGATE",
+ url: "does_not_exist.html"
+ }, "*");
+ } else if (e.data === "READY") {
+ ok(navigatorReady, "should only get navigator ready message once");
+ ok(!finalReady, "should get final ready message only once");
+ finalReady = true;
+ res();
+ }
+ }
+ });
+
+ var content = document.getElementById("content");
+ ok(content, "Parent exists.");
+
+ iframe = document.createElement("iframe");
+ iframe.setAttribute('src', "sw_clients/navigator.html");
+ content.appendChild(iframe);
+
+ return p.then(() => content.removeChild(iframe));
+ }
+
+ function runTest() {
+ start()
+ .then(testGzipRedirect)
+ .then(unregister)
+ .catch(function(e) {
+ ok(false, "Some test failed with error " + e);
+ }).then(SimpleTest.finish);
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ SpecialPowers.pushPrefEnv({"set": [
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ ]}, runTest);
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/test_hsts_upgrade_intercept.html b/dom/serviceworkers/test/test_hsts_upgrade_intercept.html
new file mode 100644
index 0000000000..59fef0ec14
--- /dev/null
+++ b/dom/serviceworkers/test/test_hsts_upgrade_intercept.html
@@ -0,0 +1,66 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test that an HSTS upgraded request can be intercepted by a service worker</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content">
+<iframe></iframe>
+</div>
+<pre id="test"></pre>
+<script class="testbody" type="text/javascript">
+
+ var iframe;
+ var framesLoaded = 0;
+ function runTest() {
+ iframe = document.querySelector("iframe");
+ iframe.src = "https://example.com/tests/dom/serviceworkers/test/fetch/hsts/register.html";
+ window.onmessage = function(e) {
+ if (e.data.status == "ok") {
+ ok(e.data.result, e.data.message);
+ } else if (e.data.status == "registrationdone") {
+ iframe.src = "http://example.com/tests/dom/serviceworkers/test/fetch/hsts/index.html";
+ } else if (e.data.status == "protocol") {
+ is(e.data.data, "https:", "Correct protocol expected");
+ ok(e.data.securityInfoPresent, "Security info present on intercepted value");
+ switch (++framesLoaded) {
+ case 1:
+ iframe.src = "https://example.com/tests/dom/serviceworkers/test/fetch/hsts/embedder.html";
+ break;
+ case 2:
+ iframe.src = "https://example.com/tests/dom/serviceworkers/test/fetch/hsts/image.html";
+ break;
+ }
+ } else if (e.data.status == "image") {
+ is(e.data.data, 40, "The image request was upgraded before interception");
+ iframe.src = "https://example.com/tests/dom/serviceworkers/test/fetch/hsts/unregister.html";
+ } else if (e.data.status == "unregistrationdone") {
+ window.onmessage = null;
+ SpecialPowers.cleanUpSTSData("http://example.com");
+ SimpleTest.finish();
+ }
+ };
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ onload = function() {
+ SpecialPowers.pushPrefEnv({"set": [
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ // This is needed so that we can test upgrading a non-secure load inside an https iframe.
+ ["security.mixed_content.block_active_content", false],
+ ["security.mixed_content.block_display_content", false],
+ ]}, runTest);
+ };
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/test_https_fetch.html b/dom/serviceworkers/test/test_https_fetch.html
new file mode 100644
index 0000000000..4ac4255889
--- /dev/null
+++ b/dom/serviceworkers/test/test_https_fetch.html
@@ -0,0 +1,62 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1133763 - test fetch event in HTTPS origins</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+<iframe></iframe>
+</div>
+<pre id="test"></pre>
+<script class="testbody" type="text/javascript">
+
+ var iframe;
+ function runTest() {
+ iframe = document.querySelector("iframe");
+ iframe.src = "https://example.com/tests/dom/serviceworkers/test/fetch/https/register.html";
+ var ios;
+ window.onmessage = function(e) {
+ if (e.data.status == "ok") {
+ ok(e.data.result, e.data.message);
+ } else if (e.data.status == "registrationdone") {
+ ios = SpecialPowers.Cc["@mozilla.org/network/io-service;1"]
+ .getService(SpecialPowers.Ci.nsIIOService);
+ ios.offline = true;
+ iframe.src = "https://example.com/tests/dom/serviceworkers/test/fetch/https/index.html";
+ } else if (e.data.status == "done") {
+ iframe.src = "https://example.com/tests/dom/serviceworkers/test/fetch/https/synth-sw.html";
+ } else if (e.data.status == "done-synth-sw") {
+ iframe.src = "https://example.com/tests/dom/serviceworkers/test/fetch/https/synth-window.html";
+ } else if (e.data.status == "done-synth-window") {
+ iframe.src = "https://example.com/tests/dom/serviceworkers/test/fetch/https/synth.html";
+ } else if (e.data.status == "done-synth") {
+ ios.offline = false;
+ iframe.src = "https://example.com/tests/dom/serviceworkers/test/fetch/https/unregister.html";
+ } else if (e.data.status == "unregistrationdone") {
+ window.onmessage = null;
+ ok(true, "Test finished successfully");
+ SimpleTest.finish();
+ }
+ };
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ onload = function() {
+ SpecialPowers.pushPrefEnv({"set": [
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ ["dom.caches.enabled", true]
+ ]}, runTest);
+ };
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/test_https_fetch_cloned_response.html b/dom/serviceworkers/test/test_https_fetch_cloned_response.html
new file mode 100644
index 0000000000..8c7129d39d
--- /dev/null
+++ b/dom/serviceworkers/test/test_https_fetch_cloned_response.html
@@ -0,0 +1,56 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1133763 - test fetch event in HTTPS origins with a cloned response</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+<iframe></iframe>
+</div>
+<pre id="test"></pre>
+<script class="testbody" type="text/javascript">
+
+ var iframe;
+ function runTest() {
+ iframe = document.querySelector("iframe");
+ iframe.src = "https://example.com/tests/dom/serviceworkers/test/fetch/https/clonedresponse/register.html";
+ var ios;
+ window.onmessage = function(e) {
+ if (e.data.status == "ok") {
+ ok(e.data.result, e.data.message);
+ } else if (e.data.status == "registrationdone") {
+ ios = SpecialPowers.Cc["@mozilla.org/network/io-service;1"]
+ .getService(SpecialPowers.Ci.nsIIOService);
+ ios.offline = true;
+ iframe.src = "https://example.com/tests/dom/serviceworkers/test/fetch/https/clonedresponse/index.html";
+ } else if (e.data.status == "done") {
+ ios.offline = false;
+ iframe.src = "https://example.com/tests/dom/serviceworkers/test/fetch/https/clonedresponse/unregister.html";
+ } else if (e.data.status == "unregistrationdone") {
+ window.onmessage = null;
+ ok(true, "Test finished successfully");
+ SimpleTest.finish();
+ }
+ };
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ onload = function() {
+ SpecialPowers.pushPrefEnv({"set": [
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ ["dom.caches.enabled", true]
+ ]}, runTest);
+ };
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/test_https_origin_after_redirect.html b/dom/serviceworkers/test/test_https_origin_after_redirect.html
new file mode 100644
index 0000000000..31ce173f34
--- /dev/null
+++ b/dom/serviceworkers/test/test_https_origin_after_redirect.html
@@ -0,0 +1,57 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test the origin of a redirected response from a service worker</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+<iframe></iframe>
+</div>
+<pre id="test"></pre>
+<script class="testbody" type="text/javascript">
+
+ var iframe;
+ function runTest() {
+ iframe = document.querySelector("iframe");
+ iframe.src = "https://example.com/tests/dom/serviceworkers/test/fetch/origin/https/register.html";
+ var win;
+ window.onmessage = function(e) {
+ if (e.data.status == "ok") {
+ ok(e.data.result, e.data.message);
+ } else if (e.data.status == "registrationdone") {
+ win = window.open("https://example.com/tests/dom/serviceworkers/test/fetch/origin/https/index-https.sjs", "mywindow", "width=100,height=100");
+ } else if (e.data.status == "domain") {
+ is(e.data.data, "example.org", "Correct domain expected");
+ } else if (e.data.status == "origin") {
+ is(e.data.data, "https://example.org", "Correct origin expected");
+ } else if (e.data.status == "done") {
+ win.close();
+ iframe.src = "https://example.com/tests/dom/serviceworkers/test/fetch/origin/https/unregister.html";
+ } else if (e.data.status == "unregistrationdone") {
+ window.onmessage = null;
+ ok(true, "Test finished successfully");
+ SimpleTest.finish();
+ }
+ };
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ onload = function() {
+ SpecialPowers.pushPrefEnv({"set": [
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ ["dom.caches.enabled", true],
+ ]}, runTest);
+ };
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/test_https_origin_after_redirect_cached.html b/dom/serviceworkers/test/test_https_origin_after_redirect_cached.html
new file mode 100644
index 0000000000..8bce413f21
--- /dev/null
+++ b/dom/serviceworkers/test/test_https_origin_after_redirect_cached.html
@@ -0,0 +1,57 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test the origin of a redirected response from a service worker</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+<iframe></iframe>
+</div>
+<pre id="test"></pre>
+<script class="testbody" type="text/javascript">
+
+ var iframe;
+ function runTest() {
+ iframe = document.querySelector("iframe");
+ iframe.src = "https://example.com/tests/dom/serviceworkers/test/fetch/origin/https/register.html";
+ var win;
+ window.onmessage = function(e) {
+ if (e.data.status == "ok") {
+ ok(e.data.result, e.data.message);
+ } else if (e.data.status == "registrationdone") {
+ win = window.open("https://example.com/tests/dom/serviceworkers/test/fetch/origin/https/index-cached-https.sjs", "mywindow", "width=100,height=100");
+ } else if (e.data.status == "domain") {
+ is(e.data.data, "example.org", "Correct domain expected");
+ } else if (e.data.status == "origin") {
+ is(e.data.data, "https://example.org", "Correct origin expected");
+ } else if (e.data.status == "done") {
+ win.close();
+ iframe.src = "https://example.com/tests/dom/serviceworkers/test/fetch/origin/https/unregister.html";
+ } else if (e.data.status == "unregistrationdone") {
+ window.onmessage = null;
+ ok(true, "Test finished successfully");
+ SimpleTest.finish();
+ }
+ };
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ onload = function() {
+ SpecialPowers.pushPrefEnv({"set": [
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ ["dom.caches.enabled", true],
+ ]}, runTest);
+ };
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/test_https_synth_fetch_from_cached_sw.html b/dom/serviceworkers/test/test_https_synth_fetch_from_cached_sw.html
new file mode 100644
index 0000000000..4186cfc340
--- /dev/null
+++ b/dom/serviceworkers/test/test_https_synth_fetch_from_cached_sw.html
@@ -0,0 +1,69 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1156847 - test fetch event generating a synthesized response in HTTPS origins from a cached SW</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" tyle="display: none">
+<iframe></iframe>
+</div>
+<pre id="test"></pre>
+<script class="testbody" type="text/javascript">
+
+ var iframe;
+ function runTest() {
+ iframe = document.querySelector("iframe");
+ iframe.src = "https://example.com/tests/dom/serviceworkers/test/fetch/https/register.html";
+ var ios;
+ window.onmessage = function(e) {
+ if (e.data.status == "ok") {
+ ok(e.data.result, e.data.message);
+ } else if (e.data.status == "registrationdone") {
+ ios = SpecialPowers.Cc["@mozilla.org/network/io-service;1"]
+ .getService(SpecialPowers.Ci.nsIIOService);
+ ios.offline = true;
+
+ // In order to load synth.html from a cached service worker, we first
+ // remove the existing window that is keeping the service worker alive,
+ // and do a GC to ensure that the SW is destroyed. This way, when we
+ // load synth.html for the second time, we will first recreate the
+ // service worker from the cache. This is intended to test that we
+ // properly store and retrieve the security info from the cache.
+ iframe.remove();
+ iframe = null;
+ SpecialPowers.exactGC(function() {
+ iframe = document.createElement("iframe");
+ iframe.src = "https://example.com/tests/dom/serviceworkers/test/fetch/https/synth.html";
+ document.body.appendChild(iframe);
+ });
+ } else if (e.data.status == "done-synth") {
+ ios.offline = false;
+ iframe.src = "https://example.com/tests/dom/serviceworkers/test/fetch/https/unregister.html";
+ } else if (e.data.status == "unregistrationdone") {
+ window.onmessage = null;
+ ok(true, "Test finished successfully");
+ SimpleTest.finish();
+ }
+ };
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ onload = function() {
+ SpecialPowers.pushPrefEnv({"set": [
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ ["dom.caches.enabled", true]
+ ]}, runTest);
+ };
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/test_imagecache.html b/dom/serviceworkers/test/test_imagecache.html
new file mode 100644
index 0000000000..52a793bfb9
--- /dev/null
+++ b/dom/serviceworkers/test/test_imagecache.html
@@ -0,0 +1,55 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1202085 - Test that images from different controllers don't cached together</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content">
+<iframe></iframe>
+</div>
+<pre id="test"></pre>
+<script class="testbody" type="text/javascript">
+
+ var iframe;
+ function runTest() {
+ iframe = document.querySelector("iframe");
+ iframe.src = "/tests/dom/serviceworkers/test/fetch/imagecache/register.html";
+ window.onmessage = function(e) {
+ if (e.data.status == "ok") {
+ ok(e.data.result, e.data.message);
+ } else if (e.data.status == "registrationdone") {
+ iframe.src = "/tests/dom/serviceworkers/test/fetch/imagecache/index.html";
+ } else if (e.data.status == "result") {
+ is(e.data.url, "image-40px.png", "Correct url expected");
+ is(e.data.width, 40, "Correct width expected");
+ iframe.src = "/tests/dom/serviceworkers/test/fetch/imagecache/unregister.html";
+ } else if (e.data.status == "unregistrationdone") {
+ iframe.src = "/tests/dom/serviceworkers/test/fetch/imagecache/postmortem.html";
+ } else if (e.data.status == "postmortem") {
+ is(e.data.width, 20, "Correct width expected");
+ window.onmessage = null;
+ ok(true, "Test finished successfully");
+ SimpleTest.finish();
+ }
+ };
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ onload = function() {
+ SpecialPowers.pushPrefEnv({"set": [
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ ]}, runTest);
+ };
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/test_imagecache_max_age.html b/dom/serviceworkers/test/test_imagecache_max_age.html
new file mode 100644
index 0000000000..fcb8d3e306
--- /dev/null
+++ b/dom/serviceworkers/test/test_imagecache_max_age.html
@@ -0,0 +1,71 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test that the image cache respects a synthesized image's Cache headers</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content">
+<iframe></iframe>
+</div>
+<pre id="test"></pre>
+<script class="testbody" type="text/javascript">
+
+ var iframe;
+ var framesLoaded = 0;
+ function runTest() {
+ iframe = document.querySelector("iframe");
+ iframe.src = "/tests/dom/serviceworkers/test/fetch/imagecache-maxage/register.html";
+ window.onmessage = function(e) {
+ if (e.data.status == "ok") {
+ ok(e.data.result, e.data.message);
+ } else if (e.data.status == "registrationdone") {
+ iframe.src = "/tests/dom/serviceworkers/test/fetch/imagecache-maxage/index.html";
+ } else if (e.data.status == "result") {
+ switch (++framesLoaded) {
+ case 1:
+ is(e.data.url, "image-20px.png", "Correct url expected");
+ is(e.data.url2, "image-20px.png", "Correct url expected");
+ is(e.data.width, 20, "Correct width expected");
+ is(e.data.width2, 20, "Correct width expected");
+ // Wait for 100ms so that the image gets expired.
+ setTimeout(function() {
+ iframe.src = "/tests/dom/serviceworkers/test/fetch/imagecache-maxage/index.html?new"
+ }, 100);
+ break;
+ case 2:
+ is(e.data.url, "image-40px.png", "Correct url expected");
+ is(e.data.url2, "image-40px.png", "Correct url expected");
+ is(e.data.width, 40, "Correct width expected");
+ is(e.data.width2, 40, "Correct width expected");
+ iframe.src = "/tests/dom/serviceworkers/test/fetch/imagecache-maxage/unregister.html";
+ break;
+ default:
+ ok(false, "This should never happen");
+ }
+ } else if (e.data.status == "unregistrationdone") {
+ window.onmessage = null;
+ SimpleTest.finish();
+ }
+ };
+ }
+
+ SimpleTest.requestFlakyTimeout("This test needs to simulate the passing of time");
+ SimpleTest.waitForExplicitFinish();
+ onload = function() {
+ SpecialPowers.pushPrefEnv({"set": [
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ ]}, runTest);
+ };
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/test_importscript.html b/dom/serviceworkers/test/test_importscript.html
new file mode 100644
index 0000000000..c0a894cf3c
--- /dev/null
+++ b/dom/serviceworkers/test/test_importscript.html
@@ -0,0 +1,74 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test service worker - script cache policy</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<div id="content"></div>
+<script src="utils.js"></script>
+<script class="testbody" type="text/javascript">
+ function start() {
+ return navigator.serviceWorker.register("importscript_worker.js",
+ { scope: "./sw_clients/" })
+ .then(swr => waitForState(swr.installing, 'activated', swr))
+ .then(swr => registration = swr);
+ }
+
+ function unregister() {
+ return fetch("importscript.sjs?clearcounter").then(function() {
+ return registration.unregister();
+ }).then(function(result) {
+ ok(result, "Unregister should return true.");
+ }, function(e) {
+ dump("Unregistering the SW failed with " + e + "\n");
+ });
+ }
+
+ function testPostMessage(swr) {
+ var p = new Promise(function(res, rej) {
+ window.onmessage = function(e) {
+ if (e.data === "READY") {
+ swr.active.postMessage("do magic");
+ return;
+ }
+
+ ok(e.data === "OK", "Worker posted the correct value: " + e.data);
+ res();
+ }
+ });
+
+ var content = document.getElementById("content");
+ ok(content, "Parent exists.");
+
+ iframe = document.createElement("iframe");
+ iframe.setAttribute('src', "sw_clients/service_worker_controlled.html");
+ content.appendChild(iframe);
+
+ return p.then(() => content.removeChild(iframe));
+ }
+
+ function runTest() {
+ start()
+ .then(testPostMessage)
+ .then(unregister)
+ .catch(function(e) {
+ ok(false, "Some test failed with error " + e);
+ }).then(SimpleTest.finish);
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ SpecialPowers.pushPrefEnv({"set": [
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true]
+ ]}, runTest);
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/test_importscript_mixedcontent.html b/dom/serviceworkers/test/test_importscript_mixedcontent.html
new file mode 100644
index 0000000000..15fe5e88b6
--- /dev/null
+++ b/dom/serviceworkers/test/test_importscript_mixedcontent.html
@@ -0,0 +1,53 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1198078 - test that we respect mixed content blocking in importScript() inside service workers</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+<iframe></iframe>
+</div>
+<pre id="test"></pre>
+<script class="testbody" type="text/javascript">
+
+ var iframe;
+ function runTest() {
+ iframe = document.querySelector("iframe");
+ iframe.src = "https://example.com/tests/dom/serviceworkers/test/fetch/importscript-mixedcontent/register.html";
+ var ios;
+ window.onmessage = function(e) {
+ if (e.data.status == "ok") {
+ ok(e.data.result, e.data.message);
+ } else if (e.data.status == "registrationdone") {
+ iframe.src = "https://example.com/tests/dom/serviceworkers/test/fetch/importscript-mixedcontent/index.html";
+ } else if (e.data.status == "done") {
+ is(e.data.data, "good", "Mixed content blocking should work correctly for service workers");
+ iframe.src = "https://example.com/tests/dom/serviceworkers/test/fetch/importscript-mixedcontent/unregister.html";
+ } else if (e.data.status == "unregistrationdone") {
+ window.onmessage = null;
+ ok(true, "Test finished successfully");
+ SimpleTest.finish();
+ }
+ };
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ onload = function() {
+ SpecialPowers.pushPrefEnv({"set": [
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ ["security.mixed_content.block_active_content", false],
+ ]}, runTest);
+ };
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/test_install_event.html b/dom/serviceworkers/test/test_install_event.html
new file mode 100644
index 0000000000..87f89725dc
--- /dev/null
+++ b/dom/serviceworkers/test/test_install_event.html
@@ -0,0 +1,143 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 94048 - test install event.</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+<script class="testbody" type="text/javascript">
+
+ function simpleRegister() {
+ var p = navigator.serviceWorker.register("worker.js", { scope: "./install_event" });
+ return p;
+ }
+
+ function nextRegister(reg) {
+ ok(reg instanceof ServiceWorkerRegistration, "reg should be a ServiceWorkerRegistration");
+ var p = navigator.serviceWorker.register("install_event_worker.js", { scope: "./install_event" });
+ return p.then(function(swr) {
+ ok(reg === swr, "register should resolve to the same registration object");
+ var update_found_promise = new Promise(function(resolve, reject) {
+ swr.addEventListener('updatefound', function(e) {
+ ok(true, "Received onupdatefound");
+ resolve();
+ });
+ });
+
+ var worker_activating = new Promise(function(res, reject) {
+ ok(swr.installing instanceof ServiceWorker, "There should be an installing worker if promise resolves.");
+ ok(swr.installing.state == "installing", "Installing worker's state should be 'installing'");
+ swr.installing.onstatechange = function(e) {
+ if (e.target.state == "activating") {
+ e.target.onstatechange = null;
+ res();
+ }
+ }
+ });
+
+ return Promise.all([update_found_promise, worker_activating]);
+ }, function(e) {
+ ok(false, "Unexpected Error in nextRegister! " + e);
+ });
+ }
+
+ function installError() {
+ // Silence worker errors so they don't cause the test to fail.
+ window.onerror = function(e) {}
+ return navigator.serviceWorker.register("install_event_error_worker.js", { scope: "./install_event" })
+ .then(function(swr) {
+ ok(swr.installing instanceof ServiceWorker, "There should be an installing worker if promise resolves.");
+ ok(swr.installing.state == "installing", "Installing worker's state should be 'installing'");
+ return new Promise(function(resolve, reject) {
+ swr.installing.onstatechange = function(e) {
+ ok(e.target.state == "redundant", "Installation of worker with error should fail.");
+ resolve();
+ }
+ });
+ }).then(function() {
+ return navigator.serviceWorker.getRegistration("./install_event").then(function(swr) {
+ var newest = swr.waiting || swr.active;
+ ok(newest, "Waiting or active worker should still exist");
+ ok(newest.scriptURL.match(/install_event_worker.js$/), "Previous worker should remain the newest worker");
+ });
+ });
+ }
+
+ function testActive(worker) {
+ is(worker.state, "activating", "Should be activating");
+ return new Promise(function(resolve, reject) {
+ worker.onstatechange = function(e) {
+ e.target.onstatechange = null;
+ is(e.target.state, "activated", "Activation of worker with error in activate event handler should still succeed.");
+ resolve();
+ }
+ });
+ }
+
+ function activateErrorShouldSucceed() {
+ // Silence worker errors so they don't cause the test to fail.
+ window.onerror = function() { }
+ return navigator.serviceWorker.register("activate_event_error_worker.js", { scope: "./activate_error" })
+ .then(function(swr) {
+ var p = new Promise(function(resolve, reject) {
+ ok(swr.installing.state == "installing", "activateErrorShouldSucceed(): Installing worker's state should be 'installing'");
+ swr.installing.onstatechange = function(e) {
+ e.target.onstatechange = null;
+ if (swr.waiting) {
+ swr.waiting.onstatechange = function(event) {
+ event.target.onstatechange = null;
+ testActive(swr.active).then(resolve, reject);
+ }
+ } else {
+ testActive(swr.active).then(resolve, reject);
+ }
+ }
+ });
+
+ return p.then(function() {
+ return Promise.resolve(swr);
+ });
+ }).then(function(swr) {
+ return swr.unregister();
+ });
+ }
+
+ function unregister() {
+ return navigator.serviceWorker.getRegistration("./install_event").then(function(reg) {
+ return reg.unregister();
+ });
+ }
+
+ function runTest() {
+ Promise.resolve()
+ .then(simpleRegister)
+ .then(nextRegister)
+ .then(installError)
+ .then(activateErrorShouldSucceed)
+ .then(unregister)
+ .then(function() {
+ SimpleTest.finish();
+ }).catch(function(e) {
+ ok(false, "Some test failed with error " + e);
+ SimpleTest.finish();
+ });
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ SpecialPowers.pushPrefEnv({"set": [
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true]
+ ]}, runTest);
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/test_install_event_gc.html b/dom/serviceworkers/test/test_install_event_gc.html
new file mode 100644
index 0000000000..8b68b8ac47
--- /dev/null
+++ b/dom/serviceworkers/test/test_install_event_gc.html
@@ -0,0 +1,121 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test install event being GC'd before waitUntil fulfills</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<script class="testbody" type="text/javascript">
+var script = 'blocking_install_event_worker.js';
+var scope = 'sw_clients/simple.html?install-event-gc';
+var registration;
+
+function register() {
+ return navigator.serviceWorker.register(script, { scope })
+ .then(swr => registration = swr);
+}
+
+function unregister() {
+ if (!registration) {
+ return;
+ }
+ return registration.unregister();
+}
+
+function waitForInstallEvent() {
+ return new Promise((resolve, reject) => {
+ navigator.serviceWorker.addEventListener('message', evt => {
+ if (evt.data.type === 'INSTALL_EVENT') {
+ resolve();
+ }
+ });
+ });
+}
+
+function gcWorker() {
+ return new Promise(function(resolve, reject) {
+ // We are able to trigger asynchronous garbage collection and cycle
+ // collection by emitting "child-cc-request" and "child-gc-request"
+ // observer notifications. The worker RuntimeService will translate
+ // these notifications into the appropriate operation on all known
+ // worker threads.
+ //
+ // In the failure case where GC/CC causes us to abort the installation,
+ // we will know something happened from the statechange event.
+ const statechangeHandler = evt => {
+ // Reject rather than resolving to avoid the possibility of us seeing
+ // an unrelated racing statechange somehow. Since in the success case we
+ // will still see a state change on termination, we do explicitly need to
+ // be removed on the success path.
+ ok(registration.installing, 'service worker is still installing?');
+ reject();
+ };
+ registration.installing.addEventListener('statechange', statechangeHandler);
+ // In the success case since the service worker installation is effectively
+ // hung, we instead depend on sending a 'ping' message to the service worker
+ // and hearing it 'pong' back. Since we issue our postMessage after we
+ // trigger the GC/CC, our 'ping' will only be processed after the GC/CC and
+ // therefore the pong will also strictly occur after the cycle collection.
+ navigator.serviceWorker.addEventListener('message', evt => {
+ if (evt.data.type === 'pong') {
+ registration.installing.removeEventListener(
+ 'statechange', statechangeHandler);
+ resolve();
+ }
+ });
+ // At the current time, the service worker will exist in our same process
+ // and notifyObservers is synchronous. However, in the future, service
+ // workers may end up in a separate process and in that case it will be
+ // appropriate to use notifyObserversInParentProcess or something like it.
+ // (notifyObserversInParentProcess is a synchronous IPC call to the parent
+ // process's main thread. IPDL PContent::CycleCollect is an async message.
+ // Ordering will be maintained if the postMessage goes via PContent as well,
+ // but that seems unlikely.)
+ SpecialPowers.notifyObservers(null, 'child-gc-request');
+ SpecialPowers.notifyObservers(null, 'child-cc-request');
+ SpecialPowers.notifyObservers(null, 'child-gc-request');
+ // (Only send the ping after we set the gc/cc/gc in motion.)
+ registration.installing.postMessage({ type: 'ping' });
+ });
+}
+
+function terminateWorker() {
+ return SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.serviceWorkers.idle_timeout", 0],
+ ["dom.serviceWorkers.idle_extended_timeout", 0]
+ ]
+ }).then(_ => {
+ registration.installing.postMessage({ type: 'RESET_TIMER' });
+ });
+}
+
+function runTest() {
+ Promise.all([
+ waitForInstallEvent(),
+ register()
+ ]).then(_ => ok(registration.installing, 'service worker is installing'))
+ .then(gcWorker)
+ .then(_ => ok(registration.installing, 'service worker is still installing'))
+ .then(terminateWorker)
+ .catch(e => ok(false, e))
+ .then(unregister)
+ .then(SimpleTest.finish);
+}
+
+SimpleTest.waitForExplicitFinish();
+SpecialPowers.pushPrefEnv({"set": [
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ ["dom.caches.enabled", true],
+]}, runTest);
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/test_installation_simple.html b/dom/serviceworkers/test/test_installation_simple.html
new file mode 100644
index 0000000000..69c9518ea0
--- /dev/null
+++ b/dom/serviceworkers/test/test_installation_simple.html
@@ -0,0 +1,208 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 930348 - test stub Navigator ServiceWorker utilities.</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+<script class="testbody" type="text/javascript">
+
+ function simpleRegister() {
+ var p = navigator.serviceWorker.register("worker.js", { scope: "simpleregister/" });
+ ok(p instanceof Promise, "register() should return a Promise");
+ return Promise.resolve();
+ }
+
+ function sameOriginWorker() {
+ p = navigator.serviceWorker.register("http://some-other-origin/worker.js");
+ return p.then(function(w) {
+ ok(false, "Worker from different origin should fail");
+ }, function(e) {
+ ok(e.name === "SecurityError", "Should fail with a SecurityError");
+ });
+ }
+
+ function sameOriginScope() {
+ p = navigator.serviceWorker.register("worker.js", { scope: "http://www.example.com/" });
+ return p.then(function(w) {
+ ok(false, "Worker controlling scope for different origin should fail");
+ }, function(e) {
+ ok(e.name === "SecurityError", "Should fail with a SecurityError");
+ });
+ }
+
+ function httpsOnly() {
+ return SpecialPowers.pushPrefEnv({'set': [["dom.serviceWorkers.testing.enabled", false]] })
+ .then(function() {
+ return navigator.serviceWorker.register("/worker.js");
+ }).then(function(w) {
+ ok(false, "non-HTTPS pages cannot register ServiceWorkers");
+ }, function(e) {
+ ok(e.name === "TypeError", "navigator.serviceWorker should be undefined");
+ }).then(function() {
+ return SpecialPowers.popPrefEnv();
+ });
+ }
+
+ function realWorker() {
+ var p = navigator.serviceWorker.register("worker.js", { scope: "realworker" });
+ return p.then(function(wr) {
+ ok(wr instanceof ServiceWorkerRegistration, "Register a ServiceWorker");
+
+ info(wr.scope);
+ ok(wr.scope == (new URL("realworker", document.baseURI)).href, "Scope should match");
+ // active, waiting, installing should return valid worker instances
+ // because the registration is for the realworker scope, so the workers
+ // should be obtained for that scope and not for
+ // test_installation_simple.html
+ var worker = wr.installing;
+ ok(worker && wr.scope.match(/realworker$/) &&
+ worker.scriptURL.match(/worker.js$/), "Valid worker instance should be available.");
+ return wr.unregister().then(function(success) {
+ ok(success, "The worker should be unregistered successfully");
+ }, function(e) {
+ dump("Error unregistering the worker: " + e + "\n");
+ });
+ }, function(e) {
+ info("Error: " + e.name);
+ ok(false, "realWorker Registration should have succeeded!");
+ });
+ }
+
+ function networkError404() {
+ return navigator.serviceWorker.register("404.js", { scope: "network_error/"}).then(function(w) {
+ ok(false, "404 response should fail with TypeError");
+ }, function(e) {
+ ok(e.name === "TypeError", "404 response should fail with TypeError");
+ });
+ }
+
+ function redirectError() {
+ return navigator.serviceWorker.register("redirect_serviceworker.sjs", { scope: "redirect_error/" }).then(function(swr) {
+ ok(false, "redirection should fail");
+ }, function (e) {
+ ok(e.name === "SecurityError", "redirection should fail with SecurityError");
+ });
+ }
+
+ function parseError() {
+ var p = navigator.serviceWorker.register("parse_error_worker.js", { scope: "parse_error/" });
+ return p.then(function(wr) {
+ ok(false, "Registration should fail with parse error");
+ return navigator.serviceWorker.getRegistration("parse_error/").then(function(swr) {
+ // See https://github.com/slightlyoff/ServiceWorker/issues/547
+ is(swr, undefined, "A failed registration for a scope with no prior controllers should clear itself");
+ });
+ }, function(e) {
+ ok(e instanceof Error, "Registration should fail with parse error");
+ });
+ }
+
+ // FIXME(nsm): test for parse error when Update step doesn't happen (directly from register).
+
+ function updatefound() {
+ var frame = document.createElement("iframe");
+ frame.setAttribute("id", "simpleregister-frame");
+ frame.setAttribute("src", new URL("simpleregister/index.html", document.baseURI).href);
+ document.body.appendChild(frame);
+ var resolve, reject;
+ var p = new Promise(function(res, rej) {
+ resolve = res;
+ reject = rej;
+ });
+
+ var regPromise;
+ function continueTest() {
+ regPromise = navigator.serviceWorker.register(
+ "worker2.js", { scope: "simpleregister/" });
+ }
+
+ window.onmessage = function(e) {
+ if (e.data.type == "ready") {
+ continueTest();
+ } else if (e.data.type == "finish") {
+ window.onmessage = null;
+ // We have to make frame navigate away, otherwise it will call
+ // MaybeStopControlling() when this document is unloaded. At that point
+ // the pref has been disabled, so the ServiceWorkerManager is not available.
+ frame.setAttribute("src", new URL("about:blank").href);
+ regPromise.then(function(reg) {
+ reg.unregister().then(function(success) {
+ ok(success, "The worker should be unregistered successfully");
+ resolve();
+ }, function(error) {
+ dump("Error unregistering the worker: " + error + "\n");
+ });
+ });
+ } else if (e.data.type == "check") {
+ ok(e.data.status, e.data.msg);
+ }
+ }
+ return p;
+ }
+
+ var readyPromiseResolved = false;
+
+ function readyPromise() {
+ var frame = document.createElement("iframe");
+ frame.setAttribute("id", "simpleregister-frame-ready");
+ frame.setAttribute("src", new URL("simpleregister/ready.html", document.baseURI).href);
+ document.body.appendChild(frame);
+
+ var channel = new MessageChannel();
+ frame.addEventListener('load', function() {
+ frame.contentWindow.postMessage('your port!', '*', [channel.port2]);
+ });
+
+ channel.port1.onmessage = function() {
+ readyPromiseResolved = true;
+ }
+
+ return Promise.resolve();
+ }
+
+ function checkReadyPromise() {
+ ok(readyPromiseResolved, "The ready promise has been resolved!");
+ return Promise.resolve();
+ }
+
+ function runTest() {
+ simpleRegister()
+ .then(sameOriginWorker)
+ .then(sameOriginScope)
+ .then(httpsOnly)
+ .then(readyPromise)
+ .then(realWorker)
+ .then(networkError404)
+ .then(redirectError)
+ .then(parseError)
+ .then(updatefound)
+ .then(checkReadyPromise)
+ // put more tests here.
+ .then(function() {
+ SimpleTest.finish();
+ }).catch(function(e) {
+ ok(false, "Some test failed with error " + e);
+ SimpleTest.finish();
+ });
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ SpecialPowers.pushPrefEnv({"set": [
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ ["dom.caches.testing.enabled", true],
+ ]}, runTest);
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/test_match_all.html b/dom/serviceworkers/test/test_match_all.html
new file mode 100644
index 0000000000..a1ee01507c
--- /dev/null
+++ b/dom/serviceworkers/test/test_match_all.html
@@ -0,0 +1,83 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 982726 - test match_all not crashing</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+<script src="utils.js"></script>
+<script class="testbody" type="text/javascript">
+ // match_all_worker will call matchAll until the worker shuts down.
+ // Test passes if the browser doesn't crash on leaked promise objects.
+ var registration;
+ var content;
+ var iframe;
+
+ function simpleRegister() {
+ return navigator.serviceWorker.register("match_all_worker.js",
+ { scope: "./sw_clients/" })
+ .then((swr) => {
+ registration = swr;
+ return waitForState(swr.installing, 'activated', swr);
+ });
+ }
+
+ function closeAndUnregister() {
+ content.removeChild(iframe);
+
+ return registration.unregister().then(function(result) {
+ ok(result, "Unregister should return true.");
+ }, function(e) {
+ dump("Unregistering the SW failed with " + e + "\n");
+ });
+ }
+
+ function openClient() {
+ var p = new Promise(function(resolve, reject) {
+ window.onmessage = function(e) {
+ if (e.data === "READY") {
+ resolve();
+ }
+ }
+ });
+
+ content = document.getElementById("content");
+ ok(content, "Parent exists.");
+
+ iframe = document.createElement("iframe");
+ iframe.setAttribute('src', "sw_clients/simple.html");
+ content.appendChild(iframe);
+
+ return p;
+ }
+
+ function runTest() {
+ simpleRegister()
+ .then(openClient)
+ .then(closeAndUnregister)
+ .catch(function(e) {
+ ok(false, "Some test failed with error " + e);
+ }).then(function() {
+ ok(true, "Didn't crash on resolving matchAll promises while worker shuts down.");
+ SimpleTest.finish();
+ });
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ SpecialPowers.pushPrefEnv({"set": [
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true]
+ ]}, runTest);
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/test_match_all_advanced.html b/dom/serviceworkers/test/test_match_all_advanced.html
new file mode 100644
index 0000000000..b4359511f3
--- /dev/null
+++ b/dom/serviceworkers/test/test_match_all_advanced.html
@@ -0,0 +1,102 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 982726 - Test matchAll with multiple clients</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+<script src="utils.js"></script>
+<script class="testbody" type="text/javascript">
+ var client_iframes = [];
+ var registration;
+
+ function start() {
+ return navigator.serviceWorker.register("match_all_advanced_worker.js",
+ { scope: "./sw_clients/" }).then(function(swr) {
+ registration = swr;
+ return waitForState(swr.installing, 'activated');
+ }).then(_ => {
+ window.onmessage = function (e) {
+ if (e.data === "READY") {
+ ok(registration.active, "Worker is active.");
+ registration.active.postMessage("RUN");
+ }
+ }
+ });
+ }
+
+ function unregister() {
+ return registration.unregister().then(function(result) {
+ ok(result, "Unregister should return true.");
+ }, function(e) {
+ dump("Unregistering the SW failed with " + e + "\n");
+ });
+ }
+
+
+ function testMatchAll() {
+ var p = new Promise(function(res, rej) {
+ navigator.serviceWorker.onmessage = function (e) {
+ ok(e.data === client_iframes.length, "MatchAll returned the correct number of clients.");
+ res();
+ }
+ });
+
+ content = document.getElementById("content");
+ ok(content, "Parent exists.");
+
+ iframe = document.createElement("iframe");
+ iframe.setAttribute('src', "sw_clients/service_worker_controlled.html");
+ content.appendChild(iframe);
+
+ client_iframes.push(iframe);
+ return p;
+ }
+
+ function removeAndTest() {
+ content = document.getElementById("content");
+ ok(content, "Parent exists.");
+
+ content.removeChild(client_iframes.pop());
+ content.removeChild(client_iframes.pop());
+
+ return testMatchAll();
+ }
+
+ function runTest() {
+ start()
+ .then(testMatchAll)
+ .then(testMatchAll)
+ .then(testMatchAll)
+ .then(removeAndTest)
+ .then(function(e) {
+ content = document.getElementById("content");
+ while (client_iframes.length) {
+ content.removeChild(client_iframes.pop());
+ }
+ }).then(unregister).catch(function(e) {
+ ok(false, "Some test failed with error " + e);
+ }).then(function() {
+ SimpleTest.finish();
+ });
+
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ SpecialPowers.pushPrefEnv({"set": [
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true]
+ ]}, runTest);
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/test_match_all_client_id.html b/dom/serviceworkers/test/test_match_all_client_id.html
new file mode 100644
index 0000000000..0294c00aba
--- /dev/null
+++ b/dom/serviceworkers/test/test_match_all_client_id.html
@@ -0,0 +1,95 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1058311 - Test matchAll client id </title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+<script src="utils.js"></script>
+<script class="testbody" type="text/javascript">
+ var registration;
+ var clientURL = "match_all_client/match_all_client_id.html";
+ function start() {
+ return navigator.serviceWorker.register("match_all_client_id_worker.js",
+ { scope: "./match_all_client/" })
+ .then((swr) => {
+ registration = swr;
+ return waitForState(swr.installing, 'activated', swr);
+ });
+ }
+
+ function unregister() {
+ return registration.unregister().then(function(result) {
+ ok(result, "Unregister should return true.");
+ }, function(e) {
+ dump("Unregistering the SW failed with " + e + "\n");
+ });
+ }
+
+ function getMessageListener() {
+ return new Promise(function(res, rej) {
+ window.onmessage = function(e) {
+ ok(e.data, "Same client id for multiple calls.");
+ is(e.origin, "http://mochi.test:8888", "Event should have the correct origin");
+
+ if (!e.data) {
+ rej();
+ return;
+ }
+
+ info("DONE from: " + e.source);
+ res();
+ }
+ });
+ }
+
+ function testNestedWindow() {
+ var p = getMessageListener();
+
+ var content = document.getElementById("content");
+ ok(content, "Parent exists.");
+
+ iframe = document.createElement("iframe");
+
+ content.appendChild(iframe);
+ iframe.setAttribute('src', clientURL);
+
+ return p.then(() => content.removeChild(iframe));
+ }
+
+ function testAuxiliaryWindow() {
+ var p = getMessageListener();
+ var w = window.open(clientURL);
+
+ return p.then(() => w.close());
+ }
+
+ function runTest() {
+ info(window.opener == undefined);
+ start()
+ .then(testAuxiliaryWindow)
+ .then(testNestedWindow)
+ .then(unregister)
+ .catch(function(e) {
+ ok(false, "Some test failed with error " + e);
+ }).then(SimpleTest.finish);
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ SpecialPowers.pushPrefEnv({"set": [
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true]
+ ]}, runTest);
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/test_match_all_client_properties.html b/dom/serviceworkers/test/test_match_all_client_properties.html
new file mode 100644
index 0000000000..c8a0b448c2
--- /dev/null
+++ b/dom/serviceworkers/test/test_match_all_client_properties.html
@@ -0,0 +1,101 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1058311 - Test matchAll clients properties </title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+<script src="utils.js"></script>
+<script class="testbody" type="text/javascript">
+ var registration;
+ var clientURL = "match_all_clients/match_all_controlled.html";
+ function start() {
+ return navigator.serviceWorker.register("match_all_properties_worker.js",
+ { scope: "./match_all_clients/" })
+ .then((swr) => {
+ registration = swr;
+ return waitForState(swr.installing, 'activated', swr);
+ });
+ }
+
+ function unregister() {
+ return registration.unregister().then(function(result) {
+ ok(result, "Unregister should return true.");
+ }, function(e) {
+ dump("Unregistering the SW failed with " + e + "\n");
+ });
+ }
+
+ function getMessageListener() {
+ return new Promise(function(res, rej) {
+ window.onmessage = function(e) {
+ if (e.data.message === undefined) {
+ info("rejecting promise");
+ rej();
+ return;
+ }
+
+ ok(e.data.result, e.data.message);
+
+ if (!e.data.result) {
+ rej();
+ }
+ if (e.data.message == "DONE") {
+ info("DONE from: " + e.source);
+ res();
+ }
+ }
+ });
+ }
+
+ function testNestedWindow() {
+ var p = getMessageListener();
+
+ var content = document.getElementById("content");
+ ok(content, "Parent exists.");
+
+ iframe = document.createElement("iframe");
+
+ content.appendChild(iframe);
+ iframe.setAttribute('src', clientURL);
+
+ return p.then(() => content.removeChild(iframe));
+ }
+
+ function testAuxiliaryWindow() {
+ var p = getMessageListener();
+ var w = window.open(clientURL);
+
+ return p.then(() => w.close());
+ }
+
+ function runTest() {
+ info("catalin");
+ info(window.opener == undefined);
+ start()
+ .then(testAuxiliaryWindow)
+ .then(testNestedWindow)
+ .then(unregister)
+ .catch(function(e) {
+ ok(false, "Some test failed with error " + e);
+ }).then(SimpleTest.finish);
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ SpecialPowers.pushPrefEnv({"set": [
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true]
+ ]}, runTest);
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/test_navigationPreload_disable_crash.html b/dom/serviceworkers/test/test_navigationPreload_disable_crash.html
new file mode 100644
index 0000000000..ea6439284d
--- /dev/null
+++ b/dom/serviceworkers/test/test_navigationPreload_disable_crash.html
@@ -0,0 +1,52 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Failure to create a Promise shouldn't crash</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test"></pre>
+<script class="testbody" type="text/javascript">
+ async function runTest() {
+ const iframe = document.createElement('iframe');
+ document.getElementById("content").appendChild(iframe);
+
+ const serviceWorker = iframe.contentWindow.navigator.serviceWorker;
+ const worker = await iframe.contentWindow.navigator.serviceWorker.register("empty.js", {});
+
+ iframe.remove();
+
+ // We can't wait for this promise to settle, because the global's
+ // browsing context has been discarded when the iframe was removed.
+ // We're just checking if this call crashes, which would happen
+ // immediately, so ignoring the promise should be fine.
+ worker.navigationPreload.disable();
+ ok(true, "navigationPreload.disable() failed but didn't crash.");
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ // We can't call unregister on the worker after its browsing context has been
+ // discarded, so use SpecialPowers.removeAllServiceWorkerData.
+ SimpleTest.registerCleanupFunction(() => SpecialPowers.removeAllServiceWorkerData());
+ onload = function() {
+ SpecialPowers.pushPrefEnv({"set": [
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.navigationPreload.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ ]}, runTest);
+ };
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/test_navigator.html b/dom/serviceworkers/test/test_navigator.html
new file mode 100644
index 0000000000..aaac04e926
--- /dev/null
+++ b/dom/serviceworkers/test/test_navigator.html
@@ -0,0 +1,40 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 930348 - test stub Navigator ServiceWorker utilities.</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+<script class="testbody" type="text/javascript">
+
+ function checkEnabled() {
+ ok(navigator.serviceWorker, "navigator.serviceWorker should exist when ServiceWorkers are enabled.");
+ ok(typeof navigator.serviceWorker.register === "function", "navigator.serviceWorker.register() should be a function.");
+ ok(typeof navigator.serviceWorker.getRegistration === "function", "navigator.serviceWorker.getAll() should be a function.");
+ ok(typeof navigator.serviceWorker.getRegistrations === "function", "navigator.serviceWorker.getAll() should be a function.");
+ ok(navigator.serviceWorker.ready instanceof Promise, "navigator.serviceWorker.ready should be a Promise.");
+ ok(navigator.serviceWorker.controller === null, "There should be no controller worker for an uncontrolled document.");
+ }
+
+ SimpleTest.waitForExplicitFinish();
+
+ SpecialPowers.pushPrefEnv({"set": [
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true]
+ ]}, function() {
+ checkEnabled();
+ SimpleTest.finish();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/test_nofetch_handler.html b/dom/serviceworkers/test/test_nofetch_handler.html
new file mode 100644
index 0000000000..0725a68561
--- /dev/null
+++ b/dom/serviceworkers/test/test_nofetch_handler.html
@@ -0,0 +1,57 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for Bugs 1181127 and 1325101</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="error_reporting_helpers.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <meta http-equiv="Content-type" content="text/html;charset=UTF-8">
+</head>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1181127">Mozilla Bug 1181127</a>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1181127">Mozilla Bug 1325101</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+
+<script src="utils.js"></script>
+<script class="testbody" type="text/javascript">
+
+add_task(function setupPrefs() {
+ return SpecialPowers.pushPrefEnv({"set": [
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ // Make sure the event handler during the install event persists. This ensures
+ // the reason for which the interception doesn't occur is because of the
+ // handlesFetch=false flag from ServiceWorkerInfo.
+ ["dom.serviceWorkers.idle_timeout", 299999],
+ ]});
+});
+
+var iframeg;
+function create_iframe(url) {
+ return new Promise(function(res) {
+ iframe = document.createElement('iframe');
+ iframe.src = url;
+ iframe.onload = function() { res(iframe) }
+ document.body.appendChild(iframe);
+ iframeg = iframe;
+ })
+}
+
+add_task(async function test_nofetch_worker() {
+ let registration = await navigator.serviceWorker.register(
+ "nofetch_handler_worker.js", { scope: "./nofetch_handler_worker/"} )
+ .then(swr => waitForState(swr.installing, 'activated', swr));
+
+ let iframe = await create_iframe("./nofetch_handler_worker/doesnt_exist.html");
+ ok(!iframe.contentDocument.body.innerHTML.includes("intercepted"), "Request was not intercepted.");
+
+ await SpecialPowers.popPrefEnv();
+ await registration.unregister();
+});
+</script>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/test_not_intercept_plugin.html b/dom/serviceworkers/test/test_not_intercept_plugin.html
new file mode 100644
index 0000000000..4e7654deea
--- /dev/null
+++ b/dom/serviceworkers/test/test_not_intercept_plugin.html
@@ -0,0 +1,75 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1187766 - Test loading plugins scenarios with fetch interception.</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+<script src="utils.js"></script>
+<script class="testbody" type="text/javascript">
+ SimpleTest.requestCompleteLog();
+
+ var registration;
+ function simpleRegister() {
+ var p = navigator.serviceWorker.register("./fetch/plugin/worker.js", { scope: "./fetch/plugin/" });
+ return p.then(function(swr) {
+ registration = swr;
+ return waitForState(swr.installing, 'activated');
+ });
+ }
+
+ function unregister() {
+ return registration.unregister().then(function(success) {
+ ok(success, "Service worker should be unregistered successfully");
+ }, function(e) {
+ dump("SW unregistration error: " + e + "\n");
+ });
+ }
+
+ function testPlugins() {
+ var p = new Promise(function(resolve, reject) {
+ window.onmessage = function(e) {
+ if (e.data.status == "ok") {
+ ok(e.data.result, e.data.message);
+ } else if (e.data.status == "done") {
+ window.onmessage = null;
+ w.close();
+ resolve();
+ }
+ }
+ });
+
+ var w = window.open("fetch/plugin/plugins.html");
+ return p;
+ }
+
+ function runTest() {
+ simpleRegister()
+ .then(testPlugins)
+ .then(unregister)
+ .then(function() {
+ SimpleTest.finish();
+ }).catch(function(e) {
+ ok(false, "Some test failed with error " + e);
+ SimpleTest.finish();
+ });
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ SpecialPowers.pushPrefEnv({"set": [
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ ]}, runTest);
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/test_notification_constructor_error.html b/dom/serviceworkers/test/test_notification_constructor_error.html
new file mode 100644
index 0000000000..46d93e781f
--- /dev/null
+++ b/dom/serviceworkers/test/test_notification_constructor_error.html
@@ -0,0 +1,51 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug XXXXXXX - Check that Notification constructor throws in ServiceWorkerGlobalScope</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/dom/notification/test/mochitest/MockServices.js"></script>
+ <script type="text/javascript" src="/tests/dom/notification/test/mochitest/NotificationTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+<script class="testbody" type="text/javascript">
+
+ function simpleRegister() {
+ return navigator.serviceWorker.register("notification_constructor_error.js", { scope: "notification_constructor_error/" }).then(function(swr) {
+ ok(false, "Registration should fail.");
+ }, function(e) {
+ is(e.name, 'TypeError', "Registration should fail with a TypeError.");
+ });
+ }
+
+ function runTest() {
+ MockServices.register();
+ simpleRegister()
+ .then(function() {
+ MockServices.unregister();
+ SimpleTest.finish();
+ }).catch(function(e) {
+ ok(false, "Some test failed with error " + e);
+ MockServices.unregister();
+ SimpleTest.finish();
+ });
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ SpecialPowers.pushPrefEnv({"set": [
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ ["notification.prompt.testing", true],
+ ]}, runTest);
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/test_notification_get.html b/dom/serviceworkers/test/test_notification_get.html
new file mode 100644
index 0000000000..44239b4f9e
--- /dev/null
+++ b/dom/serviceworkers/test/test_notification_get.html
@@ -0,0 +1,213 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>ServiceWorkerRegistration.getNotifications() on main thread and worker thread.</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/dom/notification/test/mochitest/MockServices.js"></script>
+ <script type="text/javascript" src="/tests/dom/notification/test/mochitest/NotificationTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+<script type="text/javascript">
+
+ SimpleTest.requestFlakyTimeout("untriaged");
+
+ function testFrame(src) {
+ return new Promise(function(resolve, reject) {
+ var iframe = document.createElement("iframe");
+ iframe.src = src;
+ window.callback = function(result) {
+ iframe.src = "about:blank";
+ document.body.removeChild(iframe);
+ iframe = null;
+ SpecialPowers.exactGC(function() {
+ resolve(result);
+ });
+ };
+ document.body.appendChild(iframe);
+ });
+ }
+
+ function registerSW() {
+ return testFrame('notification/register.html').then(function() {
+ ok(true, "Registered service worker.");
+ });
+ }
+
+ function unregisterSW() {
+ return testFrame('notification/unregister.html').then(function() {
+ ok(true, "Unregistered service worker.");
+ });
+ }
+
+ // To check that the scope is respected when retrieving notifications.
+ function registerAlternateSWAndAddNotification() {
+ return testFrame('notification_alt/register.html').then(function() {
+ ok(true, "Registered alternate service worker.");
+ return navigator.serviceWorker.getRegistration("./notification_alt/").then(function(reg) {
+ return reg.showNotification("This is a notification_alt");
+ });
+ });
+ }
+
+ function unregisterAlternateSWAndAddNotification() {
+ return testFrame('notification_alt/unregister.html').then(function() {
+ ok(true, "unregistered alternate service worker.");
+ });
+ }
+
+ function testDismiss() {
+ // Dismissed persistent notifications should be removed from the
+ // notification list.
+ var alertsService = SpecialPowers.Cc["@mozilla.org/alerts-service;1"]
+ .getService(SpecialPowers.Ci.nsIAlertsService);
+ return navigator.serviceWorker.getRegistration("./notification/")
+ .then(function(reg) {
+ return reg.showNotification(
+ "This is a notification that will be closed", { tag: "dismiss" })
+ .then(function() {
+ return reg;
+ });
+ }).then(function(reg) {
+ return reg.getNotifications()
+ .then(function(notifications) {
+ is(notifications.length, 1, "There should be one visible notification");
+ is(notifications[0].tag, "dismiss", "Tag should match");
+
+ // Simulate dismissing the notification by using the alerts service
+ // directly, instead of `Notification#close`.
+ var principal = SpecialPowers.wrap(document).nodePrincipal;
+ var id = principal.origin + "#tag:dismiss";
+ alertsService.closeAlert(id, principal);
+
+ return reg;
+ });
+ }).then(function(reg) {
+ return reg.getNotifications();
+ }).then(function(notifications) {
+ // Make sure dismissed notifications are no longer retrieved.
+ is(notifications.length, 0, "There should be no more stored notifications");
+ });
+ }
+
+ function testGet() {
+ // Non persistent notifications will not show up in getNotification().
+ var n = new Notification("Scope does not match");
+ var options = NotificationTest.payload;
+ return navigator.serviceWorker.getRegistration("./notification/")
+ .then(function(reg) {
+ return reg.showNotification("This is a title", options)
+ .then(function() {
+ return reg;
+ });
+ }).then(function(reg) {
+ return registerAlternateSWAndAddNotification().then(function() {
+ return reg;
+ });
+ }).then(function(reg) {
+ return reg.getNotifications();
+ }).then(function(notifications) {
+ is(notifications.length, 1, "There should be one stored notification");
+ var notification = notifications[0];
+ ok(notification instanceof Notification, "Should be a Notification");
+ is(notification.title, "This is a title", "Title should match");
+ for (var key in options) {
+ if (key === "data") {
+ ok(NotificationTest.customDataMatches(notification.data),
+ "data property should match");
+ continue;
+ }
+ is(notification[key], options[key], key + " property should match");
+ }
+ notification.close();
+ }).then(function() {
+ return navigator.serviceWorker.getRegistration("./notification/").then(function(reg) {
+ return reg.getNotifications();
+ });
+ }).then(function(notifications) {
+ // Make sure closed notifications are no longer retrieved.
+ is(notifications.length, 0, "There should be no more stored notifications");
+ }).catch(function(e) {
+ ok(false, "Something went wrong " + e.message);
+ }).then(unregisterAlternateSWAndAddNotification);
+ }
+
+ function testGetWorker() {
+ todo(false, "navigator.serviceWorker is not available on workers yet");
+ return Promise.resolve();
+ }
+
+ function waitForSWTests(reg, msg) {
+ return new Promise(function(resolve, reject) {
+ var content = document.getElementById("content");
+
+ iframe = document.createElement("iframe");
+
+ content.appendChild(iframe);
+ iframe.setAttribute('src', "notification/listener.html");
+
+ window.onmessage = function(e) {
+ if (e.data.type == 'status') {
+ ok(e.data.status, "Service worker test: " + e.data.msg);
+ } else if (e.data.type == 'finish') {
+ content.removeChild(iframe);
+ resolve();
+ }
+ }
+
+ iframe.onload = function(e) {
+ iframe.onload = null;
+ reg.active.postMessage(msg);
+ }
+ });
+ }
+
+ function testGetServiceWorker() {
+ return navigator.serviceWorker.getRegistration("./notification/")
+ .then(function(reg) {
+ return waitForSWTests(reg, 'create');
+ });
+ }
+
+ // Create a Notification here, make sure ServiceWorker sees it.
+ function testAcrossThreads() {
+ return navigator.serviceWorker.getRegistration("./notification/")
+ .then(function(reg) {
+ return reg.showNotification("This is a title")
+ .then(function() {
+ return reg;
+ });
+ }).then(function(reg) {
+ return waitForSWTests(reg, 'do-not-create');
+ });
+ }
+
+ SimpleTest.waitForExplicitFinish();
+
+ MockServices.register();
+ SpecialPowers.pushPrefEnv({"set": [
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ ["dom.webnotifications.workers.enabled", true],
+ ["dom.webnotifications.serviceworker.enabled", true],
+ ["notification.prompt.testing", true],
+ ]}, function() {
+ registerSW()
+ .then(testGet)
+ .then(testGetWorker)
+ .then(testGetServiceWorker)
+ .then(testAcrossThreads)
+ .then(testDismiss)
+ .then(unregisterSW)
+ .then(function() {
+ MockServices.unregister();
+ SimpleTest.finish();
+ });
+ });
+</script>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/test_notification_openWindow.html b/dom/serviceworkers/test/test_notification_openWindow.html
new file mode 100644
index 0000000000..830c9e03d2
--- /dev/null
+++ b/dom/serviceworkers/test/test_notification_openWindow.html
@@ -0,0 +1,91 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1578070</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="utils.js"></script>
+ <script type="text/javascript" src="/tests/dom/notification/test/mochitest/MockServices.js"></script>
+ <script type="text/javascript" src="/tests/dom/notification/test/mochitest/NotificationTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+<script class="testbody" type="text/javascript">
+// eslint-disable-next-line mozilla/no-addtask-setup
+add_task(async function setup() {
+ await SpecialPowers.pushPrefEnv({"set": [
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ ["dom.webnotifications.workers.enabled", true],
+ ["dom.webnotifications.serviceworker.enabled", true],
+ ["notification.prompt.testing", true],
+ ["dom.serviceWorkers.disable_open_click_delay", 1000],
+ ["dom.serviceWorkers.idle_timeout", 299999],
+ ["dom.serviceWorkers.idle_extended_timeout", 299999]
+ ]});
+
+ MockServices.register();
+ SimpleTest.requestFlakyTimeout("Mock alert service dispatches show and click events.");
+ SimpleTest.registerCleanupFunction(() => {
+ MockServices.unregister();
+ });
+});
+
+add_task(async function test() {
+ info("Registering service worker.");
+ let swr = await navigator.serviceWorker.register("notification_openWindow_worker.js");
+ await waitForState(swr.installing, "activated");
+
+ SimpleTest.registerCleanupFunction(async () => {
+ await swr.unregister();
+ navigator.serviceWorker.onmessage = null;
+ });
+
+ for (let prefValue of [
+ SpecialPowers.Ci.nsIBrowserDOMWindow.OPEN_CURRENTWINDOW,
+ SpecialPowers.Ci.nsIBrowserDOMWindow.OPEN_NEWWINDOW,
+ SpecialPowers.Ci.nsIBrowserDOMWindow.OPEN_NEWTAB,
+ ]) {
+ if (prefValue == SpecialPowers.Ci.nsIBrowserDOMWindow.OPEN_CURRENTWINDOW) {
+ // Let's open a new tab and focus on it. When the service
+ // worker notification is shown, the document will open in the focused tab.
+ // If we don't open a new tab, the document will be opened in the
+ // current test-runner tab and mess up the test setup.
+ window.open("");
+ }
+ info(`Setting browser.link.open_newwindow to ${prefValue}.`);
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.link.open_newwindow", prefValue]],
+ });
+
+ // The onclicknotification handler uses Clients.openWindow() to open a new
+ // window. This newly created window will attempt to open another window with
+ // Window.open() and some arbitrary URL. We crash before the second window
+ // finishes loading.
+ info("Showing notification.");
+ await swr.showNotification("notification");
+
+ info("Waiting for \"DONE\" from worker.");
+ await new Promise(resolve => {
+ navigator.serviceWorker.onmessage = event => {
+ if (event.data !== "DONE") {
+ ok(false, `Unexpected message from service worker: ${JSON.stringify(event.data)}`);
+ }
+ resolve();
+ }
+ });
+
+ // If we make it here, then we didn't crash.
+ ok(true, "Didn't crash!");
+
+ navigator.serviceWorker.onmessage = null;
+ }
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/test_notificationclick-otherwindow.html b/dom/serviceworkers/test/test_notificationclick-otherwindow.html
new file mode 100644
index 0000000000..a1ffb71c39
--- /dev/null
+++ b/dom/serviceworkers/test/test_notificationclick-otherwindow.html
@@ -0,0 +1,65 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=916893
+-->
+<head>
+ <title>Bug 1114554 - Test ServiceWorkerGlobalScope.notificationclick event.</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/dom/notification/test/mochitest/MockServices.js"></script>
+ <script type="text/javascript" src="/tests/dom/notification/test/mochitest/NotificationTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1114554">Bug 1114554</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+</pre>
+<script src="utils.js"></script>
+<script type="text/javascript">
+ SimpleTest.requestFlakyTimeout("Mock alert service dispatches show and click events.");
+
+ function testFrame(src) {
+ var iframe = document.createElement("iframe");
+ iframe.src = src;
+ window.callback = function(result) {
+ window.callback = null;
+ document.body.removeChild(iframe);
+ iframe = null;
+ ok(result, "Got notificationclick event with correct data.");
+ MockServices.unregister();
+ registration.unregister().then(function() {
+ SimpleTest.finish();
+ });
+ };
+ document.body.appendChild(iframe);
+ }
+
+ var registration;
+
+ function runTest() {
+ MockServices.register();
+ navigator.serviceWorker.register("notificationclick.js", { scope: "notificationclick-otherwindow.html" }).then(function(reg) {
+ registration = reg;
+ return waitForState(reg.installing, 'activated');
+ }, function(e) {
+ ok(false, "registration should have passed!");
+ }).then(() => {
+ testFrame('notificationclick-otherwindow.html');
+ });
+ };
+
+ SimpleTest.waitForExplicitFinish();
+ SpecialPowers.pushPrefEnv({"set": [
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ ["dom.webnotifications.workers.enabled", true],
+ ["dom.webnotifications.serviceworker.enabled", true],
+ ["notification.prompt.testing", true],
+ ]}, runTest);
+</script>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/test_notificationclick.html b/dom/serviceworkers/test/test_notificationclick.html
new file mode 100644
index 0000000000..f733a3703f
--- /dev/null
+++ b/dom/serviceworkers/test/test_notificationclick.html
@@ -0,0 +1,66 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=916893
+-->
+<head>
+ <title>Bug 1114554 - Test ServiceWorkerGlobalScope.notificationclick event.</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/dom/notification/test/mochitest/MockServices.js"></script>
+ <script type="text/javascript" src="/tests/dom/notification/test/mochitest/NotificationTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1114554">Bug 1114554</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+</pre>
+<script src="utils.js"></script>
+<script type="text/javascript">
+ SimpleTest.requestFlakyTimeout("Mock alert service dispatches show and click events.");
+
+ function testFrame(src) {
+ var iframe = document.createElement("iframe");
+ iframe.src = src;
+ window.callback = function(result) {
+ window.callback = null;
+ document.body.removeChild(iframe);
+ iframe = null;
+ ok(result, "Got notificationclick event with correct data.");
+ MockServices.unregister();
+ registration.unregister().then(function() {
+ SimpleTest.finish();
+ });
+ };
+ document.body.appendChild(iframe);
+ }
+
+ var registration;
+
+ function runTest() {
+ MockServices.register();
+ navigator.serviceWorker.register("notificationclick.js", { scope: "notificationclick.html" }).then(function(reg) {
+ registration = reg;
+ return waitForState(reg.installing, 'activated');
+ }, function(e) {
+ ok(false, "registration should have passed!");
+ }).then(() => {
+ // Now that we know the document will be controlled, create the frame.
+ testFrame('notificationclick.html');
+ });
+ };
+
+ SimpleTest.waitForExplicitFinish();
+ SpecialPowers.pushPrefEnv({"set": [
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ ["dom.webnotifications.workers.enabled", true],
+ ["dom.webnotifications.serviceworker.enabled", true],
+ ["notification.prompt.testing", true],
+ ]}, runTest);
+</script>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/test_notificationclick_focus.html b/dom/serviceworkers/test/test_notificationclick_focus.html
new file mode 100644
index 0000000000..6a99112313
--- /dev/null
+++ b/dom/serviceworkers/test/test_notificationclick_focus.html
@@ -0,0 +1,66 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=916893
+-->
+<head>
+ <title>Bug 1144660 - Test client.focus() permissions on notification click</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/dom/notification/test/mochitest/MockServices.js"></script>
+ <script type="text/javascript" src="/tests/dom/notification/test/mochitest/NotificationTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1114554">Bug 1114554</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+</pre>
+<script src="utils.js"></script>
+<script type="text/javascript">
+ SimpleTest.requestFlakyTimeout("Mock alert service dispatches show and click events.");
+
+ function testFrame(src) {
+ var iframe = document.createElement("iframe");
+ iframe.src = src;
+ window.callback = function(result) {
+ window.callback = null;
+ document.body.removeChild(iframe);
+ iframe = null;
+ ok(result, "All tests passed.");
+ MockServices.unregister();
+ registration.unregister().then(function() {
+ SimpleTest.finish();
+ });
+ };
+ document.body.appendChild(iframe);
+ }
+
+ var registration;
+
+ function runTest() {
+ MockServices.register();
+ navigator.serviceWorker.register("notificationclick_focus.js", { scope: "notificationclick_focus.html" }).then(function(reg) {
+ registration = reg;
+ return waitForState(reg.installing, 'activated');
+ }, function(e) {
+ ok(false, "registration should have passed!");
+ }).then(() => {
+ testFrame('notificationclick_focus.html');
+ });
+ };
+
+ SimpleTest.waitForExplicitFinish();
+ SpecialPowers.pushPrefEnv({"set": [
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ ["dom.webnotifications.workers.enabled", true],
+ ["dom.webnotifications.serviceworker.enabled", true],
+ ["notification.prompt.testing", true],
+ ["dom.serviceWorkers.disable_open_click_delay", 1000],
+ ]}, runTest);
+</script>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/test_notificationclose.html b/dom/serviceworkers/test/test_notificationclose.html
new file mode 100644
index 0000000000..a1ae4f0a4e
--- /dev/null
+++ b/dom/serviceworkers/test/test_notificationclose.html
@@ -0,0 +1,67 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1265841
+-->
+<head>
+ <title>Bug 1265841 - Test ServiceWorkerGlobalScope.notificationclose event.</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/dom/notification/test/mochitest/MockServices.js"></script>
+ <script type="text/javascript" src="/tests/dom/notification/test/mochitest/NotificationTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1265841">Bug 1265841</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+</pre>
+<script src="utils.js"></script>
+<script type="text/javascript">
+ SimpleTest.requestFlakyTimeout("Mock alert service dispatches show, click, and close events.");
+
+ function testFrame(src) {
+ var iframe = document.createElement("iframe");
+ iframe.src = src;
+ window.callback = function(data) {
+ window.callback = null;
+ document.body.removeChild(iframe);
+ iframe = null;
+ ok(data.result, "Got notificationclose event with correct data.");
+ ok(!data.windowOpened,
+ "Shouldn't allow to openWindow in notificationclose");
+ MockServices.unregister();
+ registration.unregister().then(function() {
+ SimpleTest.finish();
+ });
+ };
+ document.body.appendChild(iframe);
+ }
+
+ var registration;
+
+ function runTest() {
+ MockServices.register();
+ navigator.serviceWorker.register("notificationclose.js", { scope: "notificationclose.html" }).then(function(reg) {
+ registration = reg;
+ return waitForState(reg.installing, 'activated');
+ }, function(e) {
+ ok(false, "registration should have passed!");
+ }).then(() => {
+ testFrame('notificationclose.html');
+ });
+ };
+
+ SimpleTest.waitForExplicitFinish();
+ SpecialPowers.pushPrefEnv({"set": [
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ ["dom.webnotifications.workers.enabled", true],
+ ["dom.webnotifications.serviceworker.enabled", true],
+ ["notification.prompt.testing", true],
+ ]}, runTest);
+</script>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/test_onmessageerror.html b/dom/serviceworkers/test/test_onmessageerror.html
new file mode 100644
index 0000000000..425b890951
--- /dev/null
+++ b/dom/serviceworkers/test/test_onmessageerror.html
@@ -0,0 +1,128 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Test onmessageerror event handlers</title>
+ </head>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="utils.js"></script>
+ <script>
+ /**
+ * Test that ServiceWorkerGlobalScope and ServiceWorkerContainer handle
+ * `messageerror` events, using a test helper class `StructuredCloneTester`.
+ * Intances of this class can be configured to fail to serialize or
+ * deserialize, as it's difficult to artificially create the case where an
+ * object successfully serializes but fails to deserialize (which can be
+ * caused by out-of-memory failures or the target global not supporting a
+ * serialized interface).
+ */
+
+ let registration = null;
+ let serviceWorker = null;
+ let serviceWorkerContainer = null;
+ const swScript = 'onmessageerror_worker.js';
+
+ add_task(async () => {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ['dom.serviceWorkers.enabled', true],
+ ['dom.serviceWorkers.testing.enabled', true],
+ ['dom.testing.structuredclonetester.enabled', true],
+ ],
+ });
+
+ swContainer = navigator.serviceWorker;
+
+ registration = await swContainer.register(swScript);
+ ok(registration, 'Service Worker regsisters');
+
+ serviceWorker = registration.installing;
+ await waitForState(serviceWorker, 'activated');
+ }); // setup
+
+ add_task(async () => {
+ const serializable = true;
+ const deserializable = true;
+ let sct = new StructuredCloneTester(serializable, deserializable);
+
+ const p = new Promise((resolve, reject) => {
+ function onMessage(e) {
+ const expectedBehavior = 'Serializable and deserializable ' +
+ 'StructuredCloneTester serializes and deserializes';
+
+ is(e.data.received, 'message', expectedBehavior);
+ swContainer.removeEventListener('message', onMessage);
+ resolve();
+ }
+
+ swContainer.addEventListener('message', onMessage);
+ });
+
+ serviceWorker.postMessage({ serializable, deserializable, sct });
+
+ await p;
+ });
+
+ add_task(async () => {
+ const serializable = false;
+ // if it's not serializable, being deserializable or not doesn't matter
+ const deserializable = false;
+ let sct = new StructuredCloneTester(serializable, deserializable);
+
+ try {
+ serviceWorker.postMessage({ serializable, deserializable, sct });
+ ok(false, 'StructuredCloneTester serialization should have thrown -- ' +
+ 'this line should not have been reached.');
+ } catch (e) {
+ const expectedBehavior = 'Unserializable StructuredCloneTester fails ' +
+ `to send, with exception name: ${e.name}`;
+ is(e.name, 'DataCloneError', expectedBehavior);
+ }
+ });
+
+ add_task(async () => {
+ const serializable = true;
+ const deserializable = false;
+ let sct = new StructuredCloneTester(serializable, deserializable);
+
+ const p = new Promise((resolve, reject) => {
+ function onMessage(e) {
+ const expectedBehavior = 'ServiceWorkerGlobalScope handles ' +
+ 'messageerror events';
+
+ is(e.data.received, 'messageerror', expectedBehavior);
+ swContainer.removeEventListener('message', onMessage);
+ resolve();
+ }
+
+ swContainer.addEventListener('message', onMessage);
+ });
+
+ serviceWorker.postMessage({ serializable, deserializable, sct });
+
+ await p;
+ }); // test ServiceWorkerGlobalScope onmessageerror
+
+ add_task(async () => {
+ const p = new Promise((resolve, reject) => {
+ function onMessageError(e) {
+ ok(true, 'ServiceWorkerContainer handles messageerror events');
+ swContainer.removeEventListener('messageerror', onMessageError);
+ resolve();
+ }
+
+ swContainer.addEventListener('messageerror', onMessageError);
+ });
+
+ serviceWorker.postMessage('send-bad-message');
+
+ await p;
+ }); // test ServiceWorkerContainer onmessageerror
+
+ add_task(async () => {
+ await SpecialPowers.popPrefEnv();
+ ok(await registration.unregister(), 'Service Worker unregisters');
+ }); // teardown
+ </script>
+ <body>
+ </body>
+</html>
diff --git a/dom/serviceworkers/test/test_opaque_intercept.html b/dom/serviceworkers/test/test_opaque_intercept.html
new file mode 100644
index 0000000000..f0e40e0402
--- /dev/null
+++ b/dom/serviceworkers/test/test_opaque_intercept.html
@@ -0,0 +1,93 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 982726 - Test service worker post message </title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+<script class="testbody" type="text/javascript">
+ var registration;
+ function start() {
+ return navigator.serviceWorker.register("opaque_intercept_worker.js",
+ { scope: "./sw_clients/" })
+ .then((swr) => registration = swr);
+ }
+
+ function unregister() {
+ return registration.unregister().then(function(result) {
+ ok(result, "Unregister should return true.");
+ }, function(e) {
+ dump("Unregistering the SW failed with " + e + "\n");
+ });
+ }
+
+
+ function testOpaqueIntercept(swr) {
+ var p = new Promise(function(res, rej) {
+ var ready = false;
+ var scriptLoaded = false;
+ window.onmessage = function(e) {
+ if (e.data === "READY") {
+ ok(!ready, "ready message should only be received once");
+ ok(!scriptLoaded, "ready message should be received before script loaded");
+ if (ready) {
+ res();
+ return;
+ }
+ ready = true;
+ iframe.contentWindow.postMessage("REFRESH", "*");
+ } else if (e.data === "SCRIPT_LOADED") {
+ ok(ready, "script loaded should be received after ready");
+ ok(!scriptLoaded, "script loaded message should be received only once");
+ scriptLoaded = true;
+ res();
+ }
+ }
+ });
+
+ var content = document.getElementById("content");
+ ok(content, "Parent exists.");
+
+ var iframe = document.createElement("iframe");
+ iframe.setAttribute('src', "sw_clients/refresher.html");
+ content.appendChild(iframe);
+
+ // Our service worker waits for us to finish installing. If it didn't do
+ // this, then loading our frame would race with it becoming active,
+ // possibly intercepting the first load of the iframe. This guarantees
+ // that our iframe will load first directly from the network. Note that
+ // refresher.html explicitly waits for the service worker to transition to
+ // active.
+ registration.installing.postMessage("ready");
+
+ return p.then(() => content.removeChild(iframe));
+ }
+
+ function runTest() {
+ start()
+ .then(testOpaqueIntercept)
+ .then(unregister)
+ .catch(function(e) {
+ ok(false, "Some test failed with error " + e);
+ }).then(SimpleTest.finish);
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ SpecialPowers.pushPrefEnv({"set": [
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ ["dom.caches.enabled", true],
+ ]}, runTest);
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/test_openWindow.html b/dom/serviceworkers/test/test_openWindow.html
new file mode 100644
index 0000000000..448d143edd
--- /dev/null
+++ b/dom/serviceworkers/test/test_openWindow.html
@@ -0,0 +1,112 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1172870
+-->
+<head>
+ <title>Bug 1172870 - Test clients.openWindow</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/dom/notification/test/mochitest/MockServices.js"></script>
+ <script type="text/javascript" src="/tests/dom/notification/test/mochitest/NotificationTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1172870">Bug 1172870</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+</pre>
+<script src="utils.js"></script>
+<script type="text/javascript">
+ SimpleTest.requestFlakyTimeout("Mock alert service dispatches show and click events.");
+
+ function setup(ctx) {
+ MockServices.register();
+
+ return navigator.serviceWorker.register("openWindow_worker.js", {scope: "./"})
+ .then(function(swr) {
+ ok(swr, "Registration successful");
+ ctx.registration = swr;
+ return waitForState(swr.installing, 'activated', ctx);
+ });
+ }
+
+ function setupMessageHandler(ctx) {
+ return new Promise(function(res, rej) {
+ navigator.serviceWorker.onmessage = function(event) {
+ navigator.serviceWorker.onmessage = null;
+ for (i = 0; i < event.data.length; i++) {
+ ok(event.data[i].result, event.data[i].message);
+ }
+ res(ctx);
+ }
+ });
+ }
+
+ function testPopupNotAllowed(ctx) {
+ var p = setupMessageHandler(ctx);
+ ok(ctx.registration.active, "Worker is active.");
+ ctx.registration.active.postMessage("testNoPopup");
+
+ return p;
+ }
+
+ function testPopupAllowed(ctx) {
+ var p = setupMessageHandler(ctx);
+ ctx.registration.showNotification("testPopup");
+
+ return p;
+ }
+
+ function checkNumberOfWindows(ctx) {
+ return new Promise(function(res, rej) {
+ navigator.serviceWorker.onmessage = function(event) {
+ navigator.serviceWorker.onmessage = null;
+ for (i = 0; i < event.data.length; i++) {
+ ok(event.data[i].result, event.data[i].message);
+ }
+ res(ctx);
+ }
+ ctx.registration.active.postMessage("CHECK_NUMBER_OF_WINDOWS");
+ });
+ }
+
+ function clear(ctx) {
+ MockServices.unregister();
+
+ return ctx.registration.unregister().then(function(result) {
+ ctx.registration = null;
+ ok(result, "Unregister was successful.");
+ });
+ }
+
+ function runTest() {
+ setup({})
+ // Permission to allow popups persists for some time after a notification
+ // click event, so the order here is important.
+ .then(testPopupNotAllowed)
+ .then(testPopupAllowed)
+ .then(checkNumberOfWindows)
+ .then(clear)
+ .catch(function(e) {
+ ok(false, "Some test failed with error " + e);
+ }).then(SimpleTest.finish);
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ SpecialPowers.pushPrefEnv({"set": [
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ ["dom.webnotifications.workers.enabled", true],
+ ["dom.webnotifications.serviceworker.enabled", true],
+ ["notification.prompt.testing", true],
+ ["dom.serviceWorkers.disable_open_click_delay", 1000],
+ ["dom.serviceWorkers.idle_timeout", 299999],
+ ["dom.serviceWorkers.idle_extended_timeout", 299999],
+ ["dom.securecontext.allowlist", "mochi.test,example.com"],
+ ]}, runTest);
+</script>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/test_origin_after_redirect.html b/dom/serviceworkers/test/test_origin_after_redirect.html
new file mode 100644
index 0000000000..e4c0af23be
--- /dev/null
+++ b/dom/serviceworkers/test/test_origin_after_redirect.html
@@ -0,0 +1,58 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test the origin of a redirected response from a service worker</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+<iframe></iframe>
+</div>
+<pre id="test"></pre>
+<script class="testbody" type="text/javascript">
+
+ var iframe;
+ function runTest() {
+ iframe = document.querySelector("iframe");
+ iframe.src = "/tests/dom/serviceworkers/test/fetch/origin/register.html";
+ var win;
+ window.onmessage = function(e) {
+ if (e.data.status == "ok") {
+ ok(e.data.result, e.data.message);
+ } else if (e.data.status == "registrationdone") {
+ win = window.open("/tests/dom/serviceworkers/test/fetch/origin/index.sjs", "mywindow", "width=100,height=100");
+ } else if (e.data.status == "domain") {
+ is(e.data.data, "example.org", "Correct domain expected");
+ } else if (e.data.status == "origin") {
+ is(e.data.data, "http://example.org", "Correct origin expected");
+ } else if (e.data.status == "done") {
+ win.close();
+ iframe.src = "/tests/dom/serviceworkers/test/fetch/origin/unregister.html";
+ } else if (e.data.status == "unregistrationdone") {
+ window.onmessage = null;
+ ok(true, "Test finished successfully");
+ SimpleTest.finish();
+ }
+ };
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ onload = function() {
+ SpecialPowers.pushPrefEnv({"set": [
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ ["dom.caches.enabled", true],
+ ["dom.security.https_first", false],
+ ]}, runTest);
+ };
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/test_origin_after_redirect_cached.html b/dom/serviceworkers/test/test_origin_after_redirect_cached.html
new file mode 100644
index 0000000000..ca79581ccf
--- /dev/null
+++ b/dom/serviceworkers/test/test_origin_after_redirect_cached.html
@@ -0,0 +1,58 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test the origin of a redirected response from a service worker</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+<iframe></iframe>
+</div>
+<pre id="test"></pre>
+<script class="testbody" type="text/javascript">
+
+ var iframe;
+ function runTest() {
+ iframe = document.querySelector("iframe");
+ iframe.src = "/tests/dom/serviceworkers/test/fetch/origin/register.html";
+ var win;
+ window.onmessage = function(e) {
+ if (e.data.status == "ok") {
+ ok(e.data.result, e.data.message);
+ } else if (e.data.status == "registrationdone") {
+ win = window.open("/tests/dom/serviceworkers/test/fetch/origin/index-cached.sjs", "mywindow", "width=100,height=100");
+ } else if (e.data.status == "domain") {
+ is(e.data.data, "example.org", "Correct domain expected");
+ } else if (e.data.status == "origin") {
+ is(e.data.data, "http://example.org", "Correct origin expected");
+ } else if (e.data.status == "done") {
+ win.close();
+ iframe.src = "/tests/dom/serviceworkers/test/fetch/origin/unregister.html";
+ } else if (e.data.status == "unregistrationdone") {
+ window.onmessage = null;
+ ok(true, "Test finished successfully");
+ SimpleTest.finish();
+ }
+ };
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ onload = function() {
+ SpecialPowers.pushPrefEnv({"set": [
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ ["dom.caches.enabled", true],
+ ["dom.security.https_first", false],
+ ]}, runTest);
+ };
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/test_origin_after_redirect_to_https.html b/dom/serviceworkers/test/test_origin_after_redirect_to_https.html
new file mode 100644
index 0000000000..927a68ef3a
--- /dev/null
+++ b/dom/serviceworkers/test/test_origin_after_redirect_to_https.html
@@ -0,0 +1,57 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test the origin of a redirected response from a service worker</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+<iframe></iframe>
+</div>
+<pre id="test"></pre>
+<script class="testbody" type="text/javascript">
+
+ var iframe;
+ function runTest() {
+ iframe = document.querySelector("iframe");
+ iframe.src = "/tests/dom/serviceworkers/test/fetch/origin/register.html";
+ var win;
+ window.onmessage = function(e) {
+ if (e.data.status == "ok") {
+ ok(e.data.result, e.data.message);
+ } else if (e.data.status == "registrationdone") {
+ win = window.open("/tests/dom/serviceworkers/test/fetch/origin/index-to-https.sjs", "mywindow", "width=100,height=100");
+ } else if (e.data.status == "domain") {
+ is(e.data.data, "example.org", "Correct domain expected");
+ } else if (e.data.status == "origin") {
+ is(e.data.data, "https://example.org", "Correct origin expected");
+ } else if (e.data.status == "done") {
+ win.close();
+ iframe.src = "/tests/dom/serviceworkers/test/fetch/origin/unregister.html";
+ } else if (e.data.status == "unregistrationdone") {
+ window.onmessage = null;
+ ok(true, "Test finished successfully");
+ SimpleTest.finish();
+ }
+ };
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ onload = function() {
+ SpecialPowers.pushPrefEnv({"set": [
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ ["dom.caches.enabled", true],
+ ]}, runTest);
+ };
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/test_origin_after_redirect_to_https_cached.html b/dom/serviceworkers/test/test_origin_after_redirect_to_https_cached.html
new file mode 100644
index 0000000000..29686e2302
--- /dev/null
+++ b/dom/serviceworkers/test/test_origin_after_redirect_to_https_cached.html
@@ -0,0 +1,57 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test the origin of a redirected response from a service worker</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+<iframe></iframe>
+</div>
+<pre id="test"></pre>
+<script class="testbody" type="text/javascript">
+
+ var iframe;
+ function runTest() {
+ iframe = document.querySelector("iframe");
+ iframe.src = "/tests/dom/serviceworkers/test/fetch/origin/register.html";
+ var win;
+ window.onmessage = function(e) {
+ if (e.data.status == "ok") {
+ ok(e.data.result, e.data.message);
+ } else if (e.data.status == "registrationdone") {
+ win = window.open("/tests/dom/serviceworkers/test/fetch/origin/index-to-https-cached.sjs", "mywindow", "width=100,height=100");
+ } else if (e.data.status == "domain") {
+ is(e.data.data, "example.org", "Correct domain expected");
+ } else if (e.data.status == "origin") {
+ is(e.data.data, "https://example.org", "Correct origin expected");
+ } else if (e.data.status == "done") {
+ win.close();
+ iframe.src = "/tests/dom/serviceworkers/test/fetch/origin/unregister.html";
+ } else if (e.data.status == "unregistrationdone") {
+ window.onmessage = null;
+ ok(true, "Test finished successfully");
+ SimpleTest.finish();
+ }
+ };
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ onload = function() {
+ SpecialPowers.pushPrefEnv({"set": [
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ ["dom.caches.enabled", true],
+ ]}, runTest);
+ };
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/test_post_message.html b/dom/serviceworkers/test/test_post_message.html
new file mode 100644
index 0000000000..b72f948dd6
--- /dev/null
+++ b/dom/serviceworkers/test/test_post_message.html
@@ -0,0 +1,80 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 982726 - Test service worker post message </title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+<script src="utils.js"></script>
+<script class="testbody" type="text/javascript">
+ var magic_value = "MAGIC_VALUE_123";
+ var registration;
+ function start() {
+ return navigator.serviceWorker.register("message_posting_worker.js",
+ { scope: "./sw_clients/" })
+ .then(swr => waitForState(swr.installing, 'activated', swr))
+ .then((swr) => registration = swr);
+ }
+
+ function unregister() {
+ return registration.unregister().then(function(result) {
+ ok(result, "Unregister should return true.");
+ }, function(e) {
+ dump("Unregistering the SW failed with " + e + "\n");
+ });
+ }
+
+
+ function testPostMessage(swr) {
+ var p = new Promise(function(res, rej) {
+ window.onmessage = function(e) {
+ if (e.data === "READY") {
+ swr.active.postMessage(magic_value);
+ } else if (e.data === magic_value) {
+ ok(true, "Worker posted the correct value.");
+ res();
+ } else {
+ ok(false, "Wrong value. Expected: " + magic_value +
+ ", got: " + e.data);
+ res();
+ }
+ }
+ });
+
+ var content = document.getElementById("content");
+ ok(content, "Parent exists.");
+
+ iframe = document.createElement("iframe");
+ iframe.setAttribute('src', "sw_clients/service_worker_controlled.html");
+ content.appendChild(iframe);
+
+ return p.then(() => content.removeChild(iframe));
+ }
+
+ function runTest() {
+ start()
+ .then(testPostMessage)
+ .then(unregister)
+ .catch(function(e) {
+ ok(false, "Some test failed with error " + e);
+ }).then(SimpleTest.finish);
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ SpecialPowers.pushPrefEnv({"set": [
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true]
+ ]}, runTest);
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/test_post_message_advanced.html b/dom/serviceworkers/test/test_post_message_advanced.html
new file mode 100644
index 0000000000..580dfd3f07
--- /dev/null
+++ b/dom/serviceworkers/test/test_post_message_advanced.html
@@ -0,0 +1,109 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 982726 - Test service worker post message advanced </title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+<script src="utils.js"></script>
+<script class="testbody" type="text/javascript">
+ var registration;
+ var base = ["string", true, 42];
+ var blob = new Blob(["blob_content"]);
+ var file = new File(["file_content"], "file");
+ var obj = { body : "object_content" };
+
+ function readBlob(blobToRead) {
+ return new Promise(function(resolve, reject) {
+ var reader = new FileReader();
+ reader.onloadend = () => resolve(reader.result);
+ reader.readAsText(blobToRead);
+ });
+ }
+
+ function equals(v1, v2) {
+ return Promise.all([v1, v2]).then(function(val) {
+ ok(val[0] === val[1], "Values should match.");
+ });
+ }
+
+ function blob_equals(b1, b2) {
+ return equals(readBlob(b1), readBlob(b2));
+ }
+
+ function file_equals(f1, f2) {
+ return equals(f1.name, f2.name).then(blob_equals(f1, f2));
+ }
+
+ function obj_equals(o1, o2) {
+ return equals(o1.body, o2.body);
+ }
+
+ function start() {
+ return navigator.serviceWorker.register("message_posting_worker.js",
+ { scope: "./sw_clients/" })
+ .then(swr => waitForState(swr.installing, 'activated', swr))
+ .then((swr) => registration = swr);
+ }
+
+ function unregister() {
+ return registration.unregister().then(function(result) {
+ ok(result, "Unregister should return true.");
+ }, function(e) {
+ dump("Unregistering the SW failed with " + e + "\n");
+ });
+ }
+
+ function testPostMessageObject(object, test) {
+ var p = new Promise(function(res, rej) {
+ window.onmessage = function(e) {
+ if (e.data === "READY") {
+ registration.active.postMessage(object)
+ } else {
+ test(object, e.data).then(res);
+ }
+ }
+ });
+
+ var content = document.getElementById("content");
+ ok(content, "Parent exists.");
+
+ iframe = document.createElement("iframe");
+ iframe.setAttribute('src', "sw_clients/service_worker_controlled.html");
+ content.appendChild(iframe);
+
+ return p.then(() => content.removeChild(iframe));
+ }
+
+ function runTest() {
+ start()
+ .then(testPostMessageObject.bind(this, base[0], equals))
+ .then(testPostMessageObject.bind(this, base[1], equals))
+ .then(testPostMessageObject.bind(this, base[2], equals))
+ .then(testPostMessageObject.bind(this, blob, blob_equals))
+ .then(testPostMessageObject.bind(this, file, file_equals))
+ .then(testPostMessageObject.bind(this, obj, obj_equals))
+ .then(unregister)
+ .catch(function(e) {
+ ok(false, "Some test failed with error " + e);
+ }).then(SimpleTest.finish);
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ SpecialPowers.pushPrefEnv({"set": [
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true]
+ ]}, runTest);
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/test_post_message_source.html b/dom/serviceworkers/test/test_post_message_source.html
new file mode 100644
index 0000000000..b72ebe3a7c
--- /dev/null
+++ b/dom/serviceworkers/test/test_post_message_source.html
@@ -0,0 +1,66 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1142015 - Test service worker post message source </title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+<script class="testbody" type="text/javascript">
+ var magic_value = "MAGIC_VALUE_RANDOM";
+ var registration;
+ function start() {
+ return navigator.serviceWorker.register("source_message_posting_worker.js",
+ { scope: "./nonexistent_scope/" })
+ .then((swr) => registration = swr);
+ }
+
+ function unregister() {
+ return registration.unregister().then(function(result) {
+ ok(result, "Unregister should return true.");
+ }, function(e) {
+ dump("Unregistering the SW failed with " + e + "\n");
+ });
+ }
+
+
+ function testPostMessage(swr) {
+ var p = new Promise(function(res, rej) {
+ navigator.serviceWorker.onmessage = function(e) {
+ ok(e.data === magic_value, "Worker posted the correct value.");
+ res();
+ }
+ });
+
+ ok(swr.installing, "Installing worker exists.");
+ swr.installing.postMessage(magic_value);
+ return p;
+ }
+
+
+ function runTest() {
+ start()
+ .then(testPostMessage)
+ .then(unregister)
+ .catch(function(e) {
+ ok(false, "Some test failed with error " + e);
+ }).then(SimpleTest.finish);
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ SpecialPowers.pushPrefEnv({"set": [
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true]
+ ]}, runTest);
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/test_privateBrowsing.html b/dom/serviceworkers/test/test_privateBrowsing.html
new file mode 100644
index 0000000000..6a10f375f9
--- /dev/null
+++ b/dom/serviceworkers/test/test_privateBrowsing.html
@@ -0,0 +1,103 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+ <title>Test for ServiceWorker - Private Browsing</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"?>
+</head>
+<body>
+
+<script type="application/javascript">
+const {BrowserTestUtils} = ChromeUtils.import("resource://testing-common/BrowserTestUtils.jsm");
+
+var mainWindow;
+
+var contentPage = "http://mochi.test:8888/chrome/dom/workers/test/empty.html";
+var workerScope = "http://mochi.test:8888/chrome/dom/serviceworkers/test/";
+var workerURL = workerScope + "worker.js";
+
+function testOnWindow(aIsPrivate, aCallback) {
+ var win = mainWindow.OpenBrowserWindow({private: aIsPrivate});
+ win.addEventListener("load", function() {
+ win.addEventListener("DOMContentLoaded", function onInnerLoad() {
+ if (win.content.location.href != contentPage) {
+ BrowserTestUtils.loadURI(win.gBrowser, contentPage);
+ return;
+ }
+
+ win.removeEventListener("DOMContentLoaded", onInnerLoad, true);
+ SimpleTest.executeSoon(function() { aCallback(win); });
+ }, true);
+ }, {capture: true, once: true});
+}
+
+function setupWindow() {
+ mainWindow = window.browsingContext.topChromeWindow;
+ runTest();
+}
+
+var wN;
+var registration;
+var wP;
+
+function testPrivateWindow() {
+ testOnWindow(true, function(aWin) {
+ wP = aWin;
+ ok(!wP.content.eval('"serviceWorker" in navigator'), "ServiceWorkers are not available for private windows");
+ runTest();
+ });
+}
+
+function doTests() {
+ testOnWindow(false, function(aWin) {
+ wN = aWin;
+ ok("serviceWorker" in wN.content.navigator, "ServiceWorkers are available for normal windows");
+
+ wN.content.navigator.serviceWorker.register(workerURL,
+ { scope: workerScope })
+ .then(function(aRegistration) {
+ registration = aRegistration;
+ ok(registration, "Registering a service worker in a normal window should succeed");
+
+ // Bug 1255621: We should be able to load a controlled document in a private window.
+ testPrivateWindow();
+ }, function(aError) {
+ ok(false, "Error registering worker in normal window: " + aError);
+ testPrivateWindow();
+ });
+ });
+}
+
+var steps = [
+ setupWindow,
+ doTests
+];
+
+function cleanup() {
+ wN.close();
+ wP.close();
+
+ SimpleTest.finish();
+}
+
+function runTest() {
+ if (!steps.length) {
+ registration.unregister().then(cleanup, cleanup);
+
+ return;
+ }
+
+ var step = steps.shift();
+ step();
+}
+
+SimpleTest.waitForExplicitFinish();
+SpecialPowers.pushPrefEnv({"set": [
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ ["browser.startup.page", 0],
+ ["browser.startup.homepage_override.mstone", "ignore"],
+]}, runTest);
+
+</script>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/test_register_base.html b/dom/serviceworkers/test/test_register_base.html
new file mode 100644
index 0000000000..3a1f2f2621
--- /dev/null
+++ b/dom/serviceworkers/test/test_register_base.html
@@ -0,0 +1,34 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test that registering a service worker uses the docuemnt URI for the secure origin check</title>
+ <script type="text/javascript" src="http://mochi.test:8888/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="http://mochi.test:8888/tests/SimpleTest/test.css" />
+ <base href="https://mozilla.org/">
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test"></pre>
+<script class="testbody" type="text/javascript">
+
+ function runTest() {
+ ok(!("serviceWorker" in navigator), "ServiceWorkerContainer shouldn't be defined");
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ onload = function() {
+ SpecialPowers.pushPrefEnv({"set": [
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ["dom.serviceWorkers.enabled", true],
+ ]}, runTest);
+ };
+</script>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/test_register_https_in_http.html b/dom/serviceworkers/test/test_register_https_in_http.html
new file mode 100644
index 0000000000..096c3733a0
--- /dev/null
+++ b/dom/serviceworkers/test/test_register_https_in_http.html
@@ -0,0 +1,45 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1172948 - Test that registering a service worker from inside an HTTPS iframe embedded in an HTTP iframe doesn't work</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test"></pre>
+<script class="testbody" type="text/javascript">
+
+ function runTest() {
+ var iframe = document.createElement("iframe");
+ iframe.src = "https://example.com/tests/dom/serviceworkers/test/register_https.html";
+ document.body.appendChild(iframe);
+
+ window.onmessage = event => {
+ switch (event.data.type) {
+ case "ok":
+ ok(event.data.status, event.data.msg);
+ break;
+ case "done":
+ SimpleTest.finish();
+ break;
+ }
+ };
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ onload = function() {
+ SpecialPowers.pushPrefEnv({"set": [
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ["dom.serviceWorkers.enabled", true],
+ ]}, runTest);
+ };
+</script>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/test_sandbox_intercept.html b/dom/serviceworkers/test/test_sandbox_intercept.html
new file mode 100644
index 0000000000..2aa120994f
--- /dev/null
+++ b/dom/serviceworkers/test/test_sandbox_intercept.html
@@ -0,0 +1,56 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1142727 - Test that sandboxed iframes are not intercepted</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content">
+<iframe id="normal-frame"></iframe>
+<iframe sandbox="allow-scripts" id="sandbox-frame"></iframe>
+</div>
+<pre id="test"></pre>
+<script class="testbody" type="text/javascript">
+
+ var normalFrame;
+ var sandboxFrame;
+ function runTest() {
+ normalFrame = document.getElementById("normal-frame");
+ sandboxFrame = document.getElementById("sandbox-frame");
+ normalFrame.src = "/tests/dom/serviceworkers/test/fetch/sandbox/register.html";
+ window.onmessage = function(e) {
+ if (e.data.status == "ok") {
+ ok(e.data.result, e.data.message);
+ } else if (e.data.status == "registrationdone") {
+ normalFrame.src = "about:blank";
+ sandboxFrame.src = "/tests/dom/serviceworkers/test/fetch/sandbox/index.html";
+ } else if (e.data.status == "done") {
+ sandboxFrame.src = "about:blank";
+ normalFrame.src = "/tests/dom/serviceworkers/test/fetch/sandbox/unregister.html";
+ } else if (e.data.status == "unregistrationdone") {
+ normalFrame.src = "about:blank";
+ window.onmessage = null;
+ ok(true, "Test finished successfully");
+ SimpleTest.finish();
+ }
+ };
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ onload = function() {
+ SpecialPowers.pushPrefEnv({"set": [
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ ]}, runTest);
+ };
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/test_sanitize.html b/dom/serviceworkers/test/test_sanitize.html
new file mode 100644
index 0000000000..dd6bd42c8f
--- /dev/null
+++ b/dom/serviceworkers/test/test_sanitize.html
@@ -0,0 +1,86 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1080109 - Clear ServiceWorker registrations for all domains</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+<script class="testbody" type="text/javascript">
+
+ function start() {
+ const Cc = SpecialPowers.Cc;
+ const Ci = SpecialPowers.Ci;
+
+ function testNotIntercepted() {
+ testFrame("sanitize/frame.html").then(function(body) {
+ is(body, "FAIL", "Expected frame to not be controlled");
+ // No need to unregister since that already happened.
+ navigator.serviceWorker.getRegistration("sanitize/foo").then(function(reg) {
+ ok(reg === undefined, "There should no longer be a valid registration");
+ }, function(e) {
+ ok(false, "getRegistration() should not error");
+ }).then(function(e) {
+ SimpleTest.finish();
+ });
+ });
+ }
+
+ registerSW().then(function() {
+ return testFrame("sanitize/frame.html").then(function(body) {
+ is(body, "intercepted", "Expected serviceworker to intercept request");
+ });
+ }).then(function() {
+ return navigator.serviceWorker.getRegistration("sanitize/foo");
+ }).then(function(reg) {
+ reg.active.onstatechange = function(e) {
+ e.target.onstatechange = null;
+ is(e.target.state, "redundant", "On clearing data, serviceworker should become redundant");
+ testNotIntercepted();
+ };
+ }).then(function() {
+ SpecialPowers.removeAllServiceWorkerData();
+ });
+ }
+
+ function testFrame(src) {
+ return new Promise(function(resolve, reject) {
+ var iframe = document.createElement("iframe");
+ iframe.src = src;
+ window.onmessage = function(message) {
+ window.onmessage = null;
+ iframe.src = "about:blank";
+ document.body.removeChild(iframe);
+ iframe = null;
+ SpecialPowers.exactGC(function() {
+ resolve(message.data);
+ });
+ };
+ document.body.appendChild(iframe);
+ });
+ }
+
+ function registerSW() {
+ return testFrame("sanitize/register.html");
+ }
+
+ SimpleTest.waitForExplicitFinish();
+
+ SpecialPowers.pushPrefEnv({"set": [
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ ]}, function() {
+ start();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/test_sanitize_domain.html b/dom/serviceworkers/test/test_sanitize_domain.html
new file mode 100644
index 0000000000..d0f5f7f69a
--- /dev/null
+++ b/dom/serviceworkers/test/test_sanitize_domain.html
@@ -0,0 +1,89 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1080109 - Clear ServiceWorker registrations for specific domains</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+<script class="testbody" type="text/javascript">
+
+ function start() {
+ const Cc = SpecialPowers.Cc;
+ const Ci = SpecialPowers.Ci;
+
+ function checkDomainRegistration(domain, exists) {
+ return testFrame("http://" + domain + "/tests/dom/serviceworkers/test/sanitize/example_check_and_unregister.html").then(function(body) {
+ if (body === "FAIL") {
+ ok(false, "Error acquiring registration or unregistering for " + domain);
+ } else {
+ if (exists) {
+ ok(body === true, "Expected " + domain + " to still have a registration.");
+ } else {
+ ok(body === false, "Expected " + domain + " to have no registration.");
+ }
+ }
+ });
+ }
+
+ registerSW().then(function() {
+ return testFrame("http://example.com/tests/dom/serviceworkers/test/sanitize/frame.html").then(function(body) {
+ is(body, "intercepted", "Expected serviceworker to intercept request");
+ });
+ }).then(function() {
+ return SpecialPowers.removeServiceWorkerDataForExampleDomain();
+ }).then(function() {
+ return checkDomainRegistration("prefixexample.com", true /* exists */)
+ .then(function(e) {
+ return checkDomainRegistration("example.com", false /* exists */);
+ }).then(function(e) {
+ SimpleTest.finish();
+ });
+ })
+ }
+
+ function testFrame(src) {
+ return new Promise(function(resolve, reject) {
+ var iframe = document.createElement("iframe");
+ iframe.src = src;
+ window.onmessage = function(message) {
+ window.onmessage = null;
+ iframe.src = "about:blank";
+ document.body.removeChild(iframe);
+ iframe = null;
+ SpecialPowers.exactGC(function() {
+ resolve(message.data);
+ });
+ };
+ document.body.appendChild(iframe);
+ });
+ }
+
+ function registerSW() {
+ return testFrame("http://example.com/tests/dom/serviceworkers/test/sanitize/register.html")
+ .then(function(e) {
+ // Register for prefixexample.com and then ensure it does not get unregistered.
+ return testFrame("http://prefixexample.com/tests/dom/serviceworkers/test/sanitize/register.html");
+ });
+ }
+
+ SimpleTest.waitForExplicitFinish();
+
+ SpecialPowers.pushPrefEnv({"set": [
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ ]}, function() {
+ start();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/test_scopes.html b/dom/serviceworkers/test/test_scopes.html
new file mode 100644
index 0000000000..77e997766d
--- /dev/null
+++ b/dom/serviceworkers/test/test_scopes.html
@@ -0,0 +1,143 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 984048 - Test scope glob matching.</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+<script class="testbody" type="text/javascript">
+
+ var scriptsAndScopes = [
+ [ "worker.js", "./sub/dir/"],
+ [ "worker.js", "./sub/dir" ],
+ [ "worker.js", "./sub/dir.html" ],
+ [ "worker.js", "./sub/dir/a" ],
+ [ "worker.js", "./sub" ],
+ [ "worker.js", "./star*" ], // '*' has no special meaning
+ ];
+
+ function registerWorkers() {
+ var registerArray = [];
+ scriptsAndScopes.forEach(function(item) {
+ registerArray.push(navigator.serviceWorker.register(item[0], { scope: item[1] }));
+ });
+
+ // Check register()'s step 4 which uses script's url with "./" as the scope if no scope is passed.
+ // The other tests already check step 5.
+ registerArray.push(navigator.serviceWorker.register("scope/scope_worker.js"));
+
+ // Check that SW cannot be registered for a scope "above" the script's location.
+ registerArray.push(new Promise(function(resolve, reject) {
+ navigator.serviceWorker.register("scope/scope_worker.js", { scope: "./" })
+ .then(function() {
+ ok(false, "registration scope has to be inside service worker script scope.");
+ reject();
+ }, function() {
+ ok(true, "registration scope has to be inside service worker script scope.");
+ resolve();
+ });
+ }));
+ return Promise.all(registerArray);
+ }
+
+ function unregisterWorkers() {
+ var unregisterArray = [];
+ scriptsAndScopes.forEach(function(item) {
+ var p = navigator.serviceWorker.getRegistration(item[1]);
+ unregisterArray.push(p.then(function(reg) {
+ return reg.unregister();
+ }));
+ });
+
+ unregisterArray.push(navigator.serviceWorker.getRegistration("scope/").then(function (reg) {
+ return reg.unregister();
+ }));
+
+ return Promise.all(unregisterArray);
+ }
+
+ async function testScopes() {
+ function chromeScriptSource() {
+ /* eslint-env mozilla/chrome-script */
+
+ let swm = Cc["@mozilla.org/serviceworkers/manager;1"]
+ .getService(Ci.nsIServiceWorkerManager);
+ let secMan = Cc["@mozilla.org/scriptsecuritymanager;1"]
+ .getService(Ci.nsIScriptSecurityManager);
+ addMessageListener("getScope", (msg) => {
+ let principal = secMan.createContentPrincipalFromOrigin(msg.principal);
+ try {
+ return { scope: swm.getScopeForUrl(principal, msg.path) };
+ } catch (e) {
+ return { exception: e.message };
+ }
+ });
+ }
+
+ let chromeScript = SpecialPowers.loadChromeScript(chromeScriptSource);
+ let docPrincipal = SpecialPowers.wrap(document).nodePrincipal.spec;
+
+ getScope = async (path) => {
+ let rv = await chromeScript.sendQuery("getScope", { principal: docPrincipal, path });
+ if (rv.exception)
+ throw rv.exception;
+ return rv.scope;
+ };
+
+ var base = new URL(".", document.baseURI);
+
+ function p(s) {
+ return base + s;
+ }
+
+ async function fail(fn) {
+ try {
+ await getScope(p("index.html"));
+ ok(false, "No registration");
+ } catch(e) {
+ ok(true, "No registration");
+ }
+ }
+
+ is(await getScope(p("sub.html")), p("sub"), "Scope should match");
+ is(await getScope(p("sub/dir.html")), p("sub/dir.html"), "Scope should match");
+ is(await getScope(p("sub/dir")), p("sub/dir"), "Scope should match");
+ is(await getScope(p("sub/dir/foo")), p("sub/dir/"), "Scope should match");
+ is(await getScope(p("sub/dir/afoo")), p("sub/dir/a"), "Scope should match");
+ is(await getScope(p("star*wars")), p("star*"), "Scope should match");
+ is(await getScope(p("scope/some_file.html")), p("scope/"), "Scope should match");
+ await fail("index.html");
+ await fail("sua.html");
+ await fail("star/a.html");
+ }
+
+ function runTest() {
+ registerWorkers()
+ .then(testScopes)
+ .then(unregisterWorkers)
+ .then(function() {
+ SimpleTest.finish();
+ }).catch(function(e) {
+ ok(false, "Some test failed with error " + e);
+ SimpleTest.finish();
+ });
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ SpecialPowers.pushPrefEnv({"set": [
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true]
+ ]}, runTest);
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/test_script_loader_intercepted_js_cache.html b/dom/serviceworkers/test/test_script_loader_intercepted_js_cache.html
new file mode 100644
index 0000000000..d0073705bb
--- /dev/null
+++ b/dom/serviceworkers/test/test_script_loader_intercepted_js_cache.html
@@ -0,0 +1,224 @@
+<!DOCTYPE html>
+<html>
+<!-- https://bugzilla.mozilla.org/show_bug.cgi?id=1350359 -->
+<!-- The JS bytecode cache is not supposed to be observable. To make it
+ observable, the ScriptLoader is instrumented to trigger events on the
+ script tag. These events are followed to reconstruct the code path taken by
+ the script loader and associate a simple name which is checked in these
+ test cases.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for saving and loading bytecode in/from the necko cache</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="utils.js"></script>
+ <script type="application/javascript">
+
+ // This is the state machine of the trace events produced by the
+ // ScriptLoader. This state machine is used to give a name to each
+ // code path, such that we can assert each code path with a single word.
+ var scriptLoaderStateMachine = {
+ "scriptloader_load_source": {
+ "scriptloader_execute": {
+ "scriptloader_encode": {
+ "scriptloader_bytecode_saved": "bytecode_saved",
+ "scriptloader_bytecode_failed": "bytecode_failed"
+ },
+ "scriptloader_no_encode": "source_exec"
+ }
+ },
+ "scriptloader_load_bytecode": {
+ "scriptloader_fallback": {
+ // Replicate the top-level state machine without
+ // "scriptloader_load_bytecode" transition.
+ "scriptloader_load_source": {
+ "scriptloader_execute": {
+ "scriptloader_encode": {
+ "scriptloader_bytecode_saved": "fallback_bytecode_saved",
+ "scriptloader_bytecode_failed": "fallback_bytecode_failed"
+ },
+ "scriptloader_no_encode": "fallback_source_exec"
+ }
+ }
+ },
+ "scriptloader_execute": "bytecode_exec"
+ }
+ };
+
+ var gScript = SpecialPowers.
+ loadChromeScript('http://mochi.test:8888/tests/dom/serviceworkers/test/file_js_cache_cleanup.js');
+
+ function WaitForScriptTagEvent(url) {
+ var iframe = document.createElement("iframe");
+ document.body.appendChild(iframe);
+
+ var stateMachine = scriptLoaderStateMachine;
+ var stateHistory = [];
+ var stateMachineResolve, stateMachineReject;
+ var statePromise = new Promise((resolve, reject) => {
+ stateMachineResolve = resolve;
+ stateMachineReject = reject;
+ });
+ var ping = 0;
+
+ // Walk the script loader state machine with the emitted events.
+ function log_event(evt) {
+ // If we have multiple script tags in the loaded source, make sure
+ // we only watch a single one.
+ if (evt.target.id != "watchme")
+ return;
+
+ dump("## ScriptLoader event: " + evt.type + "\n");
+ stateHistory.push(evt.type)
+ if (typeof stateMachine == "object")
+ stateMachine = stateMachine[evt.type];
+ if (typeof stateMachine == "string") {
+ // We arrived to a final state, report the name of it.
+ var result = stateMachine;
+ if (ping) {
+ result = `${result} & ping(=${ping})`;
+ }
+ stateMachineResolve(result);
+ } else if (stateMachine === undefined) {
+ // We followed an unknown transition, report the known history.
+ stateMachineReject(stateHistory);
+ }
+ }
+
+ var iwin = iframe.contentWindow;
+ iwin.addEventListener("scriptloader_load_source", log_event);
+ iwin.addEventListener("scriptloader_load_bytecode", log_event);
+ iwin.addEventListener("scriptloader_generate_bytecode", log_event);
+ iwin.addEventListener("scriptloader_execute", log_event);
+ iwin.addEventListener("scriptloader_encode", log_event);
+ iwin.addEventListener("scriptloader_no_encode", log_event);
+ iwin.addEventListener("scriptloader_bytecode_saved", log_event);
+ iwin.addEventListener("scriptloader_bytecode_failed", log_event);
+ iwin.addEventListener("scriptloader_fallback", log_event);
+ iwin.addEventListener("ping", (evt) => {
+ ping += 1;
+ dump(`## Content event: ${evt.type} (=${ping})\n`);
+ });
+ iframe.src = url;
+
+ statePromise.then(() => {
+ document.body.removeChild(iframe);
+ });
+ return statePromise;
+ }
+
+ promise_test(async function() {
+ // Setting dom.expose_test_interfaces pref causes the
+ // nsScriptLoadRequest to fire event on script tags, with information
+ // about its internal state. The ScriptLoader source send events to
+ // trace these and resolve a promise with the path taken by the
+ // script loader.
+ //
+ // Setting dom.script_loader.bytecode_cache.strategy to -1 causes the
+ // nsScriptLoadRequest to force all the conditions necessary to make a
+ // script be saved as bytecode in the alternate data storage provided
+ // by the channel (necko cache).
+ await SpecialPowers.pushPrefEnv({set: [
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ ['dom.script_loader.bytecode_cache.enabled', true],
+ ['dom.expose_test_interfaces', true],
+ ['dom.script_loader.bytecode_cache.strategy', -1]
+ ]});
+
+ // Register the service worker that perform the pass-through fetch.
+ var registration = await navigator.serviceWorker
+ .register("fetch.js", {scope: "./"});
+ let sw = registration.installing || registration.active;
+
+ // wait for service worker be activated
+ await waitForState(sw, 'activated');
+
+ await testCheckTheJSBytecodeCache();
+ await testSavebytecodeAfterTheInitializationOfThePage();
+ await testDoNotSaveBytecodeOnCompilationErrors();
+
+ await registration.unregister();
+ await teardown();
+ });
+
+ function teardown() {
+ return new Promise((resolve, reject) => {
+ gScript.addMessageListener("teardown-complete", function teardownCompleteHandler() {
+ gScript.removeMessageListener("teardown-complete", teardownCompleteHandler);
+ gScript.destroy();
+ resolve();
+ });
+ gScript.sendAsyncMessage("teardown");
+ });
+ }
+
+ async function testCheckTheJSBytecodeCache() {
+ dump("## Test: Check the JS bytecode cache\n");
+
+ // Load the test page, and verify that the code path taken by the
+ // nsScriptLoadRequest corresponds to the code path which is loading a
+ // source and saving it as bytecode.
+ var stateMachineResult = WaitForScriptTagEvent("file_js_cache.html");
+ assert_equals(await stateMachineResult, "bytecode_saved",
+ "[1] ScriptLoadRequest status after the first visit");
+
+ // Reload the same test page, and verify that the code path taken by
+ // the nsScriptLoadRequest corresponds to the code path which is
+ // loading bytecode and executing it.
+ stateMachineResult = WaitForScriptTagEvent("file_js_cache.html");
+ assert_equals(await stateMachineResult, "bytecode_exec",
+ "[2] ScriptLoadRequest status after the second visit");
+
+ // Load another page which loads the same script with an SRI, while
+ // the cached bytecode does not have any. This should fallback to
+ // loading the source before saving the bytecode once more.
+ stateMachineResult = WaitForScriptTagEvent("file_js_cache_with_sri.html");
+ assert_equals(await stateMachineResult, "fallback_bytecode_saved",
+ "[3] ScriptLoadRequest status after the SRI hash");
+
+ // Loading a page, which has the same SRI should verify the SRI and
+ // continue by executing the bytecode.
+ var stateMachineResult1 = WaitForScriptTagEvent("file_js_cache_with_sri.html");
+
+ // Loading a page which does not have a SRI while we have one in the
+ // cache should not change anything. We should also be able to load
+ // the cache simultanesouly.
+ var stateMachineResult2 = WaitForScriptTagEvent("file_js_cache.html");
+
+ assert_equals(await stateMachineResult1, "bytecode_exec",
+ "[4] ScriptLoadRequest status after same SRI hash");
+ assert_equals(await stateMachineResult2, "bytecode_exec",
+ "[5] ScriptLoadRequest status after visit with no SRI");
+ }
+
+ async function testSavebytecodeAfterTheInitializationOfThePage() {
+ dump("## Test: Save bytecode after the initialization of the page");
+
+ // The test page add a new script which generate a "ping" event, which
+ // should be recorded before the bytecode is stored in the cache.
+ var stateMachineResult =
+ WaitForScriptTagEvent("file_js_cache_save_after_load.html");
+ assert_equals(await stateMachineResult, "bytecode_saved & ping(=3)",
+ "Wait on all scripts to be executed");
+ }
+
+ async function testDoNotSaveBytecodeOnCompilationErrors() {
+ dump("## Test: Do not save bytecode on compilation errors");
+
+ // The test page loads a script which contains a syntax error, we should
+ // not attempt to encode any bytecode for it.
+ var stateMachineResult =
+ WaitForScriptTagEvent("file_js_cache_syntax_error.html");
+ assert_equals(await stateMachineResult, "source_exec",
+ "Check the lack of bytecode encoding");
+ }
+
+ done();
+ </script>
+</head>
+<body>
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1350359">Mozilla Bug 1350359</a>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/test_self_update_worker.html b/dom/serviceworkers/test/test_self_update_worker.html
new file mode 100644
index 0000000000..d6d4544dd9
--- /dev/null
+++ b/dom/serviceworkers/test/test_self_update_worker.html
@@ -0,0 +1,136 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+ Test that a self updating service worker can't keep running forever when the
+ script changes.
+
+ - self_update_worker.sjs is a stateful server-side js script that returns a
+ SW script with a different version every time it's invoked. (version=1..n)
+ - The SW script will trigger an update when it reaches the activating state,
+ which, if not for the update delaying mechanism, would result in an iterative
+ cycle.
+ - We currently delay registration.update() calls originating from SWs not currently
+ controlling any clients. The delay is: 0s, 30s, 900s etc, but for the purpose of
+ this test, the delay is: 0s, infinite etc.
+ - We assert that the SW script never reaches version 3, meaning it will only
+ successfully update once.
+ - We give the worker reasonable time to self update by repeatedly registering
+ and unregistering an empty service worker.
+ -->
+<head>
+ <title>Test for Bug 1432846</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="error_reporting_helpers.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <meta http-equiv="Content-type" content="text/html;charset=UTF-8">
+</head>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1432846">Mozilla Bug 1432846</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+
+<script src="utils.js"></script>
+<script class="testbody" type="text/javascript">
+add_task(function setupPrefs() {
+ return SpecialPowers.pushPrefEnv({"set": [
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ ]});
+});
+
+function activateDummyWorker() {
+ return navigator.serviceWorker.register("empty.js",
+ { scope: "./empty?random=" + Date.now() })
+ .then(function(registration) {
+ var worker = registration.installing;
+ return waitForState(worker, 'activated', registration).then(function() {
+ ok(true, "got dummy!");
+ return registration.unregister();
+ });
+ });
+}
+
+add_task(async function test_update() {
+ navigator.serviceWorker.onmessage = function(event) {
+ ok (event.data.version < 3, "Service worker updated too many times." + event.data.version);
+ }
+
+ await SpecialPowers.pushPrefEnv({"set": [
+ ["dom.serviceWorkers.update_delay", 30000],
+ ["dom.serviceWorkers.idle_extended_timeout", 299999]]});
+
+ // clear version counter
+ await fetch("self_update_worker.sjs?clearcounter");
+
+ var worker;
+ let registration = await navigator.serviceWorker.register(
+ "self_update_worker.sjs",
+ { scope: "./test_self_update_worker.html?random=" + Date.now()})
+ .then(function(reg) {
+ worker = reg.installing;
+ // We can't wait for 'activated' here, since it's possible for
+ // the update process to kill the worker before it activates.
+ // See: https://github.com/w3c/ServiceWorker/issues/1285
+ return waitForState(worker, 'activating', reg);
+ });
+
+ // We need to wait a reasonable time to give the self updating worker a chance
+ // to change to a newer version. Register and activate an empty worker 5 times.
+ for (i = 0; i < 5; i++) {
+ await activateDummyWorker();
+ }
+
+
+ await registration.unregister();
+ await SpecialPowers.popPrefEnv();
+ await SpecialPowers.popPrefEnv();
+});
+
+// Test variant to ensure that we properly keep the timer alive by having a
+// non-zero but small timer duration. In this case, the delay is simply our
+// exponential growth rate of 30, so if we end up getting to version 4, that's
+// okay and the test may need to be updated.
+add_task(async function test_delay_update() {
+ let version;
+ navigator.serviceWorker.onmessage = function(event) {
+ ok (event.data.version <= 3, "Service worker updated too many times." + event.data.version);
+ version = event.data.version;
+ }
+
+ await SpecialPowers.pushPrefEnv({"set": [
+ ["dom.serviceWorkers.update_delay", 1],
+ ["dom.serviceWorkers.idle_extended_timeout", 299999]]});
+
+ // clear version counter
+ await fetch("self_update_worker.sjs?clearcounter");
+
+ var worker;
+ let registration = await navigator.serviceWorker.register(
+ "self_update_worker.sjs",
+ { scope: "./test_self_update_worker.html?random=" + Date.now()})
+ .then(function(reg) {
+ worker = reg.installing;
+ // We can't wait for 'activated' here, since it's possible for
+ // the update process to kill the worker before it activates.
+ // See: https://github.com/w3c/ServiceWorker/issues/1285
+ return waitForState(worker, 'activating', reg);
+ });
+
+ // We need to wait a reasonable time to give the self updating worker a chance
+ // to change to a newer version. Register and activate an empty worker 5 times.
+ for (i = 0; i < 5; i++) {
+ await activateDummyWorker();
+ }
+
+ is(version, 3, "Service worker version should be 3.");
+
+ await registration.unregister();
+ await SpecialPowers.popPrefEnv();
+ await SpecialPowers.popPrefEnv();
+});
+</script>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/test_service_worker_allowed.html b/dom/serviceworkers/test/test_service_worker_allowed.html
new file mode 100644
index 0000000000..a74379f383
--- /dev/null
+++ b/dom/serviceworkers/test/test_service_worker_allowed.html
@@ -0,0 +1,74 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test the Service-Worker-Allowed header</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<div id="content"></div>
+<script class="testbody" type="text/javascript">
+ var gTests = [
+ "worker_scope_different.js",
+ "worker_scope_different2.js",
+ "worker_scope_too_deep.js",
+ ];
+
+ function testPermissiveHeader() {
+ // Make sure that this registration succeeds, as the prefix check should pass.
+ return navigator.serviceWorker.register("swa/worker_scope_too_narrow.js", {scope: "swa/"})
+ .then(swr => {
+ ok(true, "Registration should finish successfully");
+ return swr.unregister();
+ }, err => {
+ ok(false, "Unexpected error when registering the service worker: " + err);
+ });
+ }
+
+ function testPreciseHeader() {
+ // Make sure that this registration succeeds, as the prefix check should pass
+ // given that we parse the use the full pathname from this URL..
+ return navigator.serviceWorker.register("swa/worker_scope_precise.js", {scope: "swa/"})
+ .then(swr => {
+ ok(true, "Registration should finish successfully");
+ return swr.unregister();
+ }, err => {
+ ok(false, "Unexpected error when registering the service worker: " + err);
+ });
+ }
+
+ function runTest() {
+ Promise.all(gTests.map(testName => {
+ return new Promise((resolve, reject) => {
+ // Make sure that registration fails.
+ navigator.serviceWorker.register("swa/" + testName, {scope: "swa/"})
+ .then(reject, resolve);
+ });
+ })).then(values => {
+ values.forEach(error => {
+ is(error.name, "SecurityError", "Registration should fail");
+ });
+ Promise.all([
+ testPermissiveHeader(),
+ testPreciseHeader(),
+ ]).then(SimpleTest.finish, SimpleTest.finish);
+ }, (x) => {
+ ok(false, "Registration should not succeed, but it did");
+ SimpleTest.finish();
+ });
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ SpecialPowers.pushPrefEnv({"set": [
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ]}, runTest);
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/test_serviceworker.html b/dom/serviceworkers/test/test_serviceworker.html
new file mode 100644
index 0000000000..bfc5749405
--- /dev/null
+++ b/dom/serviceworkers/test/test_serviceworker.html
@@ -0,0 +1,79 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1137245 - Allow IndexedDB usage in ServiceWorkers</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+<script src="utils.js"></script>
+<script class="testbody" type="text/javascript">
+
+ var regisration;
+ function simpleRegister() {
+ return navigator.serviceWorker.register("service_worker.js", {
+ scope: 'service_worker_client.html'
+ }).then(swr => waitForState(swr.installing, 'activated', swr));
+ }
+
+ function unregister() {
+ return registration.unregister();
+ }
+
+ function testIndexedDBAvailable(sw) {
+ registration = sw;
+ var p = new Promise(function(resolve, reject) {
+ window.onmessage = function(e) {
+ if (e.data === "READY") {
+ sw.active.postMessage("GO");
+ return;
+ }
+
+ if (!("available" in e.data)) {
+ ok(false, "Something went wrong");
+ reject();
+ return;
+ }
+
+ ok(e.data.available, "IndexedDB available in service worker.");
+ resolve();
+ }
+ });
+
+ var content = document.getElementById("content");
+ ok(content, "Parent exists.");
+
+ iframe = document.createElement("iframe");
+ iframe.setAttribute('src', "service_worker_client.html");
+ content.appendChild(iframe);
+
+ return p.then(() => content.removeChild(iframe));
+ }
+
+ function runTest() {
+ simpleRegister()
+ .then(testIndexedDBAvailable)
+ .then(unregister)
+ .then(SimpleTest.finish)
+ .catch(function(e) {
+ ok(false, "Some test failed with error " + e);
+ SimpleTest.finish();
+ });
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ SpecialPowers.pushPrefEnv({"set": [
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true]
+ ]}, runTest);
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/test_serviceworker_header.html b/dom/serviceworkers/test/test_serviceworker_header.html
new file mode 100644
index 0000000000..f607aeba3d
--- /dev/null
+++ b/dom/serviceworkers/test/test_serviceworker_header.html
@@ -0,0 +1,41 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test that service worker scripts are fetched with a Service-Worker: script header</title>
+ <script type="text/javascript" src="http://mochi.test:8888/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="http://mochi.test:8888/tests/SimpleTest/test.css" />
+ <base href="https://mozilla.org/">
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test"></pre>
+<script class="testbody" type="text/javascript">
+
+ function runTest() {
+ navigator.serviceWorker.register("http://mochi.test:8888/tests/dom/serviceworkers/test/header_checker.sjs")
+ .then(reg => {
+ ok(true, "Register should succeed");
+ reg.unregister().then(() => SimpleTest.finish());
+ }, err => {
+ ok(false, "Register should not fail");
+ SimpleTest.finish();
+ });
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ onload = function() {
+ SpecialPowers.pushPrefEnv({"set": [
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ ["dom.serviceWorkers.enabled", true],
+ ]}, runTest);
+ };
+</script>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/test_serviceworker_interfaces.html b/dom/serviceworkers/test/test_serviceworker_interfaces.html
new file mode 100644
index 0000000000..3b4ec19134
--- /dev/null
+++ b/dom/serviceworkers/test/test_serviceworker_interfaces.html
@@ -0,0 +1,116 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Validate Interfaces Exposed to Service Workers</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="../worker_driver.js"></script>
+</head>
+<body>
+<script class="testbody" type="text/javascript">
+
+ function setupSW(registration) {
+ var iframe;
+ var worker = registration.installing ||
+ registration.waiting ||
+ registration.active;
+ window.onmessage = function(event) {
+ if (event.data.type == 'finish') {
+ iframe.remove();
+ registration.unregister().then(function(success) {
+ ok(success, "The service worker should be unregistered successfully");
+
+ SimpleTest.finish();
+ }, function(e) {
+ dump("Unregistering the SW failed with " + e + "\n");
+ SimpleTest.finish();
+ });
+ } else if (event.data.type == 'status') {
+ ok(event.data.status, event.data.msg);
+
+ } else if (event.data.type == 'getPrefs') {
+ let result = {};
+ event.data.prefs.forEach(function(pref) {
+ result[pref] = SpecialPowers.Services.prefs.getBoolPref(pref);
+ });
+ worker.postMessage({
+ type: 'returnPrefs',
+ prefs: event.data.prefs,
+ result
+ });
+
+ } else if (event.data.type == 'getHelperData') {
+ const { AppConstants } = SpecialPowers.ChromeUtils.import(
+ "resource://gre/modules/AppConstants.jsm"
+ );
+ const isNightly = AppConstants.NIGHTLY_BUILD;
+ const isEarlyBetaOrEarlier = AppConstants.EARLY_BETA_OR_EARLIER;
+ const isRelease = AppConstants.RELEASE_OR_BETA;
+ const isDesktop = !/Mobile|Tablet/.test(navigator.userAgent);
+ const isMac = AppConstants.platform == "macosx";
+ const isWindows = AppConstants.platform == "win";
+ const isAndroid = AppConstants.platform == "android";
+ const isLinux = AppConstants.platform == "linux";
+ const isInsecureContext = !window.isSecureContext;
+ // Currently, MOZ_APP_NAME is always "fennec" for all mobile builds, so we can't use AppConstants for this
+ const isFennec = isAndroid && SpecialPowers.Cc["@mozilla.org/android/bridge;1"].getService(SpecialPowers.Ci.nsIAndroidBridge).isFennec;
+
+ const result = {
+ isNightly, isEarlyBetaOrEarlier, isRelease, isDesktop, isMac,
+ isWindows, isAndroid, isLinux, isInsecureContext, isFennec
+ };
+
+ worker.postMessage({
+ type: 'returnHelperData', result
+ });
+ }
+ }
+
+ worker.onerror = function(event) {
+ ok(false, 'Worker had an error: ' + event.data);
+ SimpleTest.finish();
+ };
+
+ iframe = document.createElement("iframe");
+ iframe.src = "message_receiver.html";
+ iframe.onload = function() {
+ worker.postMessage({ script: "test_serviceworker_interfaces.js" });
+ };
+ document.body.appendChild(iframe);
+ }
+
+ function runTest() {
+ navigator.serviceWorker.register("serviceworker_wrapper.js", {scope: "."})
+ .then(setupSW);
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ onload = function() {
+ // The handling of "dom.caches.enabled" here is a bit complicated. What we
+ // want to happen is that Cache is always enabled in service workers. So
+ // if service workers are disabled by default we want to force on both
+ // service workers and "dom.caches.enabled". But if service workers are
+ // enabled by default, we do not want to mess with the "dom.caches.enabled"
+ // value, since that would defeat the purpose of the test. Use a subframe
+ // to decide whether service workers are enabled by default, so we don't
+ // force creation of our own Navigator object before our prefs are set.
+ var prefs = [
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ ];
+
+ var subframe = document.createElement("iframe");
+ document.body.appendChild(subframe);
+ if (!("serviceWorker" in subframe.contentWindow.navigator)) {
+ prefs.push(["dom.caches.enabled", true]);
+ }
+ subframe.remove();
+
+ SpecialPowers.pushPrefEnv({"set": prefs}, runTest);
+ };
+</script>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/test_serviceworker_interfaces.js b/dom/serviceworkers/test/test_serviceworker_interfaces.js
new file mode 100644
index 0000000000..379b3faec9
--- /dev/null
+++ b/dom/serviceworkers/test/test_serviceworker_interfaces.js
@@ -0,0 +1,549 @@
+// This is a list of all interfaces that are exposed to workers.
+// Please only add things to this list with great care and proper review
+// from the associated module peers.
+
+// This file lists global interfaces we want exposed and verifies they
+// are what we intend. Each entry in the arrays below can either be a
+// simple string with the interface name, or an object with a 'name'
+// property giving the interface name as a string, and additional
+// properties which qualify the exposure of that interface. For example:
+//
+// [
+// "AGlobalInterface",
+// { name: "ExperimentalThing", release: false },
+// { name: "ReallyExperimentalThing", nightly: true },
+// { name: "DesktopOnlyThing", desktop: true },
+// { name: "FancyControl", xbl: true },
+// { name: "DisabledEverywhere", disabled: true },
+// ];
+//
+// See createInterfaceMap() below for a complete list of properties.
+
+// IMPORTANT: Do not change this list without review from
+// a JavaScript Engine peer!
+let wasmGlobalEntry = {
+ name: "WebAssembly",
+ insecureContext: true,
+ disabled: !getJSTestingFunctions().wasmIsSupportedByHardware(),
+};
+let wasmGlobalInterfaces = [
+ { name: "Module", insecureContext: true },
+ { name: "Instance", insecureContext: true },
+ { name: "Memory", insecureContext: true },
+ { name: "Table", insecureContext: true },
+ { name: "Global", insecureContext: true },
+ { name: "CompileError", insecureContext: true },
+ { name: "LinkError", insecureContext: true },
+ { name: "RuntimeError", insecureContext: true },
+ { name: "Function", insecureContext: true, nightly: true },
+ { name: "Exception", insecureContext: true },
+ { name: "Tag", insecureContext: true },
+ { name: "compile", insecureContext: true },
+ { name: "compileStreaming", insecureContext: true },
+ { name: "instantiate", insecureContext: true },
+ { name: "instantiateStreaming", insecureContext: true },
+ { name: "validate", insecureContext: true },
+];
+// IMPORTANT: Do not change this list without review from
+// a JavaScript Engine peer!
+let ecmaGlobals = [
+ "AggregateError",
+ "Array",
+ "ArrayBuffer",
+ "Atomics",
+ "Boolean",
+ "BigInt",
+ "BigInt64Array",
+ "BigUint64Array",
+ "DataView",
+ "Date",
+ "Error",
+ "EvalError",
+ "FinalizationRegistry",
+ "Float32Array",
+ "Float64Array",
+ "Function",
+ "Infinity",
+ "Int16Array",
+ "Int32Array",
+ "Int8Array",
+ "InternalError",
+ "Intl",
+ "JSON",
+ "Map",
+ "Math",
+ "NaN",
+ "Number",
+ "Object",
+ "Promise",
+ "Proxy",
+ "RangeError",
+ "ReferenceError",
+ "Reflect",
+ "RegExp",
+ "Set",
+ {
+ name: "SharedArrayBuffer",
+ crossOriginIsolated: true,
+ },
+ "String",
+ "Symbol",
+ "SyntaxError",
+ "TypeError",
+ "Uint16Array",
+ "Uint32Array",
+ "Uint8Array",
+ "Uint8ClampedArray",
+ "URIError",
+ "WeakMap",
+ "WeakRef",
+ "WeakSet",
+ wasmGlobalEntry,
+ "decodeURI",
+ "decodeURIComponent",
+ "encodeURI",
+ "encodeURIComponent",
+ "escape",
+ "eval",
+ "globalThis",
+ "isFinite",
+ "isNaN",
+ "parseFloat",
+ "parseInt",
+ "undefined",
+ "unescape",
+];
+// IMPORTANT: Do not change the list above without review from
+// a JavaScript Engine peer!
+
+// IMPORTANT: Do not change the list below without review from a DOM peer!
+let interfaceNamesInGlobalScope = [
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "AbortController",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "AbortSignal",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "Blob",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "BroadcastChannel",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "ByteLengthQueuingStrategy",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "Cache",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "CacheStorage",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "CanvasGradient",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "CanvasPattern",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "Client",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "Clients",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "CloseEvent",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "CountQueuingStrategy",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "Crypto",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "CustomEvent",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "Directory",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "DOMException",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "DOMMatrix",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "DOMMatrixReadOnly",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "DOMPoint",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "DOMPointReadOnly",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "DOMQuad",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "DOMRect",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "DOMRectReadOnly",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "DOMRequest",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "DOMStringList",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "ErrorEvent",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "Event",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "EventTarget",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "ExtendableEvent",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "ExtendableMessageEvent",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "FetchEvent",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "File",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "FileList",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "FileReader",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ { name: "FileSystemDirectoryHandle", nightly: true },
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ { name: "FileSystemFileHandle", nightly: true },
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ { name: "FileSystemHandle", nightly: true },
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ { name: "FileSystemWritableFileStream", nightly: true },
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "FontFace",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "FontFaceSet",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "FontFaceSetLoadEvent",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "FormData",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "Headers",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "IDBCursor",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "IDBCursorWithValue",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "IDBDatabase",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "IDBFactory",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "IDBIndex",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "IDBKeyRange",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "IDBObjectStore",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "IDBOpenDBRequest",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "IDBRequest",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "IDBTransaction",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "IDBVersionChangeEvent",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "ImageBitmap",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "ImageBitmapRenderingContext",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "ImageData",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "Lock",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "LockManager",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "MediaCapabilities",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "MediaCapabilitiesInfo",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "MessageChannel",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "MessageEvent",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "MessagePort",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ { name: "NetworkInformation", disabled: true },
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "NavigationPreloadManager",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "Notification",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "NotificationEvent",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "OffscreenCanvas",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "OffscreenCanvasRenderingContext2D",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "Path2D",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "Performance",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "PerformanceEntry",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "PerformanceMark",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "PerformanceMeasure",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "PerformanceObserver",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "PerformanceObserverEntryList",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "PerformanceResourceTiming",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "PerformanceServerTiming",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "ProgressEvent",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "PromiseRejectionEvent",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ { name: "PushEvent" },
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ { name: "PushManager" },
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ { name: "PushMessageData" },
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ { name: "PushSubscription" },
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ { name: "PushSubscriptionOptions" },
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "ReadableByteStreamController",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "ReadableStream",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "ReadableStreamBYOBReader",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "ReadableStreamBYOBRequest",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "ReadableStreamDefaultController",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "ReadableStreamDefaultReader",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "Request",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "Response",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ { name: "Scheduler", nightly: true },
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "ServiceWorker",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "ServiceWorkerGlobalScope",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "ServiceWorkerRegistration",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ { name: "StorageManager", fennec: false },
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "SubtleCrypto",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ { name: "TaskController", nightly: true },
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ { name: "TaskPriorityChangeEvent", nightly: true },
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ { name: "TaskSignal", nightly: true },
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "TextDecoder",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "TextDecoderStream",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "TextEncoder",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "TextEncoderStream",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "TextMetrics",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "TransformStream",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "TransformStreamDefaultController",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "URL",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "URLSearchParams",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "WebSocket",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "WebGL2RenderingContext",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "WebGLActiveInfo",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "WebGLBuffer",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "WebGLContextEvent",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "WebGLFramebuffer",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "WebGLProgram",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "WebGLQuery",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "WebGLRenderbuffer",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "WebGLRenderingContext",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "WebGLSampler",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "WebGLShader",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "WebGLShaderPrecisionFormat",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "WebGLSync",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "WebGLTexture",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "WebGLTransformFeedback",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "WebGLUniformLocation",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "WebGLVertexArrayObject",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "WindowClient",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "WorkerGlobalScope",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "WorkerLocation",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "WorkerNavigator",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "WritableStream",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "WritableStreamDefaultController",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "WritableStreamDefaultWriter",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "clients",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "console",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "onactivate",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "onfetch",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "oninstall",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "onmessage",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "onmessageerror",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "onnotificationclick",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "onnotificationclose",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "onpush",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "onpushsubscriptionchange",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "registration",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+ "skipWaiting",
+ // IMPORTANT: Do not change this list without review from a DOM peer!
+];
+// IMPORTANT: Do not change the list above without review from a DOM peer!
+
+// List of functions defined on the global by the test harness or this test
+// file.
+let testFunctions = [
+ "ok",
+ "is",
+ "workerTestArrayEquals",
+ "workerTestDone",
+ "workerTestGetHelperData",
+ "workerTestGetStorageManager",
+ "entryDisabled",
+ "createInterfaceMap",
+ "runTest",
+];
+
+function entryDisabled(
+ entry,
+ {
+ isNightly,
+ isEarlyBetaOrEarlier,
+ isRelease,
+ isDesktop,
+ isAndroid,
+ isInsecureContext,
+ isFennec,
+ isCrossOriginIsolated,
+ }
+) {
+ return (
+ entry.nightly === !isNightly ||
+ (entry.nightlyAndroid === !(isAndroid && isNightly) && isAndroid) ||
+ (entry.nonReleaseAndroid === !(isAndroid && !isRelease) && isAndroid) ||
+ entry.desktop === !isDesktop ||
+ (entry.android === !isAndroid &&
+ !entry.nonReleaseAndroid &&
+ !entry.nightlyAndroid) ||
+ entry.fennecOrDesktop === (isAndroid && !isFennec) ||
+ entry.fennec === !isFennec ||
+ entry.release === !isRelease ||
+ entry.earlyBetaOrEarlier === !isEarlyBetaOrEarlier ||
+ entry.crossOriginIsolated === !isCrossOriginIsolated ||
+ entry.disabled
+ );
+}
+
+function createInterfaceMap(data, ...interfaceGroups) {
+ var interfaceMap = {};
+
+ function addInterfaces(interfaces) {
+ for (var entry of interfaces) {
+ if (typeof entry === "string") {
+ ok(!(entry in interfaceMap), "duplicate entry for " + entry);
+ interfaceMap[entry] = true;
+ } else {
+ ok(!(entry.name in interfaceMap), "duplicate entry for " + entry.name);
+ ok(!("pref" in entry), "Bogus pref annotation for " + entry.name);
+ if (entryDisabled(entry, data)) {
+ interfaceMap[entry.name] = false;
+ } else if (entry.optional) {
+ interfaceMap[entry.name] = "optional";
+ } else {
+ interfaceMap[entry.name] = true;
+ }
+ }
+ }
+ }
+
+ for (let interfaceGroup of interfaceGroups) {
+ addInterfaces(interfaceGroup);
+ }
+
+ return interfaceMap;
+}
+
+function runTest(parentName, parent, data, ...interfaceGroups) {
+ var interfaceMap = createInterfaceMap(data, ...interfaceGroups);
+ for (var name of Object.getOwnPropertyNames(parent)) {
+ // Ignore functions on the global that are part of the test (harness).
+ if (parent === self && testFunctions.includes(name)) {
+ continue;
+ }
+ ok(
+ interfaceMap[name] === "optional" || interfaceMap[name],
+ "If this is failing: DANGER, are you sure you want to expose the new interface " +
+ name +
+ " to all webpages as a property on " +
+ parentName +
+ "? Do not make a change to this file without a " +
+ " review from a DOM peer for that specific change!!! (or a JS peer for changes to ecmaGlobals)"
+ );
+ delete interfaceMap[name];
+ }
+ for (var name of Object.keys(interfaceMap)) {
+ if (interfaceMap[name] === "optional") {
+ delete interfaceMap[name];
+ } else {
+ ok(
+ name in parent === interfaceMap[name],
+ name +
+ " should " +
+ (interfaceMap[name] ? "" : " NOT") +
+ " be defined on " +
+ parentName
+ );
+ if (!interfaceMap[name]) {
+ delete interfaceMap[name];
+ }
+ }
+ }
+ is(
+ Object.keys(interfaceMap).length,
+ 0,
+ "The following interface(s) are not enumerated: " +
+ Object.keys(interfaceMap).join(", ")
+ );
+}
+
+workerTestGetHelperData(function(data) {
+ runTest("self", self, data, ecmaGlobals, interfaceNamesInGlobalScope);
+ if (WebAssembly && !entryDisabled(wasmGlobalEntry, data)) {
+ runTest("WebAssembly", WebAssembly, data, wasmGlobalInterfaces);
+ }
+ workerTestDone();
+});
diff --git a/dom/serviceworkers/test/test_serviceworker_not_sharedworker.html b/dom/serviceworkers/test/test_serviceworker_not_sharedworker.html
new file mode 100644
index 0000000000..33b4428e95
--- /dev/null
+++ b/dom/serviceworkers/test/test_serviceworker_not_sharedworker.html
@@ -0,0 +1,66 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1141274 - test that service workers and shared workers are separate</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+<iframe></iframe>
+</div>
+<pre id="test"></pre>
+<script class="testbody" type="text/javascript">
+
+ var iframe;
+ const SCOPE = "http://mochi.test:8888/tests/dom/serviceworkers/test/";
+ function runTest() {
+ navigator.serviceWorker.ready.then(setupSW);
+ navigator.serviceWorker.register("serviceworker_not_sharedworker.js",
+ {scope: SCOPE});
+ }
+
+ var sw, worker;
+ function setupSW(registration) {
+ sw = registration.waiting || registration.active;
+ worker = new SharedWorker("serviceworker_not_sharedworker.js", SCOPE);
+ worker.port.start();
+ iframe = document.querySelector("iframe");
+ iframe.src = "message_receiver.html";
+ iframe.onload = function() {
+ window.onmessage = function(e) {
+ is(e.data.result, "serviceworker", "We should be talking to a service worker");
+ window.onmessage = null;
+ worker.port.onmessage = function(msg) {
+ is(msg.data.result, "sharedworker", "We should be talking to a shared worker");
+ registration.unregister().then(function(success) {
+ ok(success, "unregister should succeed");
+ SimpleTest.finish();
+ }, function(ex) {
+ dump("Unregistering the SW failed with " + ex + "\n");
+ SimpleTest.finish();
+ });
+ };
+ worker.port.postMessage({msg: "whoareyou"});
+ };
+ sw.postMessage({msg: "whoareyou"});
+ };
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ onload = function() {
+ SpecialPowers.pushPrefEnv({"set": [
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true]
+ ]}, runTest);
+ };
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/test_serviceworkerinfo.xhtml b/dom/serviceworkers/test/test_serviceworkerinfo.xhtml
new file mode 100644
index 0000000000..a3b0fe8e53
--- /dev/null
+++ b/dom/serviceworkers/test/test_serviceworkerinfo.xhtml
@@ -0,0 +1,114 @@
+<?xml version="1.0"?>
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<window title="Test for ServiceWorkerInfo"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="test();">
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+ <script type="application/javascript" src="chrome_helpers.js"/>
+ <script type="application/javascript">
+ <![CDATA[
+
+ let IFRAME_URL = EXAMPLE_URL + "serviceworkerinfo_iframe.html";
+
+ function wait_for_active_worker(registration) {
+ ok(registration, "Registration is valid.");
+ return new Promise(function(res, rej) {
+ if (registration.activeWorker) {
+ res(registration);
+ return;
+ }
+ let listener = {
+ onChange: function() {
+ if (registration.activeWorker) {
+ registration.removeListener(listener);
+ res(registration);
+ }
+ }
+ }
+ registration.addListener(listener);
+ });
+ }
+
+ function test() {
+ SimpleTest.waitForExplicitFinish();
+
+ SpecialPowers.pushPrefEnv({'set': [
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.idle_extended_timeout", 1000000],
+ ["dom.serviceWorkers.idle_timeout", 0],
+ ["dom.serviceWorkers.testing.enabled", true],
+ ]}, function () {
+ (async function() {
+ let iframe = $("iframe");
+ let promise = new Promise(function (resolve) {
+ iframe.onload = function () {
+ resolve();
+ };
+ });
+ iframe.src = IFRAME_URL;
+ await promise;
+
+ info("Check that a service worker eventually shuts down.");
+ promise = Promise.all([
+ waitForRegister(EXAMPLE_URL),
+ waitForServiceWorkerShutdown()
+ ]);
+ iframe.contentWindow.postMessage("register", "*");
+ let [registration] = await promise;
+
+ // Make sure the worker is active.
+ registration = await wait_for_active_worker(registration);
+
+ let activeWorker = registration.activeWorker;
+ ok(activeWorker !== null, "Worker is not active!");
+ ok(activeWorker.debugger === null);
+
+ info("Attach a debugger to the service worker, and check that the " +
+ "service worker is restarted.");
+ activeWorker.attachDebugger();
+ let workerDebugger = activeWorker.debugger;
+ ok(workerDebugger !== null);
+
+ // Verify debugger properties
+ ok(workerDebugger.principal instanceof Ci.nsIPrincipal);
+ is(workerDebugger.url, EXAMPLE_URL + "worker.js");
+
+ info("Verify that getRegistrationByPrincipal return the same " +
+ "nsIServiceWorkerRegistrationInfo");
+ let reg = swm.getRegistrationByPrincipal(workerDebugger.principal,
+ workerDebugger.url);
+ is(reg, registration);
+
+ info("Check that getWorkerByID returns the same nsIWorkerDebugger");
+ is(activeWorker, reg.getWorkerByID(workerDebugger.serviceWorkerID));
+
+ info("Detach the debugger from the service worker, and check that " +
+ "the service worker eventually shuts down again.");
+ promise = waitForServiceWorkerShutdown();
+ activeWorker.detachDebugger();
+ await promise;
+ ok(activeWorker.debugger === null);
+
+ promise = waitForUnregister(EXAMPLE_URL);
+ iframe.contentWindow.postMessage("unregister", "*");
+ registration = await promise;
+
+ SimpleTest.finish();
+ })();
+ });
+ }
+
+ ]]>
+ </script>
+
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <p id="display"></p>
+ <div id="content" style="display:none;"></div>
+ <pre id="test"></pre>
+ <iframe id="iframe"></iframe>
+ </body>
+ <label id="test-result"/>
+</window>
diff --git a/dom/serviceworkers/test/test_serviceworkermanager.xhtml b/dom/serviceworkers/test/test_serviceworkermanager.xhtml
new file mode 100644
index 0000000000..b118b7d3f1
--- /dev/null
+++ b/dom/serviceworkers/test/test_serviceworkermanager.xhtml
@@ -0,0 +1,79 @@
+<?xml version="1.0"?>
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<window title="Test for ServiceWorkerManager"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="test();">
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+ <script type="application/javascript" src="chrome_helpers.js"/>
+ <script type="application/javascript">
+ <![CDATA[
+
+ let IFRAME_URL = EXAMPLE_URL + "serviceworkermanager_iframe.html";
+
+ function test() {
+ SimpleTest.waitForExplicitFinish();
+
+ SpecialPowers.pushPrefEnv({'set': [
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ ]}, function () {
+ (async function() {
+ let registrations = swm.getAllRegistrations();
+ is(registrations.length, 0);
+
+ let iframe = $("iframe");
+ let promise = waitForIframeLoad(iframe);
+ iframe.src = IFRAME_URL;
+ await promise;
+
+ info("Check that the service worker manager notifies its listeners " +
+ "when a service worker is registered.");
+ promise = waitForRegister(EXAMPLE_URL);
+ iframe.contentWindow.postMessage("register", "*");
+ let registration = await promise;
+
+ registrations = swm.getAllRegistrations();
+ is(registrations.length, 1);
+ is(registrations.queryElementAt(0, Ci.nsIServiceWorkerRegistrationInfo),
+ registration);
+
+ info("Check that the service worker manager does not notify its " +
+ "listeners when a service worker is registered with the same " +
+ "scope as an existing registration.");
+ let listener = {
+ onRegister: function () {
+ ok(false, "Listener should not have been notified.");
+ }
+ };
+ swm.addListener(listener);
+ iframe.contentWindow.postMessage("register", "*");
+
+ info("Check that the service worker manager notifies its listeners " +
+ "when a service worker is unregistered.");
+ promise = waitForUnregister(EXAMPLE_URL);
+ iframe.contentWindow.postMessage("unregister", "*");
+ registration = await promise;
+ swm.removeListener(listener);
+
+ registrations = swm.getAllRegistrations();
+ is(registrations.length, 0);
+
+ SimpleTest.finish();
+ })();
+ });
+ }
+
+ ]]>
+ </script>
+
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <p id="display"></p>
+ <div id="content" style="display:none;"></div>
+ <pre id="test"></pre>
+ <iframe id="iframe"></iframe>
+ </body>
+ <label id="test-result"/>
+</window>
diff --git a/dom/serviceworkers/test/test_serviceworkerregistrationinfo.xhtml b/dom/serviceworkers/test/test_serviceworkerregistrationinfo.xhtml
new file mode 100644
index 0000000000..5b39350897
--- /dev/null
+++ b/dom/serviceworkers/test/test_serviceworkerregistrationinfo.xhtml
@@ -0,0 +1,155 @@
+<?xml version="1.0"?>
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<window title="Test for ServiceWorkerRegistrationInfo"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="test();">
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+ <script type="application/javascript" src="chrome_helpers.js"/>
+ <script type="application/javascript">
+ <![CDATA[
+
+ let IFRAME_URL = EXAMPLE_URL + "serviceworkerregistrationinfo_iframe.html";
+
+ function test() {
+ SimpleTest.waitForExplicitFinish();
+
+ SpecialPowers.pushPrefEnv({'set': [
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ ]}, function () {
+ (async function() {
+ let iframe = $("iframe");
+ let promise = waitForIframeLoad(iframe);
+ iframe.src = IFRAME_URL;
+ await promise;
+
+ // The change handler is not guaranteed to be called within the same
+ // tick of the event loop as the one in which the change happened.
+ // Because of this, the exact state of the service worker registration
+ // is only known until the handler returns.
+ //
+ // Because then-handlers are resolved asynchronously, the following
+ // checks are done using callbacks, which are called synchronously
+ // when then handler is called. These callbacks can return a promise,
+ // which is used to resolve the promise returned by the function.
+
+ info("Check that a service worker registration notifies its " +
+ "listeners when its state changes.");
+ promise = waitForRegister(EXAMPLE_URL, function (registration) {
+ is(registration.scriptSpec, "");
+ ok(registration.installingWorker === null);
+ ok(registration.waitingWorker === null);
+ ok(registration.activeWorker === null);
+
+ return waitForServiceWorkerRegistrationChange(registration, function () {
+ // Got change event for updating (byte-check)
+ ok(registration.installingWorker === null);
+ ok(registration.waitingWorker === null);
+ ok(registration.activeWorker === null);
+
+ return waitForServiceWorkerRegistrationChange(registration, function () {
+ is(registration.scriptSpec, EXAMPLE_URL + "worker.js");
+ ok(registration.evaluatingWorker !== null);
+ ok(registration.installingWorker === null);
+ ok(registration.waitingWorker === null);
+ ok(registration.activeWorker === null);
+
+ return waitForServiceWorkerRegistrationChange(registration, function () {
+ ok(registration.installingWorker !== null);
+ is(registration.installingWorker.scriptSpec, EXAMPLE_URL + "worker.js");
+ ok(registration.waitingWorker === null);
+ ok(registration.activeWorker === null);
+
+ return waitForServiceWorkerRegistrationChange(registration, function () {
+ ok(registration.installingWorker === null);
+ ok(registration.waitingWorker !== null);
+ ok(registration.activeWorker === null);
+
+ return waitForServiceWorkerRegistrationChange(registration, function () {
+ // Activating
+ ok(registration.installingWorker === null);
+ ok(registration.waitingWorker === null);
+ ok(registration.activeWorker !== null);
+
+ return waitForServiceWorkerRegistrationChange(registration, function () {
+ // Activated
+ ok(registration.installingWorker === null);
+ ok(registration.waitingWorker === null);
+ ok(registration.activeWorker !== null);
+
+ return registration;
+ });
+ });
+ });
+ });
+ });
+ });
+ });
+ iframe.contentWindow.postMessage("register", "*");
+ let registration = await promise;
+
+ promise = waitForServiceWorkerRegistrationChange(registration, function () {
+ // Got change event for updating (byte-check)
+ ok(registration.installingWorker === null);
+ ok(registration.waitingWorker === null);
+ ok(registration.activeWorker !== null);
+
+ return waitForServiceWorkerRegistrationChange(registration, function () {
+ is(registration.scriptSpec, EXAMPLE_URL + "worker2.js");
+ ok(registration.evaluatingWorker !== null);
+
+ return waitForServiceWorkerRegistrationChange(registration, function () {
+ ok(registration.installingWorker !== null);
+ is(registration.installingWorker.scriptSpec, EXAMPLE_URL + "worker2.js");
+ ok(registration.waitingWorker === null);
+ ok(registration.activeWorker !== null);
+
+ return waitForServiceWorkerRegistrationChange(registration, function () {
+ ok(registration.installingWorker === null);
+ ok(registration.waitingWorker !== null);
+ ok(registration.activeWorker !== null);
+
+ return waitForServiceWorkerRegistrationChange(registration, function () {
+ // Activating
+ ok(registration.installingWorker === null);
+ ok(registration.waitingWorker === null);
+ ok(registration.activeWorker !== null);
+
+ return waitForServiceWorkerRegistrationChange(registration, function () {
+ // Activated
+ ok(registration.installingWorker === null);
+ ok(registration.waitingWorker === null);
+ ok(registration.activeWorker !== null);
+
+ return registration;
+ });
+ });
+ });
+ });
+ });
+ });
+ iframe.contentWindow.postMessage("register", "*");
+ await promise;
+
+ iframe.contentWindow.postMessage("unregister", "*");
+ await waitForUnregister(EXAMPLE_URL);
+
+ SimpleTest.finish();
+ })();
+ });
+ }
+
+ ]]>
+ </script>
+
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <p id="display"></p>
+ <div id="content" style="display:none;"></div>
+ <pre id="test"></pre>
+ <iframe id="iframe"></iframe>
+ </body>
+ <label id="test-result"/>
+</window>
diff --git a/dom/serviceworkers/test/test_skip_waiting.html b/dom/serviceworkers/test/test_skip_waiting.html
new file mode 100644
index 0000000000..6147ad6b38
--- /dev/null
+++ b/dom/serviceworkers/test/test_skip_waiting.html
@@ -0,0 +1,86 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1131352 - Add ServiceWorkerGlobalScope skipWaiting()</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+<script src="utils.js"></script>
+<script class="testbody" type="text/javascript">
+ var registration, iframe, content;
+
+ function start() {
+ return navigator.serviceWorker.register("worker.js",
+ {scope: "./skip_waiting_scope/"});
+ }
+
+ async function waitForActivated(swr) {
+ registration = swr;
+ await waitForState(registration.installing, "activated")
+
+ iframe = document.createElement("iframe");
+ iframe.setAttribute("src", "skip_waiting_scope/index.html");
+
+ content = document.getElementById("content");
+ content.appendChild(iframe);
+
+ await new Promise(resolve => iframe.onload = resolve);
+ }
+
+ function checkWhetherItSkippedWaiting() {
+ var promise = new Promise(function(resolve, reject) {
+ window.onmessage = function (evt) {
+ if (evt.data.event === "controllerchange") {
+ ok(evt.data.controllerScriptURL.match("skip_waiting_installed_worker"),
+ "The controller changed after skiping the waiting step");
+ resolve();
+ } else {
+ ok(false, "Wrong value. Somenting went wrong");
+ resolve();
+ }
+ };
+ });
+
+ navigator.serviceWorker.register("skip_waiting_installed_worker.js",
+ {scope: "./skip_waiting_scope/"})
+ .then(swr => {
+ registration = swr;
+ });
+
+ return promise;
+ }
+
+ function clean() {
+ content.removeChild(iframe);
+
+ return registration.unregister();
+ }
+
+ function runTest() {
+ start()
+ .then(waitForActivated)
+ .then(checkWhetherItSkippedWaiting)
+ .then(clean)
+ .catch(function(e) {
+ ok(false, "Some test failed with error " + e);
+ }).then(SimpleTest.finish);
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ SpecialPowers.pushPrefEnv({"set": [
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true]
+ ]}, runTest);
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/test_streamfilter.html b/dom/serviceworkers/test/test_streamfilter.html
new file mode 100644
index 0000000000..7367fb8b84
--- /dev/null
+++ b/dom/serviceworkers/test/test_streamfilter.html
@@ -0,0 +1,207 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>
+ Test StreamFilter-monitored responses for ServiceWorker-intercepted requests
+ </title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<script>
+// eslint-disable-next-line mozilla/no-addtask-setup
+add_task(async function setup() {
+ SimpleTest.waitForExplicitFinish();
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ ],
+ });
+
+ const registration = await navigator.serviceWorker.register(
+ "streamfilter_worker.js"
+ );
+
+ SimpleTest.registerCleanupFunction(async function unregisterRegistration() {
+ await registration.unregister();
+ });
+
+ await new Promise(resolve => {
+ const serviceWorker = registration.installing;
+
+ serviceWorker.onstatechange = () => {
+ if (serviceWorker.state == "activated") {
+ resolve();
+ }
+ };
+ });
+
+ ok(navigator.serviceWorker.controller, "Page is controlled");
+});
+
+async function getExtension() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["webRequest", "webRequestBlocking", "<all_urls>"],
+ },
+
+ // This WebExtension only proxies a response's data through a StreamFilter;
+ // it doesn't modify the data itself in any way.
+ background() {
+ class FilterWrapper {
+ constructor(requestId) {
+ const filter = browser.webRequest.filterResponseData(requestId);
+ const arrayBuffers = [];
+
+ filter.onstart = () => {
+ browser.test.sendMessage("start");
+ };
+
+ filter.ondata = ({ data }) => {
+ arrayBuffers.push(data);
+ };
+
+ filter.onstop = () => {
+ browser.test.sendMessage("stop");
+ new Blob(arrayBuffers).arrayBuffer().then(buffer => {
+ filter.write(buffer);
+ filter.close();
+ });
+ };
+
+ filter.onerror = () => {
+ // We only ever expect a redirect error here.
+ browser.test.assertEq(filter.error, "ServiceWorker fallback redirection");
+ browser.test.sendMessage("error");
+ };
+ }
+ }
+
+ browser.webRequest.onBeforeRequest.addListener(
+ details => {
+ new FilterWrapper(details.requestId);
+ },
+ {
+ urls: ["<all_urls>"],
+ types: ["xmlhttprequest"],
+ },
+ ["blocking"]
+ );
+ },
+ });
+
+ await extension.startup();
+ return extension;
+}
+
+const streamFilterServerUrl = `${location.origin}/tests/dom/serviceworkers/test/streamfilter_server.sjs`;
+
+const requestUrlForServerQueryString = "syntheticResponse=0";
+
+// streamfilter_server.sjs is expected to respond to a request to this URL.
+const requestUrlForServer = `${streamFilterServerUrl}?${requestUrlForServerQueryString}`;
+
+const requestUrlForServiceWorkerQueryString = "syntheticResponse=1";
+
+// streamfilter_worker.js is expected to respond to a request to this URL.
+const requestUrlForServiceWorker = `${streamFilterServerUrl}?${requestUrlForServiceWorkerQueryString}`;
+
+// startNetworkerRequestFn must be a function that, when called, starts a
+// network request and returns a promise that resolves after the request
+// completes (or fails). This function will return the value that that promise
+// resolves with (or throw if it rejects).
+async function observeFilteredNetworkRequest(startNetworkRequestFn, promises) {
+ const networkRequestPromise = startNetworkRequestFn();
+ await Promise.all(promises);
+ return networkRequestPromise;
+}
+
+// Returns a promise that resolves with the XHR's response text.
+function callXHR(requestUrl, promises) {
+ return observeFilteredNetworkRequest(() => {
+ return new Promise((resolve, reject) => {
+ const xhr = new XMLHttpRequest();
+ xhr.onload = () => {
+ resolve(xhr.responseText);
+ };
+ xhr.onerror = reject;
+ xhr.open("GET", requestUrl);
+ xhr.send();
+ });
+ }, promises);
+}
+
+// Returns a promise that resolves with the Fetch's response text.
+function callFetch(requestUrl, promises) {
+ return observeFilteredNetworkRequest(() => {
+ return fetch(requestUrl).then(response => response.text());
+ }, promises);
+}
+
+// The expected response text is always the query string (without the leading
+// "?") of the request URL.
+add_task(async function callXhrExpectServerResponse() {
+ info(`Performing XHR at ${requestUrlForServer}...`);
+ let extension = await getExtension();
+ is(
+ await callXHR(requestUrlForServer, [
+ extension.awaitMessage("start"),
+ extension.awaitMessage("error"),
+ extension.awaitMessage("stop"),
+ ]),
+ requestUrlForServerQueryString,
+ "Server-supplied response for XHR completed successfully"
+ );
+ await extension.unload();
+});
+
+add_task(async function callXhrExpectServiceWorkerResponse() {
+ info(`Performing XHR at ${requestUrlForServiceWorker}...`);
+ let extension = await getExtension();
+ is(
+ await callXHR(requestUrlForServiceWorker, [
+ extension.awaitMessage("start"),
+ extension.awaitMessage("stop"),
+ ]),
+ requestUrlForServiceWorkerQueryString,
+ "ServiceWorker-supplied response for XHR completed successfully"
+ );
+ await extension.unload();
+});
+
+add_task(async function callFetchExpectServerResponse() {
+ info(`Performing Fetch at ${requestUrlForServer}...`);
+ let extension = await getExtension();
+ is(
+ await callFetch(requestUrlForServer, [
+ extension.awaitMessage("start"),
+ extension.awaitMessage("error"),
+ extension.awaitMessage("stop"),
+ ]),
+ requestUrlForServerQueryString,
+ "Server-supplied response for Fetch completed successfully"
+ );
+ await extension.unload();
+});
+
+add_task(async function callFetchExpectServiceWorkerResponse() {
+ info(`Performing Fetch at ${requestUrlForServiceWorker}...`);
+ let extension = await getExtension();
+ is(
+ await callFetch(requestUrlForServiceWorker, [
+ extension.awaitMessage("start"),
+ extension.awaitMessage("stop"),
+ ]),
+ requestUrlForServiceWorkerQueryString,
+ "ServiceWorker-supplied response for Fetch completed successfully"
+ );
+ await extension.unload();
+});
+</script>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/test_strict_mode_warning.html b/dom/serviceworkers/test/test_strict_mode_warning.html
new file mode 100644
index 0000000000..4df0d1a380
--- /dev/null
+++ b/dom/serviceworkers/test/test_strict_mode_warning.html
@@ -0,0 +1,42 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1170550 - test registration of service worker scripts with a strict mode warning</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test"></pre>
+<script class="testbody" type="text/javascript">
+
+ function runTest() {
+ navigator.serviceWorker
+ .register("strict_mode_warning.js", {scope: "strict_mode_warning"})
+ .then((reg) => {
+ ok(true, "Registration should not fail for warnings");
+ return reg.unregister();
+ })
+ .then(() => {
+ SimpleTest.finish();
+ });
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ onload = function() {
+ SpecialPowers.pushPrefEnv({"set": [
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ ]}, runTest);
+ };
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/test_third_party_iframes.html b/dom/serviceworkers/test/test_third_party_iframes.html
new file mode 100644
index 0000000000..90e9dadfa8
--- /dev/null
+++ b/dom/serviceworkers/test/test_third_party_iframes.html
@@ -0,0 +1,263 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta http-equiv="Content-type" content="text/html;charset=UTF-8">
+ <title>Bug 1152899 - Disallow the interception of third-party iframes using service workers when the third-party cookie preference is set</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<script class="testbody" type="text/javascript">
+
+var chromeScript;
+chromeScript = SpecialPowers.loadChromeScript(_ => {
+ /* eslint-env mozilla/chrome-script */
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => resolve());
+});
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.requestLongerTimeout(2);
+
+let index = 0;
+function next() {
+ info("Step " + index);
+ if (index >= steps.length) {
+ SimpleTest.finish();
+ return;
+ }
+ try {
+ let i = index++;
+ steps[i]();
+ } catch(ex) {
+ ok(false, "Caught exception", ex);
+ }
+}
+
+onload = next;
+
+let iframe;
+let proxyWindow;
+let basePath = "/tests/dom/serviceworkers/test/thirdparty/";
+let origin = window.location.protocol + "//" + window.location.host;
+let thirdPartyOrigin = "https://example.com";
+
+function loadIframe() {
+ let message = {
+ source: "parent",
+ href: origin + basePath + "iframe2.html"
+ };
+ iframe.contentWindow.postMessage(message, "*");
+}
+
+function loadThirdPartyIframe() {
+ let message = {
+ source: "parent",
+ href: thirdPartyOrigin + basePath + "iframe2.html"
+ }
+ iframe.contentWindow.postMessage(message, "*");
+}
+
+function runTest(aExpectedResponses) {
+ // Let's use a proxy window to have the new cookie policy applied.
+ proxyWindow = window.open("window_party_iframes.html");
+ proxyWindow.onload = _ => {
+ iframe = proxyWindow.document.querySelector("iframe");
+ iframe.src = thirdPartyOrigin + basePath + "register.html";
+ let responsesIndex = 0;
+ window.onmessage = function(e) {
+ let status = e.data.status;
+ let expected = aExpectedResponses[responsesIndex];
+ if (status == expected.status) {
+ ok(true, "Received expected " + expected.status);
+ if (expected.next) {
+ expected.next();
+ }
+ } else {
+ ok(false, "Expected " + expected.status + " got " + status);
+ }
+ responsesIndex++;
+ };
+ }
+}
+
+// Verify that we can register and intercept a 3rd party iframe with
+// the given cookie policy.
+function testShouldIntercept(behavior, done) {
+ SpecialPowers.pushPrefEnv({"set": [
+ ["network.cookie.cookieBehavior", behavior],
+ ]}, function() {
+ runTest([{
+ status: "ok"
+ }, {
+ status: "registrationdone",
+ next() {
+ iframe.src = origin + basePath + "iframe1.html";
+ }
+ }, {
+ status: "iframeloaded",
+ next: loadIframe
+ }, {
+ status: "networkresponse",
+ }, {
+ status: "worker-networkresponse",
+ next: loadThirdPartyIframe
+ }, {
+ status: "swresponse",
+ }, {
+ status: "worker-swresponse",
+ next() {
+ iframe.src = thirdPartyOrigin + basePath + "unregister.html";
+ }
+ }, {
+ status: "controlled",
+ }, {
+ status: "unregistrationdone",
+ next() {
+ window.onmessage = null;
+ proxyWindow.close();
+ ok(true, "Test finished successfully");
+ done();
+ }
+ }]);
+ });
+}
+
+// Verify that we cannot register a service worker in a 3rd party
+// iframe with the given cookie policy.
+function testShouldNotRegister(behavior, done) {
+ SpecialPowers.pushPrefEnv({"set": [
+ ["network.cookie.cookieBehavior", behavior],
+ ]}, function() {
+ runTest([{
+ status: "registrationfailed",
+ next() {
+ iframe.src = origin + basePath + "iframe1.html";
+ }
+ }, {
+ status: "iframeloaded",
+ next: loadIframe
+ }, {
+ status: "networkresponse",
+ }, {
+ status: "worker-networkresponse",
+ next: loadThirdPartyIframe
+ }, {
+ status: "networkresponse",
+ }, {
+ status: "worker-networkresponse",
+ next() {
+ window.onmessage = null;
+ proxyWindow.close();
+ ok(true, "Test finished successfully");
+ done();
+ }
+ }]);
+ });
+}
+
+// Verify that if a service worker is already registered a 3rd
+// party iframe will still not be intercepted with the given cookie
+// policy.
+function testShouldNotIntercept(behavior, done) {
+ SpecialPowers.pushPrefEnv({"set": [
+ ["network.cookie.cookieBehavior", BEHAVIOR_ACCEPT],
+ ]}, function() {
+ runTest([{
+ status: "ok"
+ }, {
+ status: "registrationdone",
+ next() {
+ SpecialPowers.pushPrefEnv({"set": [
+ ["network.cookie.cookieBehavior", behavior],
+ ]}, function() {
+ proxyWindow.close();
+ proxyWindow = window.open("window_party_iframes.html");
+ proxyWindow.onload = _ => {
+ iframe = proxyWindow.document.querySelector("iframe");
+ iframe.src = origin + basePath + "iframe1.html";
+ }
+ });
+ }
+ }, {
+ status: "iframeloaded",
+ next: loadIframe
+ }, {
+ status: "networkresponse",
+ }, {
+ status: "worker-networkresponse",
+ next: loadThirdPartyIframe
+ }, {
+ status: "networkresponse",
+ }, {
+ status: "worker-networkresponse",
+ next() {
+ iframe.src = thirdPartyOrigin + basePath + "unregister.html";
+ }
+ }, {
+ status: "uncontrolled",
+ }, {
+ status: "getregistrationfailed",
+ next() {
+ SpecialPowers.pushPrefEnv({"set": [
+ ["network.cookie.cookieBehavior", BEHAVIOR_ACCEPT],
+ ]}, function() {
+ proxyWindow.close();
+ proxyWindow = window.open("window_party_iframes.html");
+ proxyWindow.onload = _ => {
+ iframe = proxyWindow.document.querySelector("iframe");
+ iframe.src = thirdPartyOrigin + basePath + "unregister.html";
+ }
+ });
+ }
+ }, {
+ status: "controlled",
+ }, {
+ status: "unregistrationdone",
+ next() {
+ window.onmessage = null;
+ proxyWindow.close();
+ ok(true, "Test finished successfully");
+ done();
+ }
+ }]);
+ });
+}
+
+const BEHAVIOR_ACCEPT = 0;
+const BEHAVIOR_REJECTFOREIGN = 1;
+const BEHAVIOR_REJECT = 2;
+const BEHAVIOR_LIMITFOREIGN = 3;
+
+let steps = [() => {
+ SpecialPowers.pushPrefEnv({"set": [
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ ["browser.dom.window.dump.enabled", true],
+ ["network.cookie.cookieBehavior", BEHAVIOR_ACCEPT],
+ ]}, next);
+}, () => {
+ testShouldNotRegister(BEHAVIOR_REJECTFOREIGN, next);
+}, () => {
+ testShouldNotIntercept(BEHAVIOR_REJECTFOREIGN, next);
+}, () => {
+ testShouldNotRegister(BEHAVIOR_REJECT, next);
+}, () => {
+ testShouldNotIntercept(BEHAVIOR_REJECT, next);
+}, () => {
+ testShouldNotRegister(BEHAVIOR_LIMITFOREIGN, next);
+}, () => {
+ testShouldNotIntercept(BEHAVIOR_LIMITFOREIGN, next);
+}, () => {
+ testShouldIntercept(BEHAVIOR_ACCEPT, next);
+}];
+
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/test_unregister.html b/dom/serviceworkers/test/test_unregister.html
new file mode 100644
index 0000000000..959378f03a
--- /dev/null
+++ b/dom/serviceworkers/test/test_unregister.html
@@ -0,0 +1,137 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 984048 - Test unregister</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+<script class="testbody" type="text/javascript">
+
+ function simpleRegister() {
+ return navigator.serviceWorker.register("worker.js", { scope: "unregister/" }).then(function(swr) {
+ if (swr.installing) {
+ return new Promise(function(resolve, reject) {
+ swr.installing.onstatechange = function(e) {
+ if (swr.waiting) {
+ swr.waiting.onstatechange = function(event) {
+ if (swr.active) {
+ resolve();
+ } else if (swr.waiting && swr.waiting.state == "redundant") {
+ reject("Should not go into redundant");
+ }
+ }
+ } else {
+ if (swr.active) {
+ resolve();
+ } else {
+ reject("No waiting and no active!");
+ }
+ }
+ }
+ });
+ } else {
+ return Promise.reject("Installing should be non-null");
+ }
+ });
+ }
+
+ function testControlled() {
+ var testPromise = new Promise(function(res, rej) {
+ window.onmessage = function(e) {
+ if (!("controlled" in e.data)) {
+ ok(false, "Something went wrong.");
+ rej();
+ return;
+ }
+
+ ok(e.data.controlled, "New window should be controlled.");
+ res();
+ }
+ })
+
+ var div = document.getElementById("content");
+ ok(div, "Parent exists");
+
+ var ifr = document.createElement("iframe");
+ ifr.setAttribute('src', "unregister/index.html");
+ div.appendChild(ifr);
+
+ return testPromise.then(function() {
+ div.removeChild(ifr);
+ });
+ }
+
+ function unregister() {
+ return navigator.serviceWorker.getRegistration("unregister/")
+ .then(function(reg) {
+ if (!reg) {
+ info("Registration already removed");
+ return;
+ }
+
+ info("getRegistration() succeeded " + reg.scope);
+ return reg.unregister().then(function(v) {
+ ok(v, "Unregister should resolve to true");
+ }, function(e) {
+ ok(false, "Unregister failed with " + e.name);
+ });
+ });
+ }
+
+ function testUncontrolled() {
+ var testPromise = new Promise(function(res, rej) {
+ window.onmessage = function(e) {
+ if (!("controlled" in e.data)) {
+ ok(false, "Something went wrong.");
+ rej();
+ return;
+ }
+
+ ok(!e.data.controlled, "New window should not be controlled.");
+ res();
+ }
+ });
+
+ var div = document.getElementById("content");
+ ok(div, "Parent exists");
+
+ var ifr = document.createElement("iframe");
+ ifr.setAttribute('src', "unregister/index.html");
+ div.appendChild(ifr);
+
+ return testPromise.then(function() {
+ div.removeChild(ifr);
+ });
+ }
+
+ function runTest() {
+ simpleRegister()
+ .then(testControlled)
+ .then(unregister)
+ .then(testUncontrolled)
+ .then(function() {
+ SimpleTest.finish();
+ }).catch(function(e) {
+ ok(false, "Some test failed with error " + e);
+ SimpleTest.finish();
+ });
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ SpecialPowers.pushPrefEnv({"set": [
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true]
+ ]}, runTest);
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/test_unresolved_fetch_interception.html b/dom/serviceworkers/test/test_unresolved_fetch_interception.html
new file mode 100644
index 0000000000..7182b0fb86
--- /dev/null
+++ b/dom/serviceworkers/test/test_unresolved_fetch_interception.html
@@ -0,0 +1,95 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+ Test that an unresolved respondWith promise will reset the channel when
+ the service worker is terminated due to idling, and that appropriate error
+ messages are logged for both the termination of the serice worker and the
+ resetting of the channel.
+ -->
+<head>
+ <title>Test for Bug 1188545</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="error_reporting_helpers.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <meta http-equiv="Content-type" content="text/html;charset=UTF-8">
+</head>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1188545">Mozilla Bug 118845</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+
+<script src="utils.js"></script>
+<script class="testbody" type="text/javascript">
+// (This doesn't really need to be its own task, but it allows the actual test
+// case to be self-contained.)
+add_task(function setupPrefs() {
+ return SpecialPowers.pushPrefEnv({"set": [
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ ]});
+});
+
+add_task(async function grace_timeout_termination_with_interrupted_intercept() {
+ // Setup timeouts so that the service worker will go into grace timeout after
+ // a zero-length idle timeout.
+ await SpecialPowers.pushPrefEnv({"set": [
+ ["dom.serviceWorkers.idle_timeout", 0],
+ ["dom.serviceWorkers.idle_extended_timeout", 299999]]});
+
+ let registration = await navigator.serviceWorker.register(
+ "unresolved_fetch_worker.js", { scope: "./"} );
+ await waitForState(registration.installing, "activated");
+ ok(navigator.serviceWorker.controller, "Controlled"); // double check!
+
+ // We want to make sure the SW is active and processing the fetch before we
+ // try and kill it. It sends us a message when it has done so.
+ let waitForFetchActive = new Promise((resolve) => {
+ navigator.serviceWorker.onmessage = resolve;
+ });
+
+ // Issue a fetch which the SW will respondWith() a never resolved promise.
+ // The fetch, however, will terminate when the SW is killed, so check that.
+ let hangingFetch = fetch("does_not_exist.html")
+ .then(() => { ok(false, "should have rejected "); },
+ () => { ok(true, "hung fetch terminates when worker dies"); });
+
+ await waitForFetchActive;
+
+ let expectedMessage = expect_console_message(
+ // Termination error
+ "ServiceWorkerGraceTimeoutTermination",
+ [make_absolute_url("./")],
+ // The interception failure error generated by the RespondWithHandler
+ // destructor when it notices it didn't get a response before being
+ // destroyed. It logs via the intercepted channel nsIConsoleReportCollector
+ // that is eventually flushed to our document and its console.
+ "InterceptionFailedWithURL",
+ [make_absolute_url("does_not_exist.html")]
+ );
+
+ // Zero out the grace timeout too so the worker will get terminated after two
+ // zero-length timer firings. Note that we need to do something to get the
+ // SW to renew its keepalive for this to actually cause the timers to be
+ // rescheduled...
+ await SpecialPowers.pushPrefEnv({"set": [
+ ["dom.serviceWorkers.idle_extended_timeout", 0]]});
+ // ...which we do by postMessaging it.
+ navigator.serviceWorker.controller.postMessage("doomity doom doom");
+
+ // Now wait for signs that the worker was terminated by the fetch failing.
+ await hangingFetch;
+
+ // The worker should now be dead and the error logged, wait/assert.
+ await wait_for_expected_message(expectedMessage);
+
+ // roll back all of our test case specific preferences and otherwise cleanup
+ await SpecialPowers.popPrefEnv();
+ await SpecialPowers.popPrefEnv();
+ await registration.unregister();
+});
+</script>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/test_workerUnregister.html b/dom/serviceworkers/test/test_workerUnregister.html
new file mode 100644
index 0000000000..d0bc1d6ce4
--- /dev/null
+++ b/dom/serviceworkers/test/test_workerUnregister.html
@@ -0,0 +1,81 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 982728 - Test ServiceWorkerGlobalScope.unregister</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<div id="container"></div>
+<script class="testbody" type="text/javascript">
+
+ function simpleRegister() {
+ return navigator.serviceWorker.register("worker_unregister.js", { scope: "unregister/" }).then(function(swr) {
+ if (swr.installing) {
+ return new Promise(function(resolve, reject) {
+ swr.installing.onstatechange = function(e) {
+ if (swr.waiting) {
+ swr.waiting.onstatechange = function(event) {
+ if (swr.active) {
+ resolve();
+ } else if (swr.waiting && swr.waiting.state == "redundant") {
+ reject("Should not go into redundant");
+ }
+ }
+ } else {
+ if (swr.active) {
+ resolve();
+ } else {
+ reject("No waiting and no active!");
+ }
+ }
+ }
+ });
+ } else {
+ return Promise.reject("Installing should be non-null");
+ }
+ });
+ }
+
+ function waitForMessages(sw) {
+ var p = new Promise(function(resolve, reject) {
+ window.onmessage = function(e) {
+ if (e.data === "DONE") {
+ ok(true, "The worker has unregistered itself");
+ } else if (e.data === "ERROR") {
+ ok(false, "The worker has unregistered itself");
+ } else if (e.data === "FINISH") {
+ resolve();
+ }
+ }
+ });
+
+ var frame = document.createElement("iframe");
+ frame.setAttribute("src", "unregister/unregister.html");
+ document.body.appendChild(frame);
+
+ return p;
+ }
+
+ function runTest() {
+ simpleRegister().then(waitForMessages).catch(function(e) {
+ ok(false, "Something went wrong.");
+ }).then(function() {
+ SimpleTest.finish();
+ });
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ SpecialPowers.pushPrefEnv({"set": [
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true]
+ ]}, runTest);
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/test_workerUpdate.html b/dom/serviceworkers/test/test_workerUpdate.html
new file mode 100644
index 0000000000..015e6bb4ae
--- /dev/null
+++ b/dom/serviceworkers/test/test_workerUpdate.html
@@ -0,0 +1,63 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1065366 - Test ServiceWorkerGlobalScope.update</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<div id="container"></div>
+<script src="utils.js"></script>
+<script class="testbody" type="text/javascript">
+
+ function simpleRegister() {
+ return navigator.serviceWorker.register("worker_update.js", { scope: "workerUpdate/" })
+ .then(swr => waitForState(swr.installing, 'activated', swr));
+ }
+
+ var registration;
+ function waitForMessages(sw) {
+ registration = sw;
+ var p = new Promise(function(resolve, reject) {
+ window.onmessage = function(e) {
+ if (e.data === "FINISH") {
+ ok(true, "The worker has updated itself");
+ resolve();
+ } else if (e.data === "FAIL") {
+ ok(false, "The worker failed to update itself");
+ resolve();
+ }
+ }
+ });
+
+ var frame = document.createElement("iframe");
+ frame.setAttribute("src", "workerUpdate/update.html");
+ document.body.appendChild(frame);
+
+ return p;
+ }
+
+ function runTest() {
+ simpleRegister().then(waitForMessages).catch(function(e) {
+ ok(false, "Something went wrong.");
+ }).then(function() {
+ return registration.unregister();
+ }).then(function() {
+ SimpleTest.finish();
+ });
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ SpecialPowers.pushPrefEnv({"set": [
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true]
+ ]}, runTest);
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/test_worker_reference_gc_timeout.html b/dom/serviceworkers/test/test_worker_reference_gc_timeout.html
new file mode 100644
index 0000000000..cf04e13f2e
--- /dev/null
+++ b/dom/serviceworkers/test/test_worker_reference_gc_timeout.html
@@ -0,0 +1,76 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+ -->
+<head>
+ <title>Test for Bug 1317266</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="error_reporting_helpers.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <meta http-equiv="Content-type" content="text/html;charset=UTF-8">
+</head>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1317266">Mozilla Bug 1317266</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+
+<script class="testbody" type="text/javascript">
+SimpleTest.requestFlakyTimeout("Forcing a race with the cycle collector.");
+
+add_task(function setupPrefs() {
+ return SpecialPowers.pushPrefEnv({"set": [
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ ]});
+});
+
+add_task(async function test_worker_ref_gc() {
+ let registration = await navigator.serviceWorker.register(
+ "lazy_worker.js", { scope: "./lazy_worker_scope_timeout"} )
+ .then(function(reg) {
+ SpecialPowers.exactGC();
+ var worker = reg.installing;
+ return new Promise(function(resolve) {
+ worker.addEventListener('statechange', function() {
+ info("state is " + worker.state + "\n");
+ SpecialPowers.exactGC();
+ if (worker.state === 'activated') {
+ resolve(reg);
+ }
+ });
+ });
+ });
+ ok(true, "Got activated event!");
+
+ await registration.unregister();
+});
+
+add_task(async function test_worker_ref_gc_ready_promise() {
+ let wait_active = navigator.serviceWorker.ready.then(function(reg) {
+ SpecialPowers.exactGC();
+ ok(reg.active, "Got active worker.");
+ ok(reg.active.state === "activating", "Worker is in activating state");
+ return new Promise(function(res) {
+ reg.active.onstatechange = function(e) {
+ reg.active.onstatechange = null;
+ ok(reg.active.state === "activated", "Worker was activated");
+ res();
+ }
+ });
+ });
+
+ let registration = await navigator.serviceWorker.register(
+ "lazy_worker.js", { scope: "."} );
+ await wait_active;
+ await registration.unregister();
+});
+
+add_task(async function cleanup() {
+ await SpecialPowers.popPrefEnv();
+});
+</script>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/test_workerupdatefoundevent.html b/dom/serviceworkers/test/test_workerupdatefoundevent.html
new file mode 100644
index 0000000000..1c3ced13bd
--- /dev/null
+++ b/dom/serviceworkers/test/test_workerupdatefoundevent.html
@@ -0,0 +1,91 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1131327 - Test ServiceWorkerRegistration.onupdatefound on ServiceWorker</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content"></div>
+<pre id="test"></pre>
+<script src="utils.js"></script>
+<script class="testbody" type="text/javascript">
+ var registration;
+ var promise;
+
+ async function start() {
+ registration = await navigator.serviceWorker.register("worker_updatefoundevent.js",
+ { scope: "./updatefoundevent.html" })
+ await waitForState(registration.installing, 'activated');
+
+ content = document.getElementById("content");
+ iframe = document.createElement("iframe");
+ content.appendChild(iframe);
+ iframe.setAttribute("src", "./updatefoundevent.html");
+
+ await new Promise(function(resolve) { iframe.onload = resolve; });
+ ok(iframe.contentWindow.navigator.serviceWorker.controller, "Controlled client.");
+
+ return Promise.resolve();
+
+ }
+
+ function startWaitForUpdateFound() {
+ registration.onupdatefound = function(e) {
+ }
+
+ promise = new Promise(function(resolve, reject) {
+ window.onmessage = function(e) {
+
+ if (e.data == "finish") {
+ ok(true, "Received updatefound");
+ resolve();
+ }
+ }
+ });
+
+ return Promise.resolve();
+ }
+
+ function registerNext() {
+ return navigator.serviceWorker.register("worker_updatefoundevent2.js",
+ { scope: "./updatefoundevent.html" });
+ }
+
+ function waitForUpdateFound() {
+ return promise;
+ }
+
+ function unregister() {
+ window.onmessage = null;
+ return registration.unregister().then(function(result) {
+ ok(result, "Unregister should return true.");
+ });
+ }
+
+ function runTest() {
+ start()
+ .then(startWaitForUpdateFound)
+ .then(registerNext)
+ .then(waitForUpdateFound)
+ .then(unregister)
+ .catch(function(e) {
+ ok(false, "Some test failed with error " + e);
+ }).then(SimpleTest.finish);
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ SpecialPowers.pushPrefEnv({"set": [
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ ]}, runTest);
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/test_xslt.html b/dom/serviceworkers/test/test_xslt.html
new file mode 100644
index 0000000000..a955c843ac
--- /dev/null
+++ b/dom/serviceworkers/test/test_xslt.html
@@ -0,0 +1,117 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1182113 - Test service worker XSLT interception</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content"></div>
+<pre id="test"></pre>
+<script src="utils.js"></script>
+<script class="testbody" type="text/javascript">
+ var registration;
+ var worker;
+
+ function start() {
+ return navigator.serviceWorker.register("xslt_worker.js",
+ { scope: "./" })
+ .then((swr) => {
+ registration = swr;
+
+ // Ensure the registration is active before continuing
+ return waitForState(swr.installing, 'activated');
+ });
+ }
+
+ function unregister() {
+ return registration.unregister().then(function(result) {
+ ok(result, "Unregister should return true.");
+ }, function(e) {
+ dump("Unregistering the SW failed with " + e + "\n");
+ });
+ }
+
+ function getXmlString(xmlObject) {
+ serializer = new XMLSerializer();
+ return serializer.serializeToString(iframe.contentDocument);
+ }
+
+ function synthetic() {
+ content = document.getElementById("content");
+ ok(content, "parent exists.");
+
+ iframe = document.createElement("iframe");
+ content.appendChild(iframe);
+
+ iframe.setAttribute('src', "xslt/test.xml");
+
+ var p = new Promise(function(res, rej) {
+ iframe.onload = function(e) {
+ dump("Set request mode\n");
+ registration.active.postMessage("synthetic");
+ xmlString = getXmlString(iframe.contentDocument);
+ ok(!xmlString.includes("Error"), "Load synthetic cross origin XSLT should be allowed");
+ res();
+ };
+ });
+
+ return p;
+ }
+
+ function cors() {
+ var p = new Promise(function(res, rej) {
+ iframe.onload = function(e) {
+ xmlString = getXmlString(iframe.contentDocument);
+ ok(!xmlString.includes("Error"), "Load CORS cross origin XSLT should be allowed");
+ res();
+ };
+ });
+
+ registration.active.postMessage("cors");
+ iframe.setAttribute('src', "xslt/test.xml");
+
+ return p;
+ }
+
+ function opaque() {
+ var p = new Promise(function(res, rej) {
+ iframe.onload = function(e) {
+ xmlString = getXmlString(iframe.contentDocument);
+ ok(xmlString.includes("Error"), "Load opaque cross origin XSLT should not be allowed");
+ res();
+ };
+ });
+
+ registration.active.postMessage("opaque");
+ iframe.setAttribute('src', "xslt/test.xml");
+
+ return p;
+ }
+
+ function runTest() {
+ start()
+ .then(synthetic)
+ .then(opaque)
+ .then(cors)
+ .then(unregister)
+ .catch(function(e) {
+ ok(false, "Some test failed with error " + e);
+ }).then(SimpleTest.finish);
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ SpecialPowers.pushPrefEnv({"set": [
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ ]}, runTest);
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/thirdparty/iframe1.html b/dom/serviceworkers/test/thirdparty/iframe1.html
new file mode 100644
index 0000000000..e8982d306a
--- /dev/null
+++ b/dom/serviceworkers/test/thirdparty/iframe1.html
@@ -0,0 +1,42 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<html>
+<head>
+ <meta http-equiv="Content-type" content="text/html;charset=UTF-8">
+ <title>SW third party iframe test</title>
+
+ <script type="text/javascript">
+ function messageListener(event) {
+ let message = event.data;
+
+ dump("got message " + JSON.stringify(message) + "\n");
+ if (message.source == "parent") {
+ document.getElementById("iframe2").src = message.href;
+ }
+ else if (message.source == "iframe") {
+ parent.postMessage(event.data, "*");
+ } else if (message.source == "worker") {
+ parent.postMessage(event.data, "*");
+ }
+ }
+ </script>
+
+</head>
+
+<body>
+ <script>
+ onload = function() {
+ window.addEventListener('message', messageListener);
+ let message = {
+ source: "iframe",
+ status: "iframeloaded",
+ }
+ parent.postMessage(message, "*");
+ }
+ </script>
+ <iframe id="iframe2"></iframe>
+</body>
+
+</html>
diff --git a/dom/serviceworkers/test/thirdparty/iframe2.html b/dom/serviceworkers/test/thirdparty/iframe2.html
new file mode 100644
index 0000000000..8013899195
--- /dev/null
+++ b/dom/serviceworkers/test/thirdparty/iframe2.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<script>
+ window.parent.postMessage({
+ source: "iframe",
+ status: "networkresponse"
+ }, "*");
+ var w = new Worker('worker.js');
+ w.onmessage = function(evt) {
+ window.parent.postMessage({
+ source: 'worker',
+ status: evt.data,
+ }, '*');
+ };
+</script>
diff --git a/dom/serviceworkers/test/thirdparty/register.html b/dom/serviceworkers/test/thirdparty/register.html
new file mode 100644
index 0000000000..b166acb8a4
--- /dev/null
+++ b/dom/serviceworkers/test/thirdparty/register.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<script>
+ function ok(v, msg) {
+ window.parent.postMessage({status: "ok", result: !!v, message: msg}, "*");
+ }
+
+ var isDone = false;
+ function done(reg) {
+ if (!isDone) {
+ ok(reg.waiting || reg.active,
+ "Either active or waiting worker should be available.");
+ window.parent.postMessage({status: "registrationdone"}, "*");
+ isDone = true;
+ }
+ }
+
+ navigator.serviceWorker.register("sw.js", {scope: "."})
+ .then(function(registration) {
+ if (registration.installing) {
+ registration.installing.onstatechange = function(e) {
+ done(registration);
+ };
+ } else {
+ done(registration);
+ }
+ }).catch(function(e) {
+ window.parent.postMessage({status: "registrationfailed"}, "*");
+ });
+</script>
diff --git a/dom/serviceworkers/test/thirdparty/sw.js b/dom/serviceworkers/test/thirdparty/sw.js
new file mode 100644
index 0000000000..ea844e8f2b
--- /dev/null
+++ b/dom/serviceworkers/test/thirdparty/sw.js
@@ -0,0 +1,33 @@
+self.addEventListener("fetch", function(event) {
+ dump("fetch " + event.request.url + "\n");
+ if (event.request.url.includes("iframe2.html")) {
+ var body =
+ "<script>" +
+ "window.parent.postMessage({" +
+ "source: 'iframe', status: 'swresponse'" +
+ "}, '*');" +
+ "var w = new Worker('worker.js');" +
+ "w.onmessage = function(evt) {" +
+ "window.parent.postMessage({" +
+ "source: 'worker'," +
+ "status: evt.data," +
+ "}, '*');" +
+ "};" +
+ "</script>";
+ event.respondWith(
+ new Response(body, {
+ headers: { "Content-Type": "text/html" },
+ })
+ );
+ return;
+ }
+ if (event.request.url.includes("worker.js")) {
+ var body = "self.postMessage('worker-swresponse');";
+ event.respondWith(
+ new Response(body, {
+ headers: { "Content-Type": "application/javascript" },
+ })
+ );
+ return;
+ }
+});
diff --git a/dom/serviceworkers/test/thirdparty/unregister.html b/dom/serviceworkers/test/thirdparty/unregister.html
new file mode 100644
index 0000000000..65b29d5648
--- /dev/null
+++ b/dom/serviceworkers/test/thirdparty/unregister.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<script>
+ if (navigator.serviceWorker.controller) {
+ window.parent.postMessage({status: "controlled"}, "*");
+ } else {
+ window.parent.postMessage({status: "uncontrolled"}, "*");
+ }
+
+ navigator.serviceWorker.getRegistration(".").then(function(registration) {
+ if(!registration) {
+ return;
+ }
+ registration.unregister().then(() => {
+ window.parent.postMessage({status: "unregistrationdone"}, "*");
+ });
+ }).catch(function(e) {
+ window.parent.postMessage({status: "getregistrationfailed"}, "*");
+ });
+</script>
diff --git a/dom/serviceworkers/test/thirdparty/worker.js b/dom/serviceworkers/test/thirdparty/worker.js
new file mode 100644
index 0000000000..bbdc608cde
--- /dev/null
+++ b/dom/serviceworkers/test/thirdparty/worker.js
@@ -0,0 +1 @@
+self.postMessage("worker-networkresponse");
diff --git a/dom/serviceworkers/test/unregister/index.html b/dom/serviceworkers/test/unregister/index.html
new file mode 100644
index 0000000000..36cac9fcf6
--- /dev/null
+++ b/dom/serviceworkers/test/unregister/index.html
@@ -0,0 +1,26 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 984048 - Test unregister</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+<script class="testbody" type="text/javascript">
+
+ if (!parent) {
+ info("unregister/index.html should not to be launched directly!");
+ }
+
+ parent.postMessage({ controlled: !!navigator.serviceWorker.controller }, "*");
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/unregister/unregister.html b/dom/serviceworkers/test/unregister/unregister.html
new file mode 100644
index 0000000000..42633ca343
--- /dev/null
+++ b/dom/serviceworkers/test/unregister/unregister.html
@@ -0,0 +1,21 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test worker::unregister</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<script type="text/javascript">
+
+ navigator.serviceWorker.onmessage = function(e) { parent.postMessage(e.data, "*"); }
+ navigator.serviceWorker.controller.postMessage("GO");
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/unresolved_fetch_worker.js b/dom/serviceworkers/test/unresolved_fetch_worker.js
new file mode 100644
index 0000000000..80d39b037a
--- /dev/null
+++ b/dom/serviceworkers/test/unresolved_fetch_worker.js
@@ -0,0 +1,18 @@
+var keepPromiseAlive;
+onfetch = function(event) {
+ event.waitUntil(
+ clients.matchAll().then(clients => {
+ clients.forEach(client => {
+ client.postMessage("continue");
+ });
+ })
+ );
+
+ // Never resolve, and keep it alive on our global so it can't get GC'ed and
+ // make this test weird and intermittent.
+ event.respondWith((keepPromiseAlive = new Promise(function(res, rej) {})));
+};
+
+addEventListener("activate", function(event) {
+ event.waitUntil(clients.claim());
+});
diff --git a/dom/serviceworkers/test/update_worker.sjs b/dom/serviceworkers/test/update_worker.sjs
new file mode 100644
index 0000000000..44782a2732
--- /dev/null
+++ b/dom/serviceworkers/test/update_worker.sjs
@@ -0,0 +1,12 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+function handleRequest(request, response) {
+ // This header is necessary for making this script able to be loaded.
+ response.setHeader("Content-Type", "application/javascript");
+
+ var body = "/* " + Date.now() + " */";
+ response.write(body);
+}
diff --git a/dom/serviceworkers/test/updatefoundevent.html b/dom/serviceworkers/test/updatefoundevent.html
new file mode 100644
index 0000000000..78088c7cd0
--- /dev/null
+++ b/dom/serviceworkers/test/updatefoundevent.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Bug 1131327 - Test ServiceWorkerRegistration.onupdatefound on ServiceWorker</title>
+</head>
+<body>
+<script>
+ navigator.serviceWorker.onmessage = function(e) {
+ dump("NSM iframe got message " + e.data + "\n");
+ window.parent.postMessage(e.data, "*");
+ };
+</script>
+</body>
diff --git a/dom/serviceworkers/test/utils.js b/dom/serviceworkers/test/utils.js
new file mode 100644
index 0000000000..28be239593
--- /dev/null
+++ b/dom/serviceworkers/test/utils.js
@@ -0,0 +1,136 @@
+function waitForState(worker, state, context) {
+ return new Promise(resolve => {
+ function onStateChange() {
+ if (worker.state === state) {
+ worker.removeEventListener("statechange", onStateChange);
+ resolve(context);
+ }
+ }
+
+ // First add an event listener, so we won't miss any change that happens
+ // before we check the current state.
+ worker.addEventListener("statechange", onStateChange);
+
+ // Now check if the worker is already in the desired state.
+ onStateChange();
+ });
+}
+
+/**
+ * Helper for browser tests to issue register calls from the content global and
+ * wait for the SW to progress to the active state, as most tests desire.
+ * From the ContentTask.spawn, use via
+ * `content.wrappedJSObject.registerAndWaitForActive`.
+ */
+async function registerAndWaitForActive(script, maybeScope) {
+ console.log("...calling register");
+ let opts = undefined;
+ if (maybeScope) {
+ opts = { scope: maybeScope };
+ }
+ const reg = await navigator.serviceWorker.register(script, opts);
+ // Unless registration resurrection happens, the SW should be in the
+ // installing slot.
+ console.log("...waiting for activation");
+ await waitForState(reg.installing, "activated", reg);
+ console.log("...activated!");
+ return reg;
+}
+
+/**
+ * Helper to create an iframe with the given URL and return the first
+ * postMessage payload received. This is intended to be used when creating
+ * cross-origin iframes.
+ *
+ * A promise will be returned that resolves with the payload of the postMessage
+ * call.
+ */
+function createIframeAndWaitForMessage(url) {
+ const iframe = document.createElement("iframe");
+ document.body.appendChild(iframe);
+ return new Promise(resolve => {
+ window.addEventListener(
+ "message",
+ event => {
+ resolve(event.data);
+ },
+ { once: true }
+ );
+ iframe.src = url;
+ });
+}
+
+/**
+ * Helper to create a nested iframe into the iframe created by
+ * createIframeAndWaitForMessage().
+ *
+ * A promise will be returned that resolves with the payload of the postMessage
+ * call.
+ */
+function createNestedIframeAndWaitForMessage(url) {
+ const iframe = document.getElementsByTagName("iframe")[0];
+ iframe.contentWindow.postMessage("create nested iframe", "*");
+ return new Promise(resolve => {
+ window.addEventListener(
+ "message",
+ event => {
+ resolve(event.data);
+ },
+ { once: true }
+ );
+ });
+}
+
+async function unregisterAll() {
+ const registrations = await navigator.serviceWorker.getRegistrations();
+ for (const reg of registrations) {
+ await reg.unregister();
+ }
+}
+
+/**
+ * Make a blob that contains random data and therefore shouldn't compress all
+ * that well.
+ */
+function makeRandomBlob(size) {
+ const arr = new Uint8Array(size);
+ let offset = 0;
+ /**
+ * getRandomValues will only provide a maximum of 64k of data at a time and
+ * will error if we ask for more, so using a while loop for get a random value
+ * which much larger than 64k.
+ * https://developer.mozilla.org/en-US/docs/Web/API/Crypto/getRandomValues#exceptions
+ */
+ while (offset < size) {
+ const nextSize = Math.min(size - offset, 65536);
+ window.crypto.getRandomValues(new Uint8Array(arr.buffer, offset, nextSize));
+ offset += nextSize;
+ }
+ return new Blob([arr], { type: "application/octet-stream" });
+}
+
+async function fillStorage(cacheBytes, idbBytes) {
+ // ## Fill Cache API Storage
+ const cache = await caches.open("filler");
+ await cache.put("fill", new Response(makeRandomBlob(cacheBytes)));
+
+ // ## Fill IDB
+ const storeName = "filler";
+ let db = await new Promise((resolve, reject) => {
+ let openReq = indexedDB.open("filler", 1);
+ openReq.onerror = event => {
+ reject(event.target.error);
+ };
+ openReq.onsuccess = event => {
+ resolve(event.target.result);
+ };
+ openReq.onupgradeneeded = event => {
+ const useDB = event.target.result;
+ useDB.onerror = error => {
+ reject(error);
+ };
+ const store = useDB.createObjectStore(storeName);
+ store.put({ blob: makeRandomBlob(idbBytes) }, "filler-blob");
+ };
+ });
+}
diff --git a/dom/serviceworkers/test/window_party_iframes.html b/dom/serviceworkers/test/window_party_iframes.html
new file mode 100644
index 0000000000..abeea4449b
--- /dev/null
+++ b/dom/serviceworkers/test/window_party_iframes.html
@@ -0,0 +1,18 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta http-equiv="Content-type" content="text/html;charset=UTF-8">
+</head>
+<body>
+<iframe></iframe>
+<script>
+window.onmessage = e => {
+ opener.postMessage(e.data, "*");
+}
+</script>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/worker.js b/dom/serviceworkers/test/worker.js
new file mode 100644
index 0000000000..2aba167d18
--- /dev/null
+++ b/dom/serviceworkers/test/worker.js
@@ -0,0 +1 @@
+// empty worker, always succeed!
diff --git a/dom/serviceworkers/test/worker2.js b/dom/serviceworkers/test/worker2.js
new file mode 100644
index 0000000000..3072d0817f
--- /dev/null
+++ b/dom/serviceworkers/test/worker2.js
@@ -0,0 +1 @@
+// worker2.js
diff --git a/dom/serviceworkers/test/worker3.js b/dom/serviceworkers/test/worker3.js
new file mode 100644
index 0000000000..449fc2f976
--- /dev/null
+++ b/dom/serviceworkers/test/worker3.js
@@ -0,0 +1 @@
+// worker3.js
diff --git a/dom/serviceworkers/test/workerUpdate/update.html b/dom/serviceworkers/test/workerUpdate/update.html
new file mode 100644
index 0000000000..666e213d14
--- /dev/null
+++ b/dom/serviceworkers/test/workerUpdate/update.html
@@ -0,0 +1,23 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test worker::update</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<script type="text/javascript">
+
+ navigator.serviceWorker.onmessage = function(e) { parent.postMessage(e.data, "*"); }
+ navigator.serviceWorker.ready.then(function() {
+ navigator.serviceWorker.controller.postMessage("GO");
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/serviceworkers/test/worker_unregister.js b/dom/serviceworkers/test/worker_unregister.js
new file mode 100644
index 0000000000..a2b45245b4
--- /dev/null
+++ b/dom/serviceworkers/test/worker_unregister.js
@@ -0,0 +1,22 @@
+onmessage = function(e) {
+ clients.matchAll().then(function(c) {
+ if (c.length === 0) {
+ // We cannot proceed.
+ return;
+ }
+
+ registration
+ .unregister()
+ .then(
+ function() {
+ c[0].postMessage("DONE");
+ },
+ function() {
+ c[0].postMessage("ERROR");
+ }
+ )
+ .then(function() {
+ c[0].postMessage("FINISH");
+ });
+ });
+};
diff --git a/dom/serviceworkers/test/worker_update.js b/dom/serviceworkers/test/worker_update.js
new file mode 100644
index 0000000000..5c83b9cd9e
--- /dev/null
+++ b/dom/serviceworkers/test/worker_update.js
@@ -0,0 +1,25 @@
+// For now this test only calls update to verify that our registration
+// job queueing works properly when called from the worker thread. We should
+// test actual update scenarios with a SJS test.
+onmessage = function(e) {
+ self.registration
+ .update()
+ .then(function(v) {
+ return v instanceof ServiceWorkerRegistration ? "FINISH" : "FAIL";
+ })
+ .catch(function(ex) {
+ return "FAIL";
+ })
+ .then(function(result) {
+ clients.matchAll().then(function(c) {
+ if (!c.length) {
+ dump(
+ "!!!!!!!!!!! WORKER HAS NO CLIENTS TO FINISH TEST !!!!!!!!!!!!\n"
+ );
+ return;
+ }
+
+ c[0].postMessage(result);
+ });
+ });
+};
diff --git a/dom/serviceworkers/test/worker_updatefoundevent.js b/dom/serviceworkers/test/worker_updatefoundevent.js
new file mode 100644
index 0000000000..cb6c6fa862
--- /dev/null
+++ b/dom/serviceworkers/test/worker_updatefoundevent.js
@@ -0,0 +1,20 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+registration.onupdatefound = function(e) {
+ clients.matchAll().then(function(clients) {
+ if (!clients.length) {
+ // We don't control any clients when the first update event is fired
+ // because we haven't reached the 'activated' state.
+ return;
+ }
+
+ if (registration.scope.match(/updatefoundevent\.html$/)) {
+ clients[0].postMessage("finish");
+ } else {
+ dump("Scope did not match");
+ }
+ });
+};
diff --git a/dom/serviceworkers/test/worker_updatefoundevent2.js b/dom/serviceworkers/test/worker_updatefoundevent2.js
new file mode 100644
index 0000000000..da4c592aad
--- /dev/null
+++ b/dom/serviceworkers/test/worker_updatefoundevent2.js
@@ -0,0 +1 @@
+// Not useful.
diff --git a/dom/serviceworkers/test/xslt/test.xml b/dom/serviceworkers/test/xslt/test.xml
new file mode 100644
index 0000000000..83c7776339
--- /dev/null
+++ b/dom/serviceworkers/test/xslt/test.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0"?>
+<?xml-stylesheet type="text/xsl" href="test.xsl"?>
+<result>
+ <Title>Example</Title>
+ <Error>Error</Error>
+</result>
diff --git a/dom/serviceworkers/test/xslt/xslt.sjs b/dom/serviceworkers/test/xslt/xslt.sjs
new file mode 100644
index 0000000000..db681ab500
--- /dev/null
+++ b/dom/serviceworkers/test/xslt/xslt.sjs
@@ -0,0 +1,12 @@
+function handleRequest(request, response) {
+ response.setHeader("Content-Type", "application/xslt+xml", false);
+ response.setHeader("Access-Control-Allow-Origin", "*");
+
+ var body = request.queryString;
+ if (!body) {
+ response.setStatusLine(null, 500, "Invalid querystring");
+ return;
+ }
+
+ response.write(unescape(body));
+}
diff --git a/dom/serviceworkers/test/xslt_worker.js b/dom/serviceworkers/test/xslt_worker.js
new file mode 100644
index 0000000000..d3c4383f22
--- /dev/null
+++ b/dom/serviceworkers/test/xslt_worker.js
@@ -0,0 +1,58 @@
+var testType = "synthetic";
+
+var xslt =
+ '<?xml version="1.0"?> ' +
+ '<xsl:stylesheet version="1.0"' +
+ ' xmlns:xsl="http://www.w3.org/1999/XSL/Transform">' +
+ ' <xsl:template match="node()|@*">' +
+ " <xsl:copy>" +
+ ' <xsl:apply-templates select="node()|@*"/>' +
+ " </xsl:copy>" +
+ " </xsl:template>" +
+ ' <xsl:template match="Error"/>' +
+ "</xsl:stylesheet>";
+
+onfetch = function(event) {
+ if (event.request.url.includes("test.xsl")) {
+ if (testType == "synthetic") {
+ if (event.request.mode != "cors") {
+ event.respondWith(Response.error());
+ return;
+ }
+
+ event.respondWith(
+ Promise.resolve(
+ new Response(xslt, {
+ headers: { "Content-Type": "application/xslt+xml" },
+ })
+ )
+ );
+ } else if (testType == "cors") {
+ if (event.request.mode != "cors") {
+ event.respondWith(Response.error());
+ return;
+ }
+
+ var url =
+ "http://example.com/tests/dom/serviceworkers/test/xslt/xslt.sjs?" +
+ escape(xslt);
+ event.respondWith(fetch(url, { mode: "cors" }));
+ } else if (testType == "opaque") {
+ if (event.request.mode != "cors") {
+ event.respondWith(Response.error());
+ return;
+ }
+
+ var url =
+ "http://example.com/tests/dom/serviceworkers/test/xslt/xslt.sjs?" +
+ escape(xslt);
+ event.respondWith(fetch(url, { mode: "no-cors" }));
+ } else {
+ event.respondWith(Response.error());
+ }
+ }
+};
+
+onmessage = function(event) {
+ testType = event.data;
+};