summaryrefslogtreecommitdiffstats
path: root/toolkit/components/antitracking
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/antitracking')
-rw-r--r--toolkit/components/antitracking/AntiTrackingIPCUtils.h59
-rw-r--r--toolkit/components/antitracking/AntiTrackingLog.h66
-rw-r--r--toolkit/components/antitracking/AntiTrackingRedirectHeuristic.cpp434
-rw-r--r--toolkit/components/antitracking/AntiTrackingRedirectHeuristic.h34
-rw-r--r--toolkit/components/antitracking/AntiTrackingUtils.cpp899
-rw-r--r--toolkit/components/antitracking/AntiTrackingUtils.h160
-rw-r--r--toolkit/components/antitracking/ContentBlockingAllowList.cpp257
-rw-r--r--toolkit/components/antitracking/ContentBlockingAllowList.h57
-rw-r--r--toolkit/components/antitracking/ContentBlockingAllowList.sys.mjs111
-rw-r--r--toolkit/components/antitracking/ContentBlockingLog.cpp272
-rw-r--r--toolkit/components/antitracking/ContentBlockingLog.h427
-rw-r--r--toolkit/components/antitracking/ContentBlockingNotifier.cpp567
-rw-r--r--toolkit/components/antitracking/ContentBlockingNotifier.h74
-rw-r--r--toolkit/components/antitracking/ContentBlockingTelemetryService.cpp120
-rw-r--r--toolkit/components/antitracking/ContentBlockingTelemetryService.h31
-rw-r--r--toolkit/components/antitracking/ContentBlockingUserInteraction.cpp89
-rw-r--r--toolkit/components/antitracking/ContentBlockingUserInteraction.h29
-rw-r--r--toolkit/components/antitracking/DynamicFpiRedirectHeuristic.cpp343
-rw-r--r--toolkit/components/antitracking/DynamicFpiRedirectHeuristic.h20
-rw-r--r--toolkit/components/antitracking/PartitioningExceptionList.cpp221
-rw-r--r--toolkit/components/antitracking/PartitioningExceptionList.h65
-rw-r--r--toolkit/components/antitracking/PartitioningExceptionListService.sys.mjs142
-rw-r--r--toolkit/components/antitracking/PurgeTrackerService.sys.mjs471
-rw-r--r--toolkit/components/antitracking/RejectForeignAllowList.cpp127
-rw-r--r--toolkit/components/antitracking/RejectForeignAllowList.h47
-rw-r--r--toolkit/components/antitracking/SettingsChangeObserver.cpp116
-rw-r--r--toolkit/components/antitracking/SettingsChangeObserver.h39
-rw-r--r--toolkit/components/antitracking/StorageAccess.cpp916
-rw-r--r--toolkit/components/antitracking/StorageAccess.h168
-rw-r--r--toolkit/components/antitracking/StorageAccessAPIHelper.cpp1105
-rw-r--r--toolkit/components/antitracking/StorageAccessAPIHelper.h195
-rw-r--r--toolkit/components/antitracking/StoragePrincipalHelper.cpp677
-rw-r--r--toolkit/components/antitracking/StoragePrincipalHelper.h358
-rw-r--r--toolkit/components/antitracking/TemporaryAccessGrantObserver.cpp97
-rw-r--r--toolkit/components/antitracking/TemporaryAccessGrantObserver.h87
-rw-r--r--toolkit/components/antitracking/TrackingDBService.sys.mjs375
-rw-r--r--toolkit/components/antitracking/URLDecorationAnnotationsService.sys.mjs70
-rw-r--r--toolkit/components/antitracking/URLDecorationStripper.cpp80
-rw-r--r--toolkit/components/antitracking/URLDecorationStripper.h26
-rw-r--r--toolkit/components/antitracking/URLQueryStringStripper.cpp283
-rw-r--r--toolkit/components/antitracking/URLQueryStringStripper.h58
-rw-r--r--toolkit/components/antitracking/URLQueryStrippingListService.sys.mjs307
-rw-r--r--toolkit/components/antitracking/antitracking.manifest1
-rw-r--r--toolkit/components/antitracking/components.conf64
-rw-r--r--toolkit/components/antitracking/docs/cookie-purging/index.md217
-rw-r--r--toolkit/components/antitracking/docs/data-sanitization/index.md443
-rw-r--r--toolkit/components/antitracking/docs/data-sanitization/media/image1.pngbin0 -> 17565 bytes
-rw-r--r--toolkit/components/antitracking/docs/data-sanitization/media/image2.pngbin0 -> 52969 bytes
-rw-r--r--toolkit/components/antitracking/docs/data-sanitization/media/image3.pngbin0 -> 38858 bytes
-rw-r--r--toolkit/components/antitracking/docs/data-sanitization/media/image4.pngbin0 -> 25861 bytes
-rw-r--r--toolkit/components/antitracking/docs/data-sanitization/media/image5.pngbin0 -> 123390 bytes
-rw-r--r--toolkit/components/antitracking/docs/data-sanitization/media/image6.pngbin0 -> 12129 bytes
-rw-r--r--toolkit/components/antitracking/docs/data-sanitization/media/image7.pngbin0 -> 13577 bytes
-rw-r--r--toolkit/components/antitracking/docs/data-sanitization/media/image8.pngbin0 -> 59889 bytes
-rw-r--r--toolkit/components/antitracking/docs/index.rst12
-rw-r--r--toolkit/components/antitracking/docs/query-stripping/index.md153
-rw-r--r--toolkit/components/antitracking/docs/query-stripping/overview.pngbin0 -> 45036 bytes
-rw-r--r--toolkit/components/antitracking/moz.build97
-rw-r--r--toolkit/components/antitracking/nsIContentBlockingAllowList.idl20
-rw-r--r--toolkit/components/antitracking/nsIPartitioningExceptionListService.idl51
-rw-r--r--toolkit/components/antitracking/nsIPurgeTrackerService.idl15
-rw-r--r--toolkit/components/antitracking/nsITrackingDBService.idl64
-rw-r--r--toolkit/components/antitracking/nsIURLDecorationAnnotationsService.idl27
-rw-r--r--toolkit/components/antitracking/nsIURLQueryStringStripper.idl35
-rw-r--r--toolkit/components/antitracking/nsIURLQueryStrippingListService.idl71
-rw-r--r--toolkit/components/antitracking/test/browser/.eslintrc.js7
-rw-r--r--toolkit/components/antitracking/test/browser/3rdParty.html53
-rw-r--r--toolkit/components/antitracking/test/browser/3rdPartyOpen.html16
-rw-r--r--toolkit/components/antitracking/test/browser/3rdPartyOpenUI.html17
-rw-r--r--toolkit/components/antitracking/test/browser/3rdPartyPartitioned.html29
-rw-r--r--toolkit/components/antitracking/test/browser/3rdPartyRelay.html41
-rw-r--r--toolkit/components/antitracking/test/browser/3rdPartySVG.html20
-rw-r--r--toolkit/components/antitracking/test/browser/3rdPartyStorage.html44
-rw-r--r--toolkit/components/antitracking/test/browser/3rdPartyStorageWO.html8
-rw-r--r--toolkit/components/antitracking/test/browser/3rdPartyUI.html32
-rw-r--r--toolkit/components/antitracking/test/browser/3rdPartyWO.html80
-rw-r--r--toolkit/components/antitracking/test/browser/3rdPartyWorker.html55
-rw-r--r--toolkit/components/antitracking/test/browser/antitracking_head.js1448
-rw-r--r--toolkit/components/antitracking/test/browser/browser-blocking.ini72
-rw-r--r--toolkit/components/antitracking/test/browser/browser.ini226
-rw-r--r--toolkit/components/antitracking/test/browser/browser_AntiTrackingETPHeuristic.js261
-rw-r--r--toolkit/components/antitracking/test/browser/browser_PBMCookieBehavior.js108
-rw-r--r--toolkit/components/antitracking/test/browser/browser_aboutblank.js44
-rw-r--r--toolkit/components/antitracking/test/browser/browser_addonHostPermissionIgnoredInTP.js46
-rw-r--r--toolkit/components/antitracking/test/browser/browser_allowListNotifications.js141
-rw-r--r--toolkit/components/antitracking/test/browser/browser_allowListNotifications_alwaysPartition.js142
-rw-r--r--toolkit/components/antitracking/test/browser/browser_allowListSeparationInPrivateAndNormalWindows.js52
-rw-r--r--toolkit/components/antitracking/test/browser/browser_allowPermissionForTracker.js64
-rw-r--r--toolkit/components/antitracking/test/browser/browser_backgroundImageAssertion.js69
-rw-r--r--toolkit/components/antitracking/test/browser/browser_blockingCookies.js183
-rw-r--r--toolkit/components/antitracking/test/browser/browser_blockingDOMCache.js39
-rw-r--r--toolkit/components/antitracking/test/browser/browser_blockingDOMCacheAlwaysPartition.js54
-rw-r--r--toolkit/components/antitracking/test/browser/browser_blockingDOMCacheAlwaysPartitionSAA.js80
-rw-r--r--toolkit/components/antitracking/test/browser/browser_blockingDOMCacheSAA.js85
-rw-r--r--toolkit/components/antitracking/test/browser/browser_blockingIndexedDb.js102
-rw-r--r--toolkit/components/antitracking/test/browser/browser_blockingIndexedDbInWorkers.js73
-rw-r--r--toolkit/components/antitracking/test/browser/browser_blockingIndexedDbInWorkers2.js152
-rw-r--r--toolkit/components/antitracking/test/browser/browser_blockingLocalStorage.js94
-rw-r--r--toolkit/components/antitracking/test/browser/browser_blockingMessaging.js322
-rw-r--r--toolkit/components/antitracking/test/browser/browser_blockingNoOpener.js41
-rw-r--r--toolkit/components/antitracking/test/browser/browser_blockingServiceWorkers.js30
-rw-r--r--toolkit/components/antitracking/test/browser/browser_blockingServiceWorkersStorageAccessAPI.js133
-rw-r--r--toolkit/components/antitracking/test/browser/browser_blockingSessionStorage.js126
-rw-r--r--toolkit/components/antitracking/test/browser/browser_blockingSharedWorkers.js94
-rw-r--r--toolkit/components/antitracking/test/browser/browser_contentBlockingAllowListPrincipal.js237
-rw-r--r--toolkit/components/antitracking/test/browser/browser_contentBlockingTelemetry.js404
-rw-r--r--toolkit/components/antitracking/test/browser/browser_cookieBetweenTabs.js59
-rw-r--r--toolkit/components/antitracking/test/browser/browser_denyPermissionForTracker.js64
-rw-r--r--toolkit/components/antitracking/test/browser/browser_doublyNestedTracker.js130
-rw-r--r--toolkit/components/antitracking/test/browser/browser_emailtracking.js182
-rw-r--r--toolkit/components/antitracking/test/browser/browser_existingCookiesForSubresources.js235
-rw-r--r--toolkit/components/antitracking/test/browser/browser_fileUrl.js41
-rw-r--r--toolkit/components/antitracking/test/browser/browser_firstPartyCookieRejectionHonoursAllowList.js77
-rw-r--r--toolkit/components/antitracking/test/browser/browser_fpiServiceWorkers_fingerprinting.js90
-rw-r--r--toolkit/components/antitracking/test/browser/browser_hasStorageAccess.js196
-rw-r--r--toolkit/components/antitracking/test/browser/browser_hasStorageAccess_alwaysPartition.js209
-rw-r--r--toolkit/components/antitracking/test/browser/browser_iframe_document_open.js86
-rw-r--r--toolkit/components/antitracking/test/browser/browser_imageCache4.js13
-rw-r--r--toolkit/components/antitracking/test/browser/browser_imageCache8.js13
-rw-r--r--toolkit/components/antitracking/test/browser/browser_localStorageEvents.js186
-rw-r--r--toolkit/components/antitracking/test/browser/browser_onBeforeRequestNotificationForTrackingResources.js96
-rw-r--r--toolkit/components/antitracking/test/browser/browser_onModifyRequestNotificationForTrackingResources.js96
-rw-r--r--toolkit/components/antitracking/test/browser/browser_partitionedClearSiteDataHeader.js593
-rw-r--r--toolkit/components/antitracking/test/browser/browser_partitionedConsoleMessage.js80
-rw-r--r--toolkit/components/antitracking/test/browser/browser_partitionedCookies.js137
-rw-r--r--toolkit/components/antitracking/test/browser/browser_partitionedDOMCache.js110
-rw-r--r--toolkit/components/antitracking/test/browser/browser_partitionedIndexedDB.js95
-rw-r--r--toolkit/components/antitracking/test/browser/browser_partitionedLocalStorage.js115
-rw-r--r--toolkit/components/antitracking/test/browser/browser_partitionedLocalStorage_events.js1014
-rw-r--r--toolkit/components/antitracking/test/browser/browser_partitionedMessaging.js20
-rw-r--r--toolkit/components/antitracking/test/browser/browser_partitionedServiceWorkers.js625
-rw-r--r--toolkit/components/antitracking/test/browser/browser_partitionedSharedWorkers.js74
-rw-r--r--toolkit/components/antitracking/test/browser/browser_permissionInNormalWindows.js106
-rw-r--r--toolkit/components/antitracking/test/browser/browser_permissionInNormalWindows_alwaysPartition.js109
-rw-r--r--toolkit/components/antitracking/test/browser/browser_permissionInPrivateWindows.js50
-rw-r--r--toolkit/components/antitracking/test/browser/browser_permissionInPrivateWindows_alwaysPartition.js49
-rw-r--r--toolkit/components/antitracking/test/browser/browser_permissionPropagation.js425
-rw-r--r--toolkit/components/antitracking/test/browser/browser_referrerDefaultPolicy.js634
-rw-r--r--toolkit/components/antitracking/test/browser/browser_script.js224
-rw-r--r--toolkit/components/antitracking/test/browser/browser_serviceWorkersWithStorageAccessGranted.js148
-rw-r--r--toolkit/components/antitracking/test/browser/browser_siteSpecificWorkArounds.js153
-rw-r--r--toolkit/components/antitracking/test/browser/browser_socialtracking.js147
-rw-r--r--toolkit/components/antitracking/test/browser/browser_socialtracking_save_image.js114
-rw-r--r--toolkit/components/antitracking/test/browser/browser_staticPartition_CORS_preflight.js149
-rw-r--r--toolkit/components/antitracking/test/browser/browser_staticPartition_CORS_preflight.sjs40
-rw-r--r--toolkit/components/antitracking/test/browser/browser_staticPartition_HSTS.js251
-rw-r--r--toolkit/components/antitracking/test/browser/browser_staticPartition_HSTS.sjs33
-rw-r--r--toolkit/components/antitracking/test/browser/browser_staticPartition_cache.js194
-rw-r--r--toolkit/components/antitracking/test/browser/browser_staticPartition_network.js116
-rw-r--r--toolkit/components/antitracking/test/browser/browser_staticPartition_saveAs.js532
-rw-r--r--toolkit/components/antitracking/test/browser/browser_staticPartition_tls_session.js115
-rw-r--r--toolkit/components/antitracking/test/browser/browser_staticPartition_websocket.js198
-rw-r--r--toolkit/components/antitracking/test/browser/browser_storageAccessAutograntRequiresUserInteraction.js57
-rw-r--r--toolkit/components/antitracking/test/browser/browser_storageAccessDeniedGivesNoUserInteraction.js30
-rw-r--r--toolkit/components/antitracking/test/browser/browser_storageAccessDoorHanger.js372
-rw-r--r--toolkit/components/antitracking/test/browser/browser_storageAccessFrameInteractionGrantsUserInteraction.js70
-rw-r--r--toolkit/components/antitracking/test/browser/browser_storageAccessGrantedGivesUserInteraction.js40
-rw-r--r--toolkit/components/antitracking/test/browser/browser_storageAccessPrivilegeAPI.js617
-rw-r--r--toolkit/components/antitracking/test/browser/browser_storageAccessPromiseRejectHandlerUserInteraction.js36
-rw-r--r--toolkit/components/antitracking/test/browser/browser_storageAccessPromiseRejectHandlerUserInteraction_alwaysPartition.js31
-rw-r--r--toolkit/components/antitracking/test/browser/browser_storageAccessPromiseResolveHandlerUserInteraction.js37
-rw-r--r--toolkit/components/antitracking/test/browser/browser_storageAccessRemovalNavigateSubframe.js46
-rw-r--r--toolkit/components/antitracking/test/browser/browser_storageAccessRemovalNavigateSubframe_alwaysPartition.js41
-rw-r--r--toolkit/components/antitracking/test/browser/browser_storageAccessRemovalNavigateTopframe.js44
-rw-r--r--toolkit/components/antitracking/test/browser/browser_storageAccessRemovalNavigateTopframe_alwaysPartition.js39
-rw-r--r--toolkit/components/antitracking/test/browser/browser_storageAccessSandboxed.js214
-rw-r--r--toolkit/components/antitracking/test/browser/browser_storageAccessSandboxed_alwaysPartition.js214
-rw-r--r--toolkit/components/antitracking/test/browser/browser_storageAccessScopeDifferentSite.js64
-rw-r--r--toolkit/components/antitracking/test/browser/browser_storageAccessScopeSameOrigin.js43
-rw-r--r--toolkit/components/antitracking/test/browser/browser_storageAccessScopeSameSiteRead.js43
-rw-r--r--toolkit/components/antitracking/test/browser/browser_storageAccessScopeSameSiteWrite.js43
-rw-r--r--toolkit/components/antitracking/test/browser/browser_storageAccessThirdPartyChecks.js157
-rw-r--r--toolkit/components/antitracking/test/browser/browser_storageAccessThirdPartyChecks_alwaysPartition.js153
-rw-r--r--toolkit/components/antitracking/test/browser/browser_storageAccessWithDynamicFpi.js612
-rw-r--r--toolkit/components/antitracking/test/browser/browser_storageAccessWithHeuristics.js912
-rw-r--r--toolkit/components/antitracking/test/browser/browser_storageAccess_TopLevel_Arguments.js117
-rw-r--r--toolkit/components/antitracking/test/browser/browser_storageAccess_TopLevel_CookieBehavior.js412
-rw-r--r--toolkit/components/antitracking/test/browser/browser_storageAccess_TopLevel_CookiePermission.js174
-rw-r--r--toolkit/components/antitracking/test/browser/browser_storageAccess_TopLevel_CrossOriginSameSite.js162
-rw-r--r--toolkit/components/antitracking/test/browser/browser_storageAccess_TopLevel_Doorhanger.js122
-rw-r--r--toolkit/components/antitracking/test/browser/browser_storageAccess_TopLevel_Embed.js155
-rw-r--r--toolkit/components/antitracking/test/browser/browser_storageAccess_TopLevel_Enable.js86
-rw-r--r--toolkit/components/antitracking/test/browser/browser_storageAccess_TopLevel_RequireIntermediatePermission.js61
-rw-r--r--toolkit/components/antitracking/test/browser/browser_storageAccess_TopLevel_StorageAccessPermission.js94
-rw-r--r--toolkit/components/antitracking/test/browser/browser_storageAccess_TopLevel_UserActivation.js63
-rw-r--r--toolkit/components/antitracking/test/browser/browser_subResources.js277
-rw-r--r--toolkit/components/antitracking/test/browser/browser_subResourcesPartitioned.js308
-rw-r--r--toolkit/components/antitracking/test/browser/browser_subResourcesPartitioned_alwaysPartition.js313
-rw-r--r--toolkit/components/antitracking/test/browser/browser_thirdPartyStorageRejectionForCORS.js96
-rw-r--r--toolkit/components/antitracking/test/browser/browser_urlDecorationStripping.js253
-rw-r--r--toolkit/components/antitracking/test/browser/browser_urlDecorationStripping_alwaysPartition.js255
-rw-r--r--toolkit/components/antitracking/test/browser/browser_urlQueryStringStripping.js855
-rw-r--r--toolkit/components/antitracking/test/browser/browser_urlQueryStringStripping_allowList.js442
-rw-r--r--toolkit/components/antitracking/test/browser/browser_urlQueryStringStripping_nimbus.js145
-rw-r--r--toolkit/components/antitracking/test/browser/browser_urlQueryStringStripping_pbmode.js105
-rw-r--r--toolkit/components/antitracking/test/browser/browser_urlQueryStringStripping_telemetry.js355
-rw-r--r--toolkit/components/antitracking/test/browser/browser_urlQueryStringStripping_telemetry_2.js159
-rw-r--r--toolkit/components/antitracking/test/browser/browser_urlQueryStrippingListService.js249
-rw-r--r--toolkit/components/antitracking/test/browser/browser_userInteraction.js124
-rw-r--r--toolkit/components/antitracking/test/browser/browser_workerPropagation.js87
-rw-r--r--toolkit/components/antitracking/test/browser/clearSiteData.sjs6
-rw-r--r--toolkit/components/antitracking/test/browser/container.html6
-rw-r--r--toolkit/components/antitracking/test/browser/container2.html11
-rw-r--r--toolkit/components/antitracking/test/browser/cookies.sjs12
-rw-r--r--toolkit/components/antitracking/test/browser/cookiesCORS.sjs9
-rw-r--r--toolkit/components/antitracking/test/browser/dedicatedWorker.js3
-rw-r--r--toolkit/components/antitracking/test/browser/dynamicfpi_head.js180
-rw-r--r--toolkit/components/antitracking/test/browser/embedder.html4
-rw-r--r--toolkit/components/antitracking/test/browser/embedder2.html9
-rw-r--r--toolkit/components/antitracking/test/browser/empty-altsvc.js1
-rw-r--r--toolkit/components/antitracking/test/browser/empty-altsvc.js^headers^1
-rw-r--r--toolkit/components/antitracking/test/browser/empty.html1
-rw-r--r--toolkit/components/antitracking/test/browser/empty.js1
-rw-r--r--toolkit/components/antitracking/test/browser/file_iframe_document_open.html19
-rw-r--r--toolkit/components/antitracking/test/browser/file_localStorage.html21
-rw-r--r--toolkit/components/antitracking/test/browser/file_saveAsImage.sjs20
-rw-r--r--toolkit/components/antitracking/test/browser/file_saveAsPageInfo.html6
-rw-r--r--toolkit/components/antitracking/test/browser/file_saveAsVideo.sjs38
-rw-r--r--toolkit/components/antitracking/test/browser/file_stripping.html20
-rw-r--r--toolkit/components/antitracking/test/browser/file_video.ogvbin0 -> 16049 bytes
-rw-r--r--toolkit/components/antitracking/test/browser/file_ws_handshake_delay_wsh.py28
-rw-r--r--toolkit/components/antitracking/test/browser/head.js149
-rw-r--r--toolkit/components/antitracking/test/browser/iframe.html8
-rw-r--r--toolkit/components/antitracking/test/browser/image.sjs22
-rw-r--r--toolkit/components/antitracking/test/browser/imageCacheWorker.js78
-rw-r--r--toolkit/components/antitracking/test/browser/localStorage.html68
-rw-r--r--toolkit/components/antitracking/test/browser/localStorageEvents.html30
-rw-r--r--toolkit/components/antitracking/test/browser/matchAll.js16
-rw-r--r--toolkit/components/antitracking/test/browser/page.html8
-rw-r--r--toolkit/components/antitracking/test/browser/partitionedSharedWorker.js17
-rw-r--r--toolkit/components/antitracking/test/browser/partitionedstorage_head.js455
-rw-r--r--toolkit/components/antitracking/test/browser/popup.html11
-rw-r--r--toolkit/components/antitracking/test/browser/raptor.jpgbin0 -> 49629 bytes
-rw-r--r--toolkit/components/antitracking/test/browser/redirect.sjs11
-rw-r--r--toolkit/components/antitracking/test/browser/referrer.sjs49
-rw-r--r--toolkit/components/antitracking/test/browser/sandboxed.html12
-rw-r--r--toolkit/components/antitracking/test/browser/sandboxed.html^headers^1
-rw-r--r--toolkit/components/antitracking/test/browser/server.sjs20
-rw-r--r--toolkit/components/antitracking/test/browser/serviceWorker.js103
-rw-r--r--toolkit/components/antitracking/test/browser/sharedWorker.js18
-rw-r--r--toolkit/components/antitracking/test/browser/storageAccessAPIHelpers.js236
-rw-r--r--toolkit/components/antitracking/test/browser/storage_access_head.js248
-rw-r--r--toolkit/components/antitracking/test/browser/subResources.sjs33
-rw-r--r--toolkit/components/antitracking/test/browser/tracker.js7
-rw-r--r--toolkit/components/antitracking/test/browser/workerIframe.html71
-rw-r--r--toolkit/components/antitracking/test/gtest/TestPartitioningExceptionList.cpp184
-rw-r--r--toolkit/components/antitracking/test/gtest/TestStoragePrincipalHelper.cpp215
-rw-r--r--toolkit/components/antitracking/test/gtest/TestURLQueryStringStripper.cpp176
-rw-r--r--toolkit/components/antitracking/test/gtest/moz.build19
-rw-r--r--toolkit/components/antitracking/test/xpcshell/data/font.woffbin0 -> 1112 bytes
-rw-r--r--toolkit/components/antitracking/test/xpcshell/head.js11
-rw-r--r--toolkit/components/antitracking/test/xpcshell/test_ExceptionListService.js107
-rw-r--r--toolkit/components/antitracking/test/xpcshell/test_cookie_behavior.js94
-rw-r--r--toolkit/components/antitracking/test/xpcshell/test_getPartitionKeyFromURL.js223
-rw-r--r--toolkit/components/antitracking/test/xpcshell/test_purge_trackers.js523
-rw-r--r--toolkit/components/antitracking/test/xpcshell/test_purge_trackers_telemetry.js175
-rw-r--r--toolkit/components/antitracking/test/xpcshell/test_rejectForeignAllowList.js116
-rw-r--r--toolkit/components/antitracking/test/xpcshell/test_staticPartition_authhttp.js125
-rw-r--r--toolkit/components/antitracking/test/xpcshell/test_staticPartition_clientAuthRemember.js129
-rw-r--r--toolkit/components/antitracking/test/xpcshell/test_staticPartition_font.js112
-rw-r--r--toolkit/components/antitracking/test/xpcshell/test_staticPartition_image.js86
-rw-r--r--toolkit/components/antitracking/test/xpcshell/test_staticPartition_prefetch.js177
-rw-r--r--toolkit/components/antitracking/test/xpcshell/test_staticPartition_preload.js187
-rw-r--r--toolkit/components/antitracking/test/xpcshell/test_tracking_db_service.js492
-rw-r--r--toolkit/components/antitracking/test/xpcshell/test_view_source.js78
-rw-r--r--toolkit/components/antitracking/test/xpcshell/xpcshell.ini51
266 files changed, 39827 insertions, 0 deletions
diff --git a/toolkit/components/antitracking/AntiTrackingIPCUtils.h b/toolkit/components/antitracking/AntiTrackingIPCUtils.h
new file mode 100644
index 0000000000..cbe9b1eef6
--- /dev/null
+++ b/toolkit/components/antitracking/AntiTrackingIPCUtils.h
@@ -0,0 +1,59 @@
+/* -*- 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_antitrackingipcutils_h
+#define mozilla_antitrackingipcutils_h
+
+#include "ipc/EnumSerializer.h"
+
+#include "mozilla/ContentBlockingNotifier.h"
+#include "mozilla/StorageAccessAPIHelper.h"
+
+#include "nsILoadInfo.h"
+
+namespace IPC {
+
+// For allowing passing the enum
+// ContentBlockingNotifier::StorageAccessPermissionGrantedReason over IPC.
+template <>
+struct ParamTraits<
+ mozilla::ContentBlockingNotifier::StorageAccessPermissionGrantedReason>
+ : public ContiguousEnumSerializerInclusive<
+ mozilla::ContentBlockingNotifier::
+ StorageAccessPermissionGrantedReason,
+ mozilla::ContentBlockingNotifier::
+ StorageAccessPermissionGrantedReason::eStorageAccessAPI,
+ mozilla::ContentBlockingNotifier::
+ StorageAccessPermissionGrantedReason::
+ ePrivilegeStorageAccessForOriginAPI> {};
+
+// ContentBlockingNotifier::BlockingDecision over IPC.
+template <>
+struct ParamTraits<mozilla::ContentBlockingNotifier::BlockingDecision>
+ : public ContiguousEnumSerializerInclusive<
+ mozilla::ContentBlockingNotifier::BlockingDecision,
+ mozilla::ContentBlockingNotifier::BlockingDecision::eBlock,
+ mozilla::ContentBlockingNotifier::BlockingDecision::eAllow> {};
+
+// StorageAccessAPIHelper::StorageAccessPromptChoices over IPC.
+template <>
+struct ParamTraits<mozilla::StorageAccessAPIHelper::StorageAccessPromptChoices>
+ : public ContiguousEnumSerializerInclusive<
+ mozilla::StorageAccessAPIHelper::StorageAccessPromptChoices,
+ mozilla::StorageAccessAPIHelper::StorageAccessPromptChoices::eAllow,
+ mozilla::StorageAccessAPIHelper::StorageAccessPromptChoices::
+ eAllowAutoGrant> {};
+
+// nsILoadInfo::StoragePermissionState over IPC.
+template <>
+struct ParamTraits<nsILoadInfo::StoragePermissionState>
+ : public ContiguousEnumSerializerInclusive<
+ nsILoadInfo::StoragePermissionState,
+ nsILoadInfo::StoragePermissionState::NoStoragePermission,
+ nsILoadInfo::StoragePermissionState::StoragePermissionAllowListed> {};
+} // namespace IPC
+
+#endif // mozilla_antitrackingipcutils_h
diff --git a/toolkit/components/antitracking/AntiTrackingLog.h b/toolkit/components/antitracking/AntiTrackingLog.h
new file mode 100644
index 0000000000..09d4828c44
--- /dev/null
+++ b/toolkit/components/antitracking/AntiTrackingLog.h
@@ -0,0 +1,66 @@
+/* -*- 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_antitrackinglog_h
+#define mozilla_antitrackinglog_h
+
+#include "mozilla/Logging.h"
+#include "nsString.h"
+
+namespace mozilla {
+
+extern LazyLogModule gAntiTrackingLog;
+static const nsCString::size_type sMaxSpecLength = 128;
+
+#define LOG(format) MOZ_LOG(gAntiTrackingLog, mozilla::LogLevel::Debug, format)
+
+#define LOG_SPEC(format, uri) \
+ PR_BEGIN_MACRO \
+ if (MOZ_LOG_TEST(gAntiTrackingLog, mozilla::LogLevel::Debug)) { \
+ nsAutoCString _specStr("(null)"_ns); \
+ if (uri) { \
+ _specStr = (uri)->GetSpecOrDefault(); \
+ } \
+ _specStr.Truncate(std::min(_specStr.Length(), sMaxSpecLength)); \
+ const char* _spec = _specStr.get(); \
+ LOG(format); \
+ } \
+ PR_END_MACRO
+
+#define LOG_SPEC2(format, uri1, uri2) \
+ PR_BEGIN_MACRO \
+ if (MOZ_LOG_TEST(gAntiTrackingLog, mozilla::LogLevel::Debug)) { \
+ nsAutoCString _specStr1("(null)"_ns); \
+ if (uri1) { \
+ _specStr1 = (uri1)->GetSpecOrDefault(); \
+ } \
+ _specStr1.Truncate(std::min(_specStr1.Length(), sMaxSpecLength)); \
+ const char* _spec1 = _specStr1.get(); \
+ nsAutoCString _specStr2("(null)"_ns); \
+ if (uri2) { \
+ _specStr2 = (uri2)->GetSpecOrDefault(); \
+ } \
+ _specStr2.Truncate(std::min(_specStr2.Length(), sMaxSpecLength)); \
+ const char* _spec2 = _specStr2.get(); \
+ LOG(format); \
+ } \
+ PR_END_MACRO
+
+#define LOG_PRIN(format, principal) \
+ PR_BEGIN_MACRO \
+ if (MOZ_LOG_TEST(gAntiTrackingLog, mozilla::LogLevel::Debug)) { \
+ nsAutoCString _specStr("(null)"_ns); \
+ if (principal) { \
+ (principal)->GetAsciiSpec(_specStr); \
+ } \
+ _specStr.Truncate(std::min(_specStr.Length(), sMaxSpecLength)); \
+ const char* _spec = _specStr.get(); \
+ LOG(format); \
+ } \
+ PR_END_MACRO
+} // namespace mozilla
+
+#endif // mozilla_antitrackinglog_h
diff --git a/toolkit/components/antitracking/AntiTrackingRedirectHeuristic.cpp b/toolkit/components/antitracking/AntiTrackingRedirectHeuristic.cpp
new file mode 100644
index 0000000000..747427abcc
--- /dev/null
+++ b/toolkit/components/antitracking/AntiTrackingRedirectHeuristic.cpp
@@ -0,0 +1,434 @@
+/* -*- 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 "AntiTrackingLog.h"
+#include "AntiTrackingRedirectHeuristic.h"
+#include "ContentBlockingAllowList.h"
+#include "ContentBlockingUserInteraction.h"
+#include "StorageAccessAPIHelper.h"
+
+#include "mozilla/dom/BrowsingContext.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/net/CookieJarSettings.h"
+#include "mozilla/net/UrlClassifierCommon.h"
+#include "mozilla/StaticPrefs_network.h"
+#include "mozilla/Telemetry.h"
+#include "nsContentUtils.h"
+#include "nsIChannel.h"
+#include "nsIClassifiedChannel.h"
+#include "nsICookieService.h"
+#include "nsIHttpChannel.h"
+#include "nsIRedirectHistoryEntry.h"
+#include "nsIScriptError.h"
+#include "nsIURI.h"
+#include "nsNetUtil.h"
+#include "nsPIDOMWindow.h"
+#include "nsScriptSecurityManager.h"
+
+namespace mozilla {
+
+namespace {
+
+// The helper function to check if we need to check the redirect heuristic for
+// ETP later when we know the classification flags of the new channel. This
+// check is from the perspective of the old channel. We don't check for the new
+// channel because the classification flags are not ready yet when we call this
+// function.
+bool ShouldCheckRedirectHeuristicETP(nsIChannel* aOldChannel, nsIURI* aOldURI,
+ nsIPrincipal* aOldPrincipal) {
+ nsCOMPtr<nsIClassifiedChannel> oldClassifiedChannel =
+ do_QueryInterface(aOldChannel);
+
+ if (!oldClassifiedChannel) {
+ LOG_SPEC(("Ignoring the redirect from %s because there is no "
+ "nsIClassifiedChannel interface",
+ _spec),
+ aOldURI);
+ return false;
+ }
+
+ nsCOMPtr<nsILoadInfo> oldLoadInfo = aOldChannel->LoadInfo();
+ MOZ_ASSERT(oldLoadInfo);
+
+ bool allowedByPreviousRedirect =
+ oldLoadInfo->GetAllowListFutureDocumentsCreatedFromThisRedirectChain();
+
+ uint32_t oldClassificationFlags =
+ oldClassifiedChannel->GetFirstPartyClassificationFlags();
+
+ // We will skip this check if we have granted storage access before so that we
+ // can grant the storage access to the rest of the chain.
+ if (!net::UrlClassifierCommon::IsTrackingClassificationFlag(
+ oldClassificationFlags, NS_UsePrivateBrowsing(aOldChannel)) &&
+ !allowedByPreviousRedirect) {
+ // This is not a tracking -> non-tracking redirect.
+ LOG_SPEC(("Ignoring the redirect from %s because it's not tracking to "
+ "non-tracking.",
+ _spec),
+ aOldURI);
+ return false;
+ }
+
+ if (!ContentBlockingUserInteraction::Exists(aOldPrincipal)) {
+ LOG_SPEC(("Ignoring redirect for %s because no user-interaction on "
+ "tracker",
+ _spec),
+ aOldURI);
+ return false;
+ }
+
+ return true;
+}
+
+// The helper function to decide and set the storage access after we know the
+// classification flags of the new channel.
+bool ShouldRedirectHeuristicApplyETP(nsIChannel* aNewChannel, nsIURI* aNewURI) {
+ nsCOMPtr<nsIClassifiedChannel> newClassifiedChannel =
+ do_QueryInterface(aNewChannel);
+
+ if (!aNewChannel) {
+ LOG_SPEC(("Ignoring the redirect to %s because there is no "
+ "nsIClassifiedChannel interface",
+ _spec),
+ aNewURI);
+ return false;
+ }
+
+ // We're looking at the first-party classification flags because we're
+ // interested in first-party redirects.
+ uint32_t newClassificationFlags =
+ newClassifiedChannel->GetFirstPartyClassificationFlags();
+
+ if (net::UrlClassifierCommon::IsTrackingClassificationFlag(
+ newClassificationFlags, NS_UsePrivateBrowsing(aNewChannel))) {
+ // This is not a tracking -> non-tracking redirect.
+ LOG_SPEC(("Ignoring the redirect to %s because it's not tracking to "
+ "non-tracking.",
+ _spec),
+ aNewURI);
+ return false;
+ }
+
+ return true;
+}
+
+// The helper function to check if we need to check the redirect heuristic for
+// RejectForeign later from the old channel perspective.
+bool ShouldCheckRedirectHeuristicRejectForeign(nsIChannel* aOldChannel,
+ nsIURI* aOldURI,
+ nsIPrincipal* aOldPrincipal) {
+ if (!ContentBlockingUserInteraction::Exists(aOldPrincipal)) {
+ LOG_SPEC(("Ignoring redirect from %s because no user-interaction on "
+ "old origin",
+ _spec),
+ aOldURI);
+ return false;
+ }
+
+ return true;
+}
+
+bool ShouldRedirectHeuristicApply(nsIChannel* aNewChannel, nsIURI* aNewURI) {
+ nsCOMPtr<nsILoadInfo> newLoadInfo = aNewChannel->LoadInfo();
+ MOZ_ASSERT(newLoadInfo);
+
+ nsCOMPtr<nsICookieJarSettings> cookieJarSettings;
+
+ nsresult rv =
+ newLoadInfo->GetCookieJarSettings(getter_AddRefs(cookieJarSettings));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ LOG(("Can't obtain the cookieJarSetting from the channel"));
+ return false;
+ }
+
+ uint32_t cookieBehavior = cookieJarSettings->GetCookieBehavior();
+ if (cookieBehavior == nsICookieService::BEHAVIOR_REJECT_TRACKER ||
+ cookieBehavior ==
+ nsICookieService::BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN) {
+ return ShouldRedirectHeuristicApplyETP(aNewChannel, aNewURI);
+ }
+
+ // We will grant the storage access regardless the new channel is a tracker or
+ // not for RejectForeign.
+ if (cookieBehavior == nsICookieService::BEHAVIOR_REJECT_FOREIGN &&
+ StaticPrefs::network_cookie_rejectForeignWithExceptions_enabled()) {
+ return true;
+ }
+
+ LOG((
+ "Heuristic doesn't apply because the cookieBehavior doesn't require it"));
+ return false;
+}
+
+bool ShouldCheckRedirectHeuristic(nsIChannel* aOldChannel, nsIURI* aOldURI,
+ nsIPrincipal* aOldPrincipal) {
+ nsCOMPtr<nsILoadInfo> oldLoadInfo = aOldChannel->LoadInfo();
+ MOZ_ASSERT(oldLoadInfo);
+
+ nsCOMPtr<nsICookieJarSettings> cookieJarSettings;
+
+ nsresult rv =
+ oldLoadInfo->GetCookieJarSettings(getter_AddRefs(cookieJarSettings));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ LOG(("Can't obtain the cookieJarSettings from the old channel"));
+ return false;
+ }
+
+ uint32_t cookieBehavior = cookieJarSettings->GetCookieBehavior();
+ if (cookieBehavior == nsICookieService::BEHAVIOR_REJECT_TRACKER ||
+ cookieBehavior ==
+ nsICookieService::BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN) {
+ return ShouldCheckRedirectHeuristicETP(aOldChannel, aOldURI, aOldPrincipal);
+ }
+
+ if (cookieBehavior == nsICookieService::BEHAVIOR_REJECT_FOREIGN &&
+ StaticPrefs::network_cookie_rejectForeignWithExceptions_enabled()) {
+ return ShouldCheckRedirectHeuristicRejectForeign(aOldChannel, aOldURI,
+ aOldPrincipal);
+ }
+
+ LOG(
+ ("Heuristic doesn't be needed because the cookieBehavior doesn't require "
+ "it"));
+ return false;
+}
+
+} // namespace
+
+void PrepareForAntiTrackingRedirectHeuristic(nsIChannel* aOldChannel,
+ nsIURI* aOldURI,
+ nsIChannel* aNewChannel,
+ nsIURI* aNewURI) {
+ MOZ_ASSERT(aOldChannel);
+ MOZ_ASSERT(aOldURI);
+ MOZ_ASSERT(aNewChannel);
+ MOZ_ASSERT(aNewURI);
+
+ // This heuristic works only on the parent process.
+ if (!XRE_IsParentProcess()) {
+ return;
+ }
+
+ if (!StaticPrefs::privacy_antitracking_enableWebcompat() ||
+ !StaticPrefs::privacy_restrict3rdpartystorage_heuristic_redirect()) {
+ return;
+ }
+
+ nsCOMPtr<nsIHttpChannel> oldChannel = do_QueryInterface(aOldChannel);
+ if (!oldChannel) {
+ return;
+ }
+
+ nsCOMPtr<nsIHttpChannel> newChannel = do_QueryInterface(aNewChannel);
+ if (!newChannel) {
+ return;
+ }
+
+ LOG_SPEC2(("Preparing redirect-heuristic for the redirect %s -> %s", _spec1,
+ _spec2),
+ aOldURI, aNewURI);
+
+ nsCOMPtr<nsILoadInfo> oldLoadInfo = aOldChannel->LoadInfo();
+ MOZ_ASSERT(oldLoadInfo);
+
+ nsCOMPtr<nsILoadInfo> newLoadInfo = aNewChannel->LoadInfo();
+ MOZ_ASSERT(newLoadInfo);
+
+ // We need to clear the flag first because the new loadInfo was cloned from
+ // the old loadInfo.
+ newLoadInfo->SetNeedForCheckingAntiTrackingHeuristic(false);
+
+ nsCOMPtr<nsICookieJarSettings> cookieJarSettings;
+ nsresult rv =
+ oldLoadInfo->GetCookieJarSettings(getter_AddRefs(cookieJarSettings));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ LOG(("Can't get the cookieJarSettings"));
+ return;
+ }
+
+ int32_t behavior = cookieJarSettings->GetCookieBehavior();
+
+ if (!cookieJarSettings->GetRejectThirdPartyContexts()) {
+ LOG(
+ ("Disabled by network.cookie.cookieBehavior pref (%d), bailing out "
+ "early",
+ behavior));
+ return;
+ }
+
+ MOZ_ASSERT(
+ behavior == nsICookieService::BEHAVIOR_REJECT_TRACKER ||
+ behavior ==
+ nsICookieService::BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN ||
+ net::CookieJarSettings::IsRejectThirdPartyWithExceptions(behavior));
+
+ ExtContentPolicyType contentType =
+ oldLoadInfo->GetExternalContentPolicyType();
+ if (contentType != ExtContentPolicy::TYPE_DOCUMENT ||
+ !aOldChannel->IsDocument()) {
+ LOG_SPEC(("Ignoring redirect for %s because it's not a document", _spec),
+ aOldURI);
+ // We care about document redirects only.
+ return;
+ }
+
+ if (ContentBlockingAllowList::Check(newChannel)) {
+ return;
+ }
+
+ nsIScriptSecurityManager* ssm =
+ nsScriptSecurityManager::GetScriptSecurityManager();
+ MOZ_ASSERT(ssm);
+
+ nsCOMPtr<nsIPrincipal> oldPrincipal;
+
+ const nsTArray<nsCOMPtr<nsIRedirectHistoryEntry>>& chain =
+ oldLoadInfo->RedirectChain();
+
+ if (oldLoadInfo->GetAllowListFutureDocumentsCreatedFromThisRedirectChain() &&
+ !chain.IsEmpty()) {
+ rv = chain[0]->GetPrincipal(getter_AddRefs(oldPrincipal));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ LOG(("Can't obtain the principal from the redirect chain"));
+ return;
+ }
+ } else {
+ rv = ssm->GetChannelResultPrincipal(aOldChannel,
+ getter_AddRefs(oldPrincipal));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ LOG(("Can't obtain the principal from the previous channel"));
+ return;
+ }
+ }
+
+ newLoadInfo->SetNeedForCheckingAntiTrackingHeuristic(
+ ShouldCheckRedirectHeuristic(aOldChannel, aOldURI, oldPrincipal));
+}
+
+void FinishAntiTrackingRedirectHeuristic(nsIChannel* aNewChannel,
+ nsIURI* aNewURI) {
+ MOZ_ASSERT(aNewChannel);
+ MOZ_ASSERT(aNewURI);
+
+ // This heuristic works only on the parent process.
+ if (!XRE_IsParentProcess()) {
+ return;
+ }
+
+ if (!StaticPrefs::privacy_antitracking_enableWebcompat() ||
+ !StaticPrefs::privacy_restrict3rdpartystorage_heuristic_redirect()) {
+ return;
+ }
+
+ nsCOMPtr<nsIHttpChannel> newChannel = do_QueryInterface(aNewChannel);
+ if (!newChannel) {
+ return;
+ }
+
+ LOG_SPEC(("Finishing redirect-heuristic for the redirect to %s", _spec),
+ aNewURI);
+
+ nsCOMPtr<nsILoadInfo> newLoadInfo = newChannel->LoadInfo();
+ MOZ_ASSERT(newLoadInfo);
+
+ // Bailing out early if there is no need to do the heuristic.
+ if (!newLoadInfo->GetNeedForCheckingAntiTrackingHeuristic()) {
+ return;
+ }
+
+ if (!ShouldRedirectHeuristicApply(aNewChannel, aNewURI)) {
+ return;
+ }
+
+ nsIScriptSecurityManager* ssm =
+ nsScriptSecurityManager::GetScriptSecurityManager();
+ MOZ_ASSERT(ssm);
+
+ const nsTArray<nsCOMPtr<nsIRedirectHistoryEntry>>& chain =
+ newLoadInfo->RedirectChain();
+
+ if (chain.IsEmpty()) {
+ LOG(("Can't obtain the redirect chain"));
+ return;
+ }
+
+ nsCOMPtr<nsIPrincipal> oldPrincipal;
+ uint32_t idx =
+ newLoadInfo->GetAllowListFutureDocumentsCreatedFromThisRedirectChain()
+ ? 0
+ : chain.Length() - 1;
+
+ nsresult rv = chain[idx]->GetPrincipal(getter_AddRefs(oldPrincipal));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ LOG(("Can't obtain the principal from the redirect chain"));
+ return;
+ }
+
+ nsCOMPtr<nsIPrincipal> newPrincipal;
+ rv =
+ ssm->GetChannelResultPrincipal(aNewChannel, getter_AddRefs(newPrincipal));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ LOG(("Can't obtain the principal from the new channel"));
+ return;
+ }
+
+ if (oldPrincipal->Equals(newPrincipal)) {
+ LOG(("No permission needed for same principals."));
+ return;
+ }
+
+ nsAutoCString oldOrigin;
+ rv = oldPrincipal->GetOrigin(oldOrigin);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ LOG(("Can't get the origin from the Principal"));
+ return;
+ }
+
+ nsAutoCString newOrigin;
+ rv = nsContentUtils::GetASCIIOrigin(aNewURI, newOrigin);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ LOG(("Can't get the origin from the URI"));
+ return;
+ }
+
+ LOG(("Adding a first-party storage exception for %s...", newOrigin.get()));
+
+ LOG(("Saving the permission: oldOrigin=%s, grantedOrigin=%s", oldOrigin.get(),
+ newOrigin.get()));
+
+ // Any new redirect from this loadInfo must be considered as granted.
+ newLoadInfo->SetAllowListFutureDocumentsCreatedFromThisRedirectChain(true);
+
+ uint64_t innerWindowID;
+ Unused << newChannel->GetTopLevelContentWindowId(&innerWindowID);
+
+ nsAutoString errorText;
+ AutoTArray<nsString, 2> params = {NS_ConvertUTF8toUTF16(newOrigin),
+ NS_ConvertUTF8toUTF16(oldOrigin)};
+ rv = nsContentUtils::FormatLocalizedString(
+ nsContentUtils::eNECKO_PROPERTIES, "CookieAllowedForOriginByHeuristic",
+ params, errorText);
+ if (NS_SUCCEEDED(rv)) {
+ nsContentUtils::ReportToConsoleByWindowID(
+ errorText, nsIScriptError::warningFlag, ANTITRACKING_CONSOLE_CATEGORY,
+ innerWindowID);
+ }
+
+ Telemetry::AccumulateCategorical(
+ Telemetry::LABELS_STORAGE_ACCESS_GRANTED_COUNT::StorageGranted);
+ Telemetry::AccumulateCategorical(
+ Telemetry::LABELS_STORAGE_ACCESS_GRANTED_COUNT::Redirect);
+
+ // We don't care about this promise because the operation is actually sync.
+ RefPtr<StorageAccessAPIHelper::ParentAccessGrantPromise> promise =
+ StorageAccessAPIHelper::SaveAccessForOriginOnParentProcess(
+ newPrincipal, oldPrincipal,
+ StorageAccessAPIHelper::StorageAccessPromptChoices::eAllow,
+ StaticPrefs::privacy_restrict3rdpartystorage_expiration_redirect());
+ Unused << promise;
+}
+
+} // namespace mozilla
diff --git a/toolkit/components/antitracking/AntiTrackingRedirectHeuristic.h b/toolkit/components/antitracking/AntiTrackingRedirectHeuristic.h
new file mode 100644
index 0000000000..6a6ccbab0c
--- /dev/null
+++ b/toolkit/components/antitracking/AntiTrackingRedirectHeuristic.h
@@ -0,0 +1,34 @@
+/* -*- 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_antitrackingredirectheuristic_h
+#define mozilla_antitrackingredirectheuristic_h
+
+class nsIChannel;
+class nsIURI;
+
+namespace mozilla {
+
+// This function will be called when we know we are about to perform a http
+// redirect. It will check if we need to perform the AntiTracking redirect
+// heuristic from the old channel perspective. We cannot know the classification
+// flags of the new channel at the point. So, we will save the result in the
+// loadInfo in order to finish the heuristic once the classification flags is
+// ready.
+void PrepareForAntiTrackingRedirectHeuristic(nsIChannel* aOldChannel,
+ nsIURI* aOldURI,
+ nsIChannel* aNewChannel,
+ nsIURI* aNewURI);
+
+// This function will be called once the classification flags of the new channel
+// is known. It will check and perform the AntiTracking redirect heuristic
+// according to the flags and the result from previous preparation.
+void FinishAntiTrackingRedirectHeuristic(nsIChannel* aNewChannel,
+ nsIURI* aNewURI);
+
+} // namespace mozilla
+
+#endif // mozilla_antitrackingredirectheuristic_h
diff --git a/toolkit/components/antitracking/AntiTrackingUtils.cpp b/toolkit/components/antitracking/AntiTrackingUtils.cpp
new file mode 100644
index 0000000000..d40cd71c48
--- /dev/null
+++ b/toolkit/components/antitracking/AntiTrackingUtils.cpp
@@ -0,0 +1,899 @@
+/* -*- 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 "AntiTrackingUtils.h"
+
+#include "AntiTrackingLog.h"
+#include "mozilla/BasePrincipal.h"
+#include "mozilla/Components.h"
+#include "mozilla/dom/BrowsingContext.h"
+#include "mozilla/dom/CanonicalBrowsingContext.h"
+#include "mozilla/net/CookieJarSettings.h"
+#include "mozilla/LoadInfo.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/WindowGlobalParent.h"
+#include "mozilla/dom/WindowContext.h"
+#include "mozilla/net/NeckoChannelParams.h"
+#include "mozilla/PermissionManager.h"
+#include "mozIThirdPartyUtil.h"
+#include "nsEffectiveTLDService.h"
+#include "nsGlobalWindowInner.h"
+#include "nsIChannel.h"
+#include "nsICookieService.h"
+#include "nsIHttpChannel.h"
+#include "nsIPermission.h"
+#include "nsIURI.h"
+#include "nsNetUtil.h"
+#include "nsPIDOMWindow.h"
+#include "nsRFPService.h"
+#include "nsSandboxFlags.h"
+#include "nsScriptSecurityManager.h"
+#include "PartitioningExceptionList.h"
+
+#define ANTITRACKING_PERM_KEY "3rdPartyStorage"
+
+using namespace mozilla;
+using namespace mozilla::dom;
+
+/* static */ already_AddRefed<nsPIDOMWindowInner>
+AntiTrackingUtils::GetInnerWindow(BrowsingContext* aBrowsingContext) {
+ MOZ_ASSERT(aBrowsingContext);
+
+ nsCOMPtr<nsPIDOMWindowOuter> outer = aBrowsingContext->GetDOMWindow();
+ if (!outer) {
+ return nullptr;
+ }
+
+ nsCOMPtr<nsPIDOMWindowInner> inner = outer->GetCurrentInnerWindow();
+ return inner.forget();
+}
+
+/* static */ already_AddRefed<nsPIDOMWindowOuter>
+AntiTrackingUtils::GetTopWindow(nsPIDOMWindowInner* aWindow) {
+ Document* document = aWindow->GetExtantDoc();
+ if (!document) {
+ return nullptr;
+ }
+
+ nsIChannel* channel = document->GetChannel();
+ if (!channel) {
+ return nullptr;
+ }
+
+ nsCOMPtr<nsPIDOMWindowOuter> pwin =
+ aWindow->GetBrowsingContext()->Top()->GetDOMWindow();
+
+ if (!pwin) {
+ return nullptr;
+ }
+
+ return pwin.forget();
+}
+
+/* static */
+already_AddRefed<nsIURI> AntiTrackingUtils::MaybeGetDocumentURIBeingLoaded(
+ nsIChannel* aChannel) {
+ nsCOMPtr<nsIURI> uriBeingLoaded;
+ nsLoadFlags loadFlags = 0;
+ nsresult rv = aChannel->GetLoadFlags(&loadFlags);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return nullptr;
+ }
+ if (loadFlags & nsIChannel::LOAD_DOCUMENT_URI) {
+ // If the channel being loaded is a document channel, this call may be
+ // coming from an OnStopRequest notification, which might mean that our
+ // document may still be in the loading process, so we may need to pass in
+ // the uriBeingLoaded argument explicitly.
+ rv = NS_GetFinalChannelURI(aChannel, getter_AddRefs(uriBeingLoaded));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return nullptr;
+ }
+ }
+ return uriBeingLoaded.forget();
+}
+
+// static
+void AntiTrackingUtils::CreateStoragePermissionKey(
+ const nsACString& aTrackingOrigin, nsACString& aPermissionKey) {
+ MOZ_ASSERT(aPermissionKey.IsEmpty());
+
+ static const nsLiteralCString prefix =
+ nsLiteralCString(ANTITRACKING_PERM_KEY "^");
+
+ aPermissionKey.SetCapacity(prefix.Length() + aTrackingOrigin.Length());
+ aPermissionKey.Append(prefix);
+ aPermissionKey.Append(aTrackingOrigin);
+}
+
+// static
+bool AntiTrackingUtils::CreateStoragePermissionKey(nsIPrincipal* aPrincipal,
+ nsACString& aKey) {
+ if (!aPrincipal) {
+ return false;
+ }
+
+ nsAutoCString origin;
+ nsresult rv = aPrincipal->GetOriginNoSuffix(origin);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return false;
+ }
+
+ CreateStoragePermissionKey(origin, aKey);
+ return true;
+}
+
+// static
+bool AntiTrackingUtils::CreateStorageRequestPermissionKey(
+ nsIURI* aURI, nsACString& aPermissionKey) {
+ MOZ_ASSERT(aPermissionKey.IsEmpty());
+ RefPtr<nsEffectiveTLDService> eTLDService =
+ nsEffectiveTLDService::GetInstance();
+ if (!eTLDService) {
+ return false;
+ }
+ nsCString site;
+ nsresult rv = eTLDService->GetSite(aURI, site);
+ if (NS_FAILED(rv)) {
+ return false;
+ }
+ static const nsLiteralCString prefix =
+ nsLiteralCString("AllowStorageAccessRequest^");
+ aPermissionKey.SetCapacity(prefix.Length() + site.Length());
+ aPermissionKey.Append(prefix);
+ aPermissionKey.Append(site);
+ return true;
+}
+
+// static
+bool AntiTrackingUtils::IsStorageAccessPermission(nsIPermission* aPermission,
+ nsIPrincipal* aPrincipal) {
+ MOZ_ASSERT(aPermission);
+ MOZ_ASSERT(aPrincipal);
+
+ // The permission key may belong either to a tracking origin on the same
+ // origin as the granted origin, or on another origin as the granted origin
+ // (for example when a tracker in a third-party context uses window.open to
+ // open another origin where that second origin would be the granted origin.)
+ // But even in the second case, the type of the permission would still be
+ // formed by concatenating the granted origin to the end of the type name
+ // (see CreatePermissionKey). Therefore, we pass in the same argument to
+ // both tracking origin and granted origin here in order to compute the
+ // shorter permission key and will then do a prefix match on the type of the
+ // input permission to see if it is a storage access permission or not.
+ nsAutoCString permissionKey;
+ bool result = CreateStoragePermissionKey(aPrincipal, permissionKey);
+ if (NS_WARN_IF(!result)) {
+ return false;
+ }
+
+ nsAutoCString type;
+ nsresult rv = aPermission->GetType(type);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return false;
+ }
+
+ return StringBeginsWith(type, permissionKey);
+}
+
+// static
+Maybe<size_t> AntiTrackingUtils::CountSitesAllowStorageAccess(
+ nsIPrincipal* aPrincipal) {
+ PermissionManager* permManager = PermissionManager::GetInstance();
+ if (NS_WARN_IF(!permManager)) {
+ return Nothing();
+ }
+
+ nsAutoCString prefix;
+ AntiTrackingUtils::CreateStoragePermissionKey(aPrincipal, prefix);
+
+ using Permissions = nsTArray<RefPtr<nsIPermission>>;
+ Permissions perms;
+ nsresult rv = permManager->GetAllWithTypePrefix(prefix, perms);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return Nothing();
+ }
+
+ // Create a set of unique origins
+ using Origins = nsTArray<nsCString>;
+ Origins origins;
+
+ // Iterate over all permissions that have a prefix equal to the expected
+ // permission we are looking for. This includes two things we need to remove:
+ // duplicates and origin strings that have a prefix of aPrincipal's origin
+ // string, e.g. https://example.company when aPrincipal is
+ // https://example.com.
+ for (const auto& perm : perms) {
+ nsAutoCString type;
+ rv = perm->GetType(type);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return Nothing();
+ }
+ // Let's make sure that we're not looking at a permission for
+ // https://exampletracker.company when we mean to look for the
+ // permission for https://exampletracker.com!
+ if (type != prefix) {
+ continue;
+ }
+
+ nsCOMPtr<nsIPrincipal> principal;
+ rv = perm->GetPrincipal(getter_AddRefs(principal));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return Nothing();
+ }
+
+ nsAutoCString origin;
+ rv = principal->GetOrigin(origin);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return Nothing();
+ }
+
+ ToLowerCase(origin);
+
+ // Append if it would not be a duplicate
+ if (origins.IndexOf(origin) == Origins::NoIndex) {
+ origins.AppendElement(origin);
+ }
+ }
+
+ return Some(origins.Length());
+}
+
+// static
+bool AntiTrackingUtils::CheckStoragePermission(nsIPrincipal* aPrincipal,
+ const nsAutoCString& aType,
+ bool aIsInPrivateBrowsing,
+ uint32_t* aRejectedReason,
+ uint32_t aBlockedReason) {
+ PermissionManager* permManager = PermissionManager::GetInstance();
+ if (NS_WARN_IF(!permManager)) {
+ LOG(("Failed to obtain the permission manager"));
+ return false;
+ }
+
+ uint32_t result = 0;
+ if (aIsInPrivateBrowsing) {
+ LOG_PRIN(("Querying the permissions for private modei looking for a "
+ "permission of type %s for %s",
+ aType.get(), _spec),
+ aPrincipal);
+ if (!permManager->PermissionAvailable(aPrincipal, aType)) {
+ LOG(
+ ("Permission isn't available for this principal in the current "
+ "process"));
+ return false;
+ }
+ nsTArray<RefPtr<nsIPermission>> permissions;
+ nsresult rv = permManager->GetAllForPrincipal(aPrincipal, permissions);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ LOG(("Failed to get the list of permissions"));
+ return false;
+ }
+
+ bool found = false;
+ for (const auto& permission : permissions) {
+ if (!permission) {
+ LOG(("Couldn't get the permission for unknown reasons"));
+ continue;
+ }
+
+ nsAutoCString permissionType;
+ if (NS_SUCCEEDED(permission->GetType(permissionType)) &&
+ permissionType != aType) {
+ LOG(("Non-matching permission type: %s", aType.get()));
+ continue;
+ }
+
+ uint32_t capability = 0;
+ if (NS_SUCCEEDED(permission->GetCapability(&capability)) &&
+ capability != nsIPermissionManager::ALLOW_ACTION) {
+ LOG(("Non-matching permission capability: %d", capability));
+ continue;
+ }
+
+ uint32_t expirationType = 0;
+ if (NS_SUCCEEDED(permission->GetExpireType(&expirationType)) &&
+ expirationType != nsIPermissionManager ::EXPIRE_SESSION) {
+ LOG(("Non-matching permission expiration type: %d", expirationType));
+ continue;
+ }
+
+ int64_t expirationTime = 0;
+ if (NS_SUCCEEDED(permission->GetExpireTime(&expirationTime)) &&
+ expirationTime != 0) {
+ LOG(("Non-matching permission expiration time: %" PRId64,
+ expirationTime));
+ continue;
+ }
+
+ LOG(("Found a matching permission"));
+ found = true;
+ break;
+ }
+
+ if (!found) {
+ if (aRejectedReason) {
+ *aRejectedReason = aBlockedReason;
+ }
+ return false;
+ }
+ } else {
+ nsresult rv = permManager->TestPermissionWithoutDefaultsFromPrincipal(
+ aPrincipal, aType, &result);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ LOG(("Failed to test the permission"));
+ return false;
+ }
+
+ LOG_PRIN(
+ ("Testing permission type %s for %s resulted in %d (%s)", aType.get(),
+ _spec, int(result),
+ result == nsIPermissionManager::ALLOW_ACTION ? "success" : "failure"),
+ aPrincipal);
+
+ if (result != nsIPermissionManager::ALLOW_ACTION) {
+ if (aRejectedReason) {
+ *aRejectedReason = aBlockedReason;
+ }
+ return false;
+ }
+ }
+
+ return true;
+}
+
+/* static */
+nsILoadInfo::StoragePermissionState
+AntiTrackingUtils::GetStoragePermissionStateInParent(nsIChannel* aChannel) {
+ MOZ_ASSERT(aChannel);
+ MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess());
+
+ nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo();
+ nsCOMPtr<nsICookieJarSettings> cookieJarSettings;
+
+ auto policyType = loadInfo->GetExternalContentPolicyType();
+
+ // The channel is for the document load of the top-level window. The top-level
+ // window should always has 'hasStoragePermission' flag as false. So, we can
+ // return here directly.
+ if (policyType == ExtContentPolicy::TYPE_DOCUMENT) {
+ return nsILoadInfo::NoStoragePermission;
+ }
+
+ nsresult rv =
+ loadInfo->GetCookieJarSettings(getter_AddRefs(cookieJarSettings));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return nsILoadInfo::NoStoragePermission;
+ }
+
+ int32_t cookieBehavior = cookieJarSettings->GetCookieBehavior();
+
+ // We only need to check the storage permission if the cookie behavior is
+ // BEHAVIOR_REJECT_TRACKER, BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN or
+ // BEHAVIOR_REJECT_FOREIGN with exceptions. Because ContentBlocking wouldn't
+ // update or check the storage permission if the cookie behavior is not
+ // belongs to these three.
+ if (!net::CookieJarSettings::IsRejectThirdPartyContexts(cookieBehavior)) {
+ return nsILoadInfo::NoStoragePermission;
+ }
+
+ RefPtr<BrowsingContext> bc;
+ rv = loadInfo->GetTargetBrowsingContext(getter_AddRefs(bc));
+ if (NS_WARN_IF(NS_FAILED(rv)) || !bc) {
+ return nsILoadInfo::NoStoragePermission;
+ }
+
+ uint64_t targetWindowId = GetTopLevelAntiTrackingWindowId(bc);
+ nsCOMPtr<nsIPrincipal> targetPrincipal;
+
+ if (targetWindowId) {
+ RefPtr<WindowGlobalParent> wgp =
+ WindowGlobalParent::GetByInnerWindowId(targetWindowId);
+
+ if (NS_WARN_IF(!wgp)) {
+ return nsILoadInfo::NoStoragePermission;
+ }
+
+ targetPrincipal = wgp->DocumentPrincipal();
+ } else {
+ // We try to use the loading principal if there is no AntiTrackingWindowId.
+ targetPrincipal = loadInfo->GetLoadingPrincipal();
+ }
+
+ if (!targetPrincipal) {
+ nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(aChannel);
+
+ if (httpChannel) {
+ // We don't have a loading principal, let's see if this is a document
+ // channel which belongs to a top-level window.
+ bool isDocument = false;
+ rv = httpChannel->GetIsMainDocumentChannel(&isDocument);
+ if (NS_SUCCEEDED(rv) && isDocument) {
+ nsIScriptSecurityManager* ssm =
+ nsScriptSecurityManager::GetScriptSecurityManager();
+ Unused << ssm->GetChannelResultPrincipal(
+ aChannel, getter_AddRefs(targetPrincipal));
+ }
+ }
+ }
+
+ // Let's use the triggering principal if we still have nothing on the hand.
+ if (!targetPrincipal) {
+ targetPrincipal = loadInfo->TriggeringPrincipal();
+ }
+
+ // Cannot get the target principal, bail out.
+ if (NS_WARN_IF(!targetPrincipal)) {
+ return nsILoadInfo::NoStoragePermission;
+ }
+
+ nsCOMPtr<nsIURI> trackingURI;
+ rv = aChannel->GetURI(getter_AddRefs(trackingURI));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return nsILoadInfo::NoStoragePermission;
+ }
+
+ nsAutoCString trackingOrigin;
+ rv = nsContentUtils::GetASCIIOrigin(trackingURI, trackingOrigin);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return nsILoadInfo::NoStoragePermission;
+ }
+
+ if (IsThirdPartyChannel(aChannel)) {
+ nsAutoCString targetOrigin;
+ if (NS_FAILED(targetPrincipal->GetAsciiOrigin(targetOrigin))) {
+ return nsILoadInfo::NoStoragePermission;
+ }
+
+ if (PartitioningExceptionList::Check(targetOrigin, trackingOrigin)) {
+ return nsILoadInfo::StoragePermissionAllowListed;
+ }
+ }
+
+ nsAutoCString type;
+ AntiTrackingUtils::CreateStoragePermissionKey(trackingOrigin, type);
+
+ uint32_t unusedReason = 0;
+
+ if (AntiTrackingUtils::CheckStoragePermission(targetPrincipal, type,
+ NS_UsePrivateBrowsing(aChannel),
+ &unusedReason, unusedReason)) {
+ return nsILoadInfo::HasStoragePermission;
+ }
+
+ return nsILoadInfo::NoStoragePermission;
+}
+
+uint64_t AntiTrackingUtils::GetTopLevelAntiTrackingWindowId(
+ BrowsingContext* aBrowsingContext) {
+ MOZ_ASSERT(aBrowsingContext);
+
+ RefPtr<WindowContext> winContext =
+ aBrowsingContext->GetCurrentWindowContext();
+ if (!winContext || winContext->GetCookieBehavior().isNothing()) {
+ return 0;
+ }
+
+ // Do not check BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN her because when
+ // a third-party subresource is inside the main frame, we need to return the
+ // top-level window id to partition its cookies correctly.
+ uint32_t behavior = *winContext->GetCookieBehavior();
+ if (behavior == nsICookieService::BEHAVIOR_REJECT_TRACKER &&
+ aBrowsingContext->IsTop()) {
+ return 0;
+ }
+
+ return aBrowsingContext->Top()->GetCurrentInnerWindowId();
+}
+
+uint64_t AntiTrackingUtils::GetTopLevelStorageAreaWindowId(
+ BrowsingContext* aBrowsingContext) {
+ MOZ_ASSERT(aBrowsingContext);
+
+ if (Document::StorageAccessSandboxed(aBrowsingContext->GetSandboxFlags())) {
+ return 0;
+ }
+
+ BrowsingContext* parentBC = aBrowsingContext->GetParent();
+ if (!parentBC) {
+ // No parent browsing context available!
+ return 0;
+ }
+
+ if (!parentBC->IsTop()) {
+ return 0;
+ }
+
+ return parentBC->GetCurrentInnerWindowId();
+}
+
+/* static */
+already_AddRefed<nsIPrincipal> AntiTrackingUtils::GetPrincipal(
+ BrowsingContext* aBrowsingContext) {
+ MOZ_ASSERT(aBrowsingContext);
+
+ nsCOMPtr<nsIPrincipal> principal;
+
+ if (XRE_IsContentProcess()) {
+ // Passing an out-of-process browsing context in child processes to
+ // this API won't get any result, so just assert.
+ MOZ_ASSERT(aBrowsingContext->IsInProcess());
+
+ nsPIDOMWindowOuter* outer = aBrowsingContext->GetDOMWindow();
+ if (NS_WARN_IF(!outer)) {
+ return nullptr;
+ }
+
+ nsPIDOMWindowInner* inner = outer->GetCurrentInnerWindow();
+ if (NS_WARN_IF(!inner)) {
+ return nullptr;
+ }
+
+ principal = nsGlobalWindowInner::Cast(inner)->GetPrincipal();
+ } else {
+ WindowGlobalParent* wgp =
+ aBrowsingContext->Canonical()->GetCurrentWindowGlobal();
+ if (NS_WARN_IF(!wgp)) {
+ return nullptr;
+ }
+
+ principal = wgp->DocumentPrincipal();
+ }
+ return principal.forget();
+}
+
+/* static */
+bool AntiTrackingUtils::GetPrincipalAndTrackingOrigin(
+ BrowsingContext* aBrowsingContext, nsIPrincipal** aPrincipal,
+ nsACString& aTrackingOrigin) {
+ MOZ_ASSERT(aBrowsingContext);
+
+ // Passing an out-of-process browsing context in child processes to
+ // this API won't get any result, so just assert.
+ MOZ_ASSERT_IF(XRE_IsContentProcess(), aBrowsingContext->IsInProcess());
+
+ // Let's take the principal and the origin of the tracker.
+ nsCOMPtr<nsIPrincipal> principal =
+ AntiTrackingUtils::GetPrincipal(aBrowsingContext);
+ if (NS_WARN_IF(!principal)) {
+ return false;
+ }
+
+ nsresult rv = principal->GetOriginNoSuffix(aTrackingOrigin);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return false;
+ }
+
+ if (aPrincipal) {
+ principal.forget(aPrincipal);
+ }
+
+ return true;
+};
+
+/* static */
+uint32_t AntiTrackingUtils::GetCookieBehavior(
+ BrowsingContext* aBrowsingContext) {
+ MOZ_ASSERT(aBrowsingContext);
+
+ RefPtr<dom::WindowContext> win = aBrowsingContext->GetCurrentWindowContext();
+ if (!win || win->GetCookieBehavior().isNothing()) {
+ return nsICookieService::BEHAVIOR_REJECT;
+ }
+
+ return *win->GetCookieBehavior();
+}
+
+/* static */
+already_AddRefed<WindowGlobalParent>
+AntiTrackingUtils::GetTopWindowExcludingExtensionAccessibleContentFrames(
+ CanonicalBrowsingContext* aBrowsingContext, nsIURI* aURIBeingLoaded) {
+ MOZ_ASSERT(XRE_IsParentProcess());
+ MOZ_ASSERT(aBrowsingContext);
+
+ CanonicalBrowsingContext* bc = aBrowsingContext;
+ RefPtr<WindowGlobalParent> prev;
+ while (RefPtr<WindowGlobalParent> parent = bc->GetParentWindowContext()) {
+ CanonicalBrowsingContext* parentBC = parent->BrowsingContext();
+
+ nsIPrincipal* parentPrincipal = parent->DocumentPrincipal();
+ nsIURI* uri = prev ? prev->GetDocumentURI() : aURIBeingLoaded;
+
+ // If the new parent has permission to load the current page, we're
+ // at a moz-extension:// frame which has a host permission that allows
+ // it to load the document that we've loaded. In that case, stop at
+ // this frame and consider it the top-level frame.
+ if (uri &&
+ BasePrincipal::Cast(parentPrincipal)->AddonAllowsLoad(uri, true)) {
+ break;
+ }
+
+ bc = parentBC;
+ prev = parent;
+ }
+ if (!prev) {
+ prev = bc->GetCurrentWindowGlobal();
+ }
+ return prev.forget();
+}
+
+/* static */
+void AntiTrackingUtils::ComputeIsThirdPartyToTopWindow(nsIChannel* aChannel) {
+ MOZ_ASSERT(aChannel);
+ MOZ_ASSERT(XRE_IsParentProcess());
+
+ nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo();
+
+ // When a top-level load is opened by window.open, BrowsingContext from
+ // LoadInfo is its opener, which may make the third-party caculation code
+ // below returns an incorrect result. So we use TYPE_DOCUMENT to
+ // ensure a top-level load is not considered 3rd-party.
+ auto policyType = loadInfo->GetExternalContentPolicyType();
+ if (policyType == ExtContentPolicy::TYPE_DOCUMENT) {
+ loadInfo->SetIsThirdPartyContextToTopWindow(false);
+ return;
+ }
+
+ RefPtr<BrowsingContext> bc;
+ loadInfo->GetBrowsingContext(getter_AddRefs(bc));
+
+ nsCOMPtr<nsIURI> uri;
+ Unused << aChannel->GetURI(getter_AddRefs(uri));
+
+ // In some cases we don't have a browsingContext. For example, in xpcshell
+ // tests, channels that are used to download images and channels for loading
+ // worker script.
+ if (!bc) {
+ // If the flag was set before, we don't need to compute again. This could
+ // happen for the channels used to load worker scripts.
+ //
+ // Note that we cannot stop computing the flag in general even it has set
+ // before because sometimes we need to get the up-to-date flag, e.g.
+ // redirects.
+ if (static_cast<net::LoadInfo*>(loadInfo.get())
+ ->HasIsThirdPartyContextToTopWindowSet()) {
+ return;
+ }
+
+ // We turn to check the loading principal if there is no browsing context.
+ auto* loadingPrincipal =
+ BasePrincipal::Cast(loadInfo->GetLoadingPrincipal());
+
+ if (uri && loadingPrincipal) {
+ bool isThirdParty = true;
+ nsresult rv = loadingPrincipal->IsThirdPartyURI(uri, &isThirdParty);
+
+ if (NS_SUCCEEDED(rv)) {
+ loadInfo->SetIsThirdPartyContextToTopWindow(isThirdParty);
+ }
+ }
+ return;
+ }
+
+ RefPtr<WindowGlobalParent> topWindow =
+ GetTopWindowExcludingExtensionAccessibleContentFrames(bc->Canonical(),
+ uri);
+
+ if (NS_WARN_IF(!topWindow)) {
+ return;
+ }
+
+ nsCOMPtr<nsIPrincipal> topWindowPrincipal = topWindow->DocumentPrincipal();
+ if (topWindowPrincipal && !topWindowPrincipal->GetIsNullPrincipal()) {
+ auto* basePrin = BasePrincipal::Cast(topWindowPrincipal);
+ bool isThirdParty = true;
+
+ // For about:blank and about:srcdoc, we can't just compare uri to determine
+ // whether the page is third-party, so we use channel result principal
+ // instead. By doing this, an the resource inherits the principal from
+ // its parent is considered not a third-party.
+ if (NS_IsAboutBlank(uri) || NS_IsAboutSrcdoc(uri)) {
+ nsIScriptSecurityManager* ssm = nsContentUtils::GetSecurityManager();
+ if (NS_WARN_IF(!ssm)) {
+ return;
+ }
+
+ nsCOMPtr<nsIPrincipal> principal;
+ nsresult rv =
+ ssm->GetChannelResultPrincipal(aChannel, getter_AddRefs(principal));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+
+ basePrin->IsThirdPartyPrincipal(principal, &isThirdParty);
+ } else {
+ basePrin->IsThirdPartyURI(uri, &isThirdParty);
+ }
+
+ loadInfo->SetIsThirdPartyContextToTopWindow(isThirdParty);
+ }
+}
+
+/* static */
+bool AntiTrackingUtils::IsThirdPartyChannel(nsIChannel* aChannel) {
+ MOZ_ASSERT(aChannel);
+
+ // We only care whether the channel is 3rd-party with respect to
+ // the top-level.
+ nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo();
+ return loadInfo->GetIsThirdPartyContextToTopWindow();
+}
+
+/* static */
+bool AntiTrackingUtils::IsThirdPartyWindow(nsPIDOMWindowInner* aWindow,
+ nsIURI* aURI) {
+ MOZ_ASSERT(aWindow);
+
+ // We assume that the window is foreign to the URI by default.
+ bool thirdParty = true;
+
+ // We will skip checking URIs for about:blank and about:srcdoc because they
+ // have no domain. So, comparing them will always fail.
+ if (aURI && !NS_IsAboutBlank(aURI) && !NS_IsAboutSrcdoc(aURI)) {
+ nsCOMPtr<nsIScriptObjectPrincipal> scriptObjPrin =
+ do_QueryInterface(aWindow);
+ if (!scriptObjPrin) {
+ return thirdParty;
+ }
+
+ nsCOMPtr<nsIPrincipal> prin = scriptObjPrin->GetPrincipal();
+ if (!prin) {
+ return thirdParty;
+ }
+
+ // Determine whether aURI is foreign with respect to the current principal.
+ nsresult rv = prin->IsThirdPartyURI(aURI, &thirdParty);
+ if (NS_FAILED(rv)) {
+ return thirdParty;
+ }
+
+ if (thirdParty) {
+ return thirdParty;
+ }
+ }
+
+ RefPtr<Document> doc = aWindow->GetDoc();
+ if (!doc) {
+ // If we can't get the document from the window, ex, about:blank, fallback
+ // to use IsThirdPartyWindow check that examine the whole hierarchy.
+ nsCOMPtr<mozIThirdPartyUtil> thirdPartyUtil =
+ components::ThirdPartyUtil::Service();
+ Unused << thirdPartyUtil->IsThirdPartyWindow(aWindow->GetOuterWindow(),
+ nullptr, &thirdParty);
+ return thirdParty;
+ }
+
+ return IsThirdPartyDocument(doc);
+}
+
+/* static */
+bool AntiTrackingUtils::IsThirdPartyDocument(Document* aDocument) {
+ MOZ_ASSERT(aDocument);
+ if (!aDocument->GetChannel()) {
+ // If we can't get the channel from the document, i.e. initial about:blank
+ // page, we use the browsingContext of the document to check if it's in the
+ // third-party context. If the browsing context is still not available, we
+ // will treat the window as third-party.
+ RefPtr<BrowsingContext> bc = aDocument->GetBrowsingContext();
+ return bc ? IsThirdPartyContext(bc) : true;
+ }
+
+ // We only care whether the channel is 3rd-party with respect to
+ // the top-level.
+ nsCOMPtr<nsILoadInfo> loadInfo = aDocument->GetChannel()->LoadInfo();
+ return loadInfo->GetIsThirdPartyContextToTopWindow();
+}
+
+/* static */
+bool AntiTrackingUtils::IsThirdPartyContext(BrowsingContext* aBrowsingContext) {
+ MOZ_ASSERT(aBrowsingContext);
+ MOZ_ASSERT(aBrowsingContext->IsInProcess());
+
+ if (aBrowsingContext->IsTopContent()) {
+ return false;
+ }
+
+ // If the top browsing context is not in the same process, it's cross-origin.
+ if (!aBrowsingContext->Top()->IsInProcess()) {
+ return true;
+ }
+
+ nsIDocShell* docShell = aBrowsingContext->GetDocShell();
+ if (!docShell) {
+ return true;
+ }
+ Document* doc = docShell->GetExtantDocument();
+ if (!doc) {
+ return true;
+ }
+ nsIPrincipal* principal = doc->NodePrincipal();
+
+ nsIDocShell* topDocShell = aBrowsingContext->Top()->GetDocShell();
+ if (!topDocShell) {
+ return true;
+ }
+ Document* topDoc = topDocShell->GetDocument();
+ if (!topDoc) {
+ return true;
+ }
+ nsIPrincipal* topPrincipal = topDoc->NodePrincipal();
+
+ auto* topBasePrin = BasePrincipal::Cast(topPrincipal);
+ bool isThirdParty = true;
+
+ topBasePrin->IsThirdPartyPrincipal(principal, &isThirdParty);
+
+ return isThirdParty;
+}
+
+/* static */
+nsCString AntiTrackingUtils::GrantedReasonToString(
+ ContentBlockingNotifier::StorageAccessPermissionGrantedReason aReason) {
+ switch (aReason) {
+ case ContentBlockingNotifier::eOpener:
+ return "opener"_ns;
+ case ContentBlockingNotifier::eOpenerAfterUserInteraction:
+ return "user interaction"_ns;
+ default:
+ return "stroage access API"_ns;
+ }
+}
+
+/* static */
+void AntiTrackingUtils::UpdateAntiTrackingInfoForChannel(nsIChannel* aChannel) {
+ MOZ_ASSERT(aChannel);
+
+ if (!XRE_IsParentProcess()) {
+ return;
+ }
+
+ MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess());
+
+ AntiTrackingUtils::ComputeIsThirdPartyToTopWindow(aChannel);
+
+ nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo();
+
+ Unused << loadInfo->SetStoragePermission(
+ AntiTrackingUtils::GetStoragePermissionStateInParent(aChannel));
+
+ // We only update the IsOnContentBlockingAllowList flag and the partition key
+ // for the top-level http channel.
+ //
+ // The IsOnContentBlockingAllowList is only for http. For other types of
+ // channels, such as 'file:', there will be no interface to modify this. So,
+ // we only update it in http channels.
+ //
+ // The partition key is computed based on the site, so it's no point to set it
+ // for channels other than http channels.
+ nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(aChannel);
+ if (!httpChannel || loadInfo->GetExternalContentPolicyType() !=
+ ExtContentPolicy::TYPE_DOCUMENT) {
+ return;
+ }
+
+ nsCOMPtr<nsICookieJarSettings> cookieJarSettings;
+ Unused << loadInfo->GetCookieJarSettings(getter_AddRefs(cookieJarSettings));
+
+ // Update the IsOnContentBlockingAllowList flag in the CookieJarSettings
+ // if this is a top level loading. For sub-document loading, this flag
+ // would inherit from the parent.
+ net::CookieJarSettings::Cast(cookieJarSettings)
+ ->UpdateIsOnContentBlockingAllowList(aChannel);
+
+ // We only need to set FPD for top-level loads. FPD will automatically be
+ // propagated to non-top level loads via CookieJarSetting.
+ nsCOMPtr<nsIURI> uri;
+ Unused << aChannel->GetURI(getter_AddRefs(uri));
+ net::CookieJarSettings::Cast(cookieJarSettings)->SetPartitionKey(uri);
+
+ // Generate the fingerprinting randomization key for top-level loads. The key
+ // will automatically be propagated to sub loads.
+ auto RFPRandomKey =
+ nsRFPService::GenerateKey(uri, NS_UsePrivateBrowsing(aChannel));
+ if (RFPRandomKey) {
+ net::CookieJarSettings::Cast(cookieJarSettings)
+ ->SetFingerprintingRandomizationKey(RFPRandomKey.ref());
+ }
+}
diff --git a/toolkit/components/antitracking/AntiTrackingUtils.h b/toolkit/components/antitracking/AntiTrackingUtils.h
new file mode 100644
index 0000000000..07bd39257a
--- /dev/null
+++ b/toolkit/components/antitracking/AntiTrackingUtils.h
@@ -0,0 +1,160 @@
+/* -*- 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_antitrackingutils_h
+#define mozilla_antitrackingutils_h
+
+#include "mozilla/AlreadyAddRefed.h"
+#include "mozilla/Maybe.h"
+#include "nsStringFwd.h"
+#include "ContentBlockingNotifier.h"
+
+#include "nsILoadInfo.h"
+
+class nsPIDOMWindowInner;
+class nsPIDOMWindowOuter;
+class nsIChannel;
+class nsIPermission;
+class nsIPrincipal;
+class nsIURI;
+
+namespace mozilla {
+namespace dom {
+class BrowsingContext;
+class CanonicalBrowsingContext;
+class Document;
+class WindowGlobalParent;
+} // namespace dom
+
+class AntiTrackingUtils final {
+ public:
+ static already_AddRefed<nsPIDOMWindowInner> GetInnerWindow(
+ dom::BrowsingContext* aBrowsingContext);
+
+ static already_AddRefed<nsPIDOMWindowOuter> GetTopWindow(
+ nsPIDOMWindowInner* aWindow);
+
+ // Get the current document URI from a document channel as it is being loaded.
+ static already_AddRefed<nsIURI> MaybeGetDocumentURIBeingLoaded(
+ nsIChannel* aChannel);
+
+ static void CreateStoragePermissionKey(const nsACString& aTrackingOrigin,
+ nsACString& aPermissionKey);
+
+ // Given a principal, returns the storage permission key that will be used for
+ // the principal. Returns true on success.
+ static bool CreateStoragePermissionKey(nsIPrincipal* aPrincipal,
+ nsACString& aKey);
+
+ // Given and embedded URI, returns the permission for allowing storage access
+ // requests from that URI's site. This permission is site-scoped in two ways:
+ // the principal it is stored under and the suffix built from aURI are both
+ // the Site rather than Origin.
+ static bool CreateStorageRequestPermissionKey(nsIURI* aURI,
+ nsACString& aPermissionKey);
+
+ // Returns true if the permission passed in is a storage access permission
+ // for the passed in principal argument.
+ static bool IsStorageAccessPermission(nsIPermission* aPermission,
+ nsIPrincipal* aPrincipal);
+
+ // Returns true if the storage permission is granted for the given principal
+ // and the storage permission key.
+ static bool CheckStoragePermission(nsIPrincipal* aPrincipal,
+ const nsAutoCString& aType,
+ bool aIsInPrivateBrowsing,
+ uint32_t* aRejectedReason,
+ uint32_t aBlockedReason);
+
+ // Returns the number of sites that give this principal's origin storage
+ // access.
+ static Maybe<size_t> CountSitesAllowStorageAccess(nsIPrincipal* aPrincipal);
+
+ // Returns the storage permission state for the given channel. And this is
+ // meant to be called in the parent process. This only reflects the fact that
+ // whether the channel has the storage permission. It doesn't take the window
+ // hierarchy into account. i.e. this will return
+ // nsILoadInfo::HasStoragePermission even for a nested iframe that has storage
+ // permission.
+ static nsILoadInfo::StoragePermissionState GetStoragePermissionStateInParent(
+ nsIChannel* aChannel);
+
+ // Returns the toplevel inner window id, returns 0 if this is a toplevel
+ // window.
+ static uint64_t GetTopLevelAntiTrackingWindowId(
+ dom::BrowsingContext* aBrowsingContext);
+
+ // Returns the parent inner window id, returns 0 if this or the parent are not
+ // a toplevel window. This is mainly used to determine the anti-tracking
+ // storage area.
+ static uint64_t GetTopLevelStorageAreaWindowId(
+ dom::BrowsingContext* aBrowsingContext);
+
+ // Returns the principal of the given browsing context.
+ // This API should only be used either in child processes with an in-process
+ // browsing context or in the parent process.
+ static already_AddRefed<nsIPrincipal> GetPrincipal(
+ dom::BrowsingContext* aBrowsingContext);
+
+ // Returns the principal of the given browsing context and tracking origin.
+ // This API should only be used either in child processes with an in-process
+ // browsing context or in the parent process.
+ static bool GetPrincipalAndTrackingOrigin(
+ dom::BrowsingContext* aBrowsingContext, nsIPrincipal** aPrincipal,
+ nsACString& aTrackingOrigin);
+
+ // Retruns the cookie behavior of the given browsingContext,
+ // return BEHAVIOR_REJECT when fail.
+ static uint32_t GetCookieBehavior(dom::BrowsingContext* aBrowsingContext);
+
+ // Returns the top-level global window parent. But we would stop at the
+ // content window which is loaded by addons and consider this window as a top.
+ //
+ // Note that this is the parent-process implementation of
+ // nsGlobalWindowOuter::GetTopExcludingExtensionAccessibleContentFrames
+ static already_AddRefed<dom::WindowGlobalParent>
+ GetTopWindowExcludingExtensionAccessibleContentFrames(
+ dom::CanonicalBrowsingContext* aBrowsingContext, nsIURI* aURIBeingLoaded);
+
+ // Given a channel, compute and set the IsThirdPartyContextToTopWindow for
+ // this channel. This function is supposed to be called in the parent process.
+ static void ComputeIsThirdPartyToTopWindow(nsIChannel* aChannel);
+
+ // Given a channel, this function determines if this channel is a third party.
+ // Note that this function also considers the top-level window. The channel
+ // will be considered as a third party only when it's a third party to both
+ // its parent and the top-level window.
+ static bool IsThirdPartyChannel(nsIChannel* aChannel);
+
+ // Given a window and a URI, this function first determines if the window is
+ // third-party with respect to the URI. The function returns if it's true.
+ // Otherwise, it will continue to check if the window is third-party.
+ static bool IsThirdPartyWindow(nsPIDOMWindowInner* aWindow, nsIURI* aURI);
+
+ // Given a Document, this function determines if this document
+ // is considered as a third party with respect to the top level context.
+ // This prefers to use the document's Channel's LoadInfo, but falls back to
+ // the BrowsingContext.
+ static bool IsThirdPartyDocument(dom::Document* aDocument);
+
+ // Given a browsing context, this function determines if this browsing context
+ // is considered as a third party in respect to the top-level context.
+ static bool IsThirdPartyContext(dom::BrowsingContext* aBrowsingContext);
+
+ static nsCString GrantedReasonToString(
+ ContentBlockingNotifier::StorageAccessPermissionGrantedReason aReason);
+
+ /**
+ * This function updates all the fields used by anti-tracking when a channel
+ * is opened. We have to do this in the parent to access cross-origin info
+ * that is not exposed to child processes.
+ */
+ static void UpdateAntiTrackingInfoForChannel(nsIChannel* aChannel);
+};
+
+} // namespace mozilla
+
+#endif // mozilla_antitrackingutils_h
diff --git a/toolkit/components/antitracking/ContentBlockingAllowList.cpp b/toolkit/components/antitracking/ContentBlockingAllowList.cpp
new file mode 100644
index 0000000000..25806cd1e7
--- /dev/null
+++ b/toolkit/components/antitracking/ContentBlockingAllowList.cpp
@@ -0,0 +1,257 @@
+/* -*- 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 "AntiTrackingLog.h"
+#include "ContentBlockingAllowList.h"
+
+#include "mozilla/dom/BrowsingContext.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/BasePrincipal.h"
+#include "mozilla/PermissionManager.h"
+#include "mozilla/ScopeExit.h"
+#include "nsContentUtils.h"
+#include "nsGlobalWindowInner.h"
+#include "nsICookieJarSettings.h"
+#include "nsIHttpChannel.h"
+#include "nsIHttpChannelInternal.h"
+#include "nsNetUtil.h"
+
+using namespace mozilla;
+
+NS_IMPL_ISUPPORTS(ContentBlockingAllowList, nsIContentBlockingAllowList)
+
+NS_IMETHODIMP
+// Wrapper for the static ContentBlockingAllowList::ComputePrincipal method
+ContentBlockingAllowList::ComputeContentBlockingAllowListPrincipal(
+ nsIPrincipal* aDocumentPrincipal, nsIPrincipal** aPrincipal) {
+ NS_ENSURE_ARG_POINTER(aDocumentPrincipal);
+ NS_ENSURE_ARG_POINTER(aPrincipal);
+
+ nsCOMPtr<nsIPrincipal> principal;
+ ContentBlockingAllowList::ComputePrincipal(aDocumentPrincipal,
+ getter_AddRefs(principal));
+
+ NS_ENSURE_TRUE(principal, NS_ERROR_FAILURE);
+
+ principal.forget(aPrincipal);
+
+ return NS_OK;
+}
+
+/* static */ bool ContentBlockingAllowList::Check(
+ nsIPrincipal* aTopWinPrincipal, bool aIsPrivateBrowsing) {
+ bool isAllowed = false;
+ nsresult rv = Check(aTopWinPrincipal, aIsPrivateBrowsing, isAllowed);
+ if (NS_SUCCEEDED(rv) && isAllowed) {
+ LOG(
+ ("The top-level window is on the content blocking allow list, "
+ "bail out early"));
+ return true;
+ }
+ if (NS_FAILED(rv)) {
+ LOG(("Checking the content blocking allow list for failed with %" PRIx32,
+ static_cast<uint32_t>(rv)));
+ }
+ return false;
+}
+
+/* static */ bool ContentBlockingAllowList::Check(
+ nsICookieJarSettings* aCookieJarSettings) {
+ if (!aCookieJarSettings) {
+ LOG(
+ ("Could not check the content blocking allow list because the cookie "
+ "jar settings wasn't available"));
+ return false;
+ }
+
+ return aCookieJarSettings->GetIsOnContentBlockingAllowList();
+}
+
+/* static */ bool ContentBlockingAllowList::Check(nsPIDOMWindowInner* aWindow) {
+ // TODO: this is a quick fix to ensure that we allow storage permission for
+ // a chrome window. We should check if there is a better way to do this in
+ // Bug 1626223.
+ if (nsGlobalWindowInner::Cast(aWindow)->GetPrincipal() ==
+ nsContentUtils::GetSystemPrincipal()) {
+ return true;
+ }
+
+ // We can check the IsOnContentBlockingAllowList flag in the document's
+ // CookieJarSettings. Because this flag represents the fact that whether the
+ // top-level document is on the content blocking allow list. And this flag was
+ // propagated from the top-level as the CookieJarSettings inherits from the
+ // parent.
+ RefPtr<dom::Document> doc = nsGlobalWindowInner::Cast(aWindow)->GetDocument();
+
+ if (!doc) {
+ LOG(
+ ("Could not check the content blocking allow list because the document "
+ "wasn't available"));
+ return false;
+ }
+
+ nsCOMPtr<nsICookieJarSettings> cookieJarSettings = doc->CookieJarSettings();
+
+ return ContentBlockingAllowList::Check(cookieJarSettings);
+}
+
+/* static */ bool ContentBlockingAllowList::Check(nsIHttpChannel* aChannel) {
+ nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo();
+ nsCOMPtr<nsICookieJarSettings> cookieJarSettings;
+
+ Unused << loadInfo->GetCookieJarSettings(getter_AddRefs(cookieJarSettings));
+
+ return ContentBlockingAllowList::Check(cookieJarSettings);
+}
+
+nsresult ContentBlockingAllowList::Check(
+ nsIPrincipal* aContentBlockingAllowListPrincipal, bool aIsPrivateBrowsing,
+ bool& aIsAllowListed) {
+ MOZ_ASSERT(XRE_IsParentProcess());
+ aIsAllowListed = false;
+
+ if (!aContentBlockingAllowListPrincipal) {
+ // Nothing to do!
+ return NS_OK;
+ }
+
+ LOG_PRIN(("Deciding whether the user has overridden content blocking for %s",
+ _spec),
+ aContentBlockingAllowListPrincipal);
+
+ PermissionManager* permManager = PermissionManager::GetInstance();
+ NS_ENSURE_TRUE(permManager, NS_ERROR_FAILURE);
+
+ // Check both the normal mode and private browsing mode user override
+ // permissions.
+ std::pair<const nsLiteralCString, bool> types[] = {
+ {"trackingprotection"_ns, false}, {"trackingprotection-pb"_ns, true}};
+
+ for (const auto& type : types) {
+ if (aIsPrivateBrowsing != type.second) {
+ continue;
+ }
+
+ uint32_t permissions = nsIPermissionManager::UNKNOWN_ACTION;
+ nsresult rv = permManager->TestPermissionFromPrincipal(
+ aContentBlockingAllowListPrincipal, type.first, &permissions);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (permissions == nsIPermissionManager::ALLOW_ACTION) {
+ aIsAllowListed = true;
+ LOG(("Found user override type %s", type.first.get()));
+ // Stop checking the next permisson type if we decided to override.
+ break;
+ }
+ }
+
+ if (!aIsAllowListed) {
+ LOG(("No user override found"));
+ }
+
+ return NS_OK;
+}
+
+/* static */ void ContentBlockingAllowList::ComputePrincipal(
+ nsIPrincipal* aDocumentPrincipal, nsIPrincipal** aPrincipal) {
+ MOZ_ASSERT(aPrincipal);
+
+ auto returnInputArgument =
+ MakeScopeExit([&] { NS_IF_ADDREF(*aPrincipal = aDocumentPrincipal); });
+
+ BasePrincipal* bp = BasePrincipal::Cast(aDocumentPrincipal);
+ if (!bp || !bp->IsContentPrincipal()) {
+ // If we have something other than a content principal, just return what we
+ // have. This includes the case where we were passed a nullptr.
+ return;
+ }
+
+ if (aDocumentPrincipal->SchemeIs("chrome") ||
+ aDocumentPrincipal->SchemeIs("about")) {
+ returnInputArgument.release();
+ *aPrincipal = nullptr;
+ return;
+ }
+
+ // Take the host/port portion so we can allowlist by site. Also ignore the
+ // scheme, since users who put sites on the allowlist probably don't expect
+ // allowlisting to depend on scheme.
+ nsAutoCString escaped("https://"_ns);
+ nsAutoCString temp;
+ nsresult rv = aDocumentPrincipal->GetHostPort(temp);
+ // view-source URIs will be handled by the next block.
+ if (NS_FAILED(rv) && !aDocumentPrincipal->SchemeIs("view-source")) {
+ // Normal for some loads, no need to print a warning
+ return;
+ }
+
+ // GetHostPort returns an empty string (with a success error code) for file://
+ // URIs.
+ if (temp.IsEmpty()) {
+ // In this case we want to make sure that our allow list principal would be
+ // computed as null.
+ returnInputArgument.release();
+ *aPrincipal = nullptr;
+ return;
+ }
+ escaped.Append(temp);
+ nsCOMPtr<nsIURI> uri;
+ rv = NS_NewURI(getter_AddRefs(uri), escaped);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+
+ nsCOMPtr<nsIPrincipal> principal = BasePrincipal::CreateContentPrincipal(
+ uri, aDocumentPrincipal->OriginAttributesRef());
+ if (NS_WARN_IF(!principal)) {
+ return;
+ }
+
+ returnInputArgument.release();
+ principal.forget(aPrincipal);
+}
+
+/* static */ void ContentBlockingAllowList::RecomputePrincipal(
+ nsIURI* aURIBeingLoaded, const OriginAttributes& aAttrs,
+ nsIPrincipal** aPrincipal) {
+ MOZ_ASSERT(aPrincipal);
+
+ auto returnInputArgument = MakeScopeExit([&] { *aPrincipal = nullptr; });
+
+ // Take the host/port portion so we can allowlist by site. Also ignore the
+ // scheme, since users who put sites on the allowlist probably don't expect
+ // allowlisting to depend on scheme.
+ nsAutoCString escaped("https://"_ns);
+ nsAutoCString temp;
+ nsresult rv = aURIBeingLoaded->GetHostPort(temp);
+ // view-source URIs will be handled by the next block.
+ if (NS_FAILED(rv) && !aURIBeingLoaded->SchemeIs("view-source")) {
+ // Normal for some loads, no need to print a warning
+ return;
+ }
+
+ // GetHostPort returns an empty string (with a success error code) for file://
+ // URIs.
+ if (temp.IsEmpty()) {
+ return;
+ }
+ escaped.Append(temp);
+
+ nsCOMPtr<nsIURI> uri;
+ rv = NS_NewURI(getter_AddRefs(uri), escaped);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+
+ nsCOMPtr<nsIPrincipal> principal =
+ BasePrincipal::CreateContentPrincipal(uri, aAttrs);
+ if (NS_WARN_IF(!principal)) {
+ return;
+ }
+
+ returnInputArgument.release();
+ principal.forget(aPrincipal);
+}
diff --git a/toolkit/components/antitracking/ContentBlockingAllowList.h b/toolkit/components/antitracking/ContentBlockingAllowList.h
new file mode 100644
index 0000000000..2601173db7
--- /dev/null
+++ b/toolkit/components/antitracking/ContentBlockingAllowList.h
@@ -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/. */
+
+#ifndef mozilla_contentblockingallowlist_h
+#define mozilla_contentblockingallowlist_h
+
+#include "mozilla/dom/BrowsingContext.h"
+#include "nsIContentBlockingAllowList.h"
+
+class nsICookieJarSettings;
+class nsIHttpChannel;
+class nsIPrincipal;
+class nsIURI;
+class nsPIDOMWindowInner;
+
+namespace mozilla {
+
+class OriginAttributes;
+struct ContentBlockingAllowListCache;
+
+class ContentBlockingAllowList final : public nsIContentBlockingAllowList {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSICONTENTBLOCKINGALLOWLIST
+ // Check whether a principal is on the content blocking allow list.
+ // aPrincipal should be a "content blocking allow list principal".
+ // This principal can be obtained from the load info object for top-level
+ // windows.
+ static nsresult Check(nsIPrincipal* aContentBlockingAllowListPrincipal,
+ bool aIsPrivateBrowsing, bool& aIsAllowListed);
+
+ static bool Check(nsIHttpChannel* aChannel);
+ // Utility APIs for ContentBlocking.
+ static bool Check(nsPIDOMWindowInner* aWindow);
+ static bool Check(nsIPrincipal* aTopWinPrincipal, bool aIsPrivateBrowsing);
+ static bool Check(nsICookieJarSettings* aCookieJarSettings);
+
+ // Computes the principal used to check the content blocking allow list for a
+ // top-level document based on the document principal. This function is used
+ // right after setting up the document principal.
+ static void ComputePrincipal(nsIPrincipal* aDocumentPrincipal,
+ nsIPrincipal** aPrincipal);
+
+ static void RecomputePrincipal(nsIURI* aURIBeingLoaded,
+ const OriginAttributes& aAttrs,
+ nsIPrincipal** aPrincipal);
+
+ private:
+ ~ContentBlockingAllowList() = default;
+};
+
+} // namespace mozilla
+
+#endif // mozilla_contentblockingallowlist_h
diff --git a/toolkit/components/antitracking/ContentBlockingAllowList.sys.mjs b/toolkit/components/antitracking/ContentBlockingAllowList.sys.mjs
new file mode 100644
index 0000000000..af1028083c
--- /dev/null
+++ b/toolkit/components/antitracking/ContentBlockingAllowList.sys.mjs
@@ -0,0 +1,111 @@
+/* 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/. */
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
+});
+
+/**
+ * A helper module to manage the Content Blocking Allow List.
+ *
+ * This module provides a couple of utility APIs for adding or
+ * removing a given browser object to the Content Blocking allow
+ * list.
+ */
+export const ContentBlockingAllowList = {
+ _observingLastPBContext: false,
+
+ _maybeSetupLastPBContextObserver() {
+ if (!this._observingLastPBContext) {
+ this._observer = {
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIObserver",
+ "nsISupportsWeakReference",
+ ]),
+
+ observe(subject, topic, data) {
+ if (topic == "last-pb-context-exited") {
+ Services.perms.removeByType("trackingprotection-pb");
+ }
+ },
+ };
+ Services.obs.addObserver(this._observer, "last-pb-context-exited", true);
+ this._observingLastPBContext = true;
+ }
+ },
+
+ _basePrincipalForAntiTrackingCommon(browser) {
+ let principal =
+ browser.browsingContext.currentWindowGlobal
+ ?.contentBlockingAllowListPrincipal;
+ // We can only use content principals for this purpose.
+ if (!principal || !principal.isContentPrincipal) {
+ return null;
+ }
+ return principal;
+ },
+
+ _permissionTypeFor(browser) {
+ return lazy.PrivateBrowsingUtils.isBrowserPrivate(browser)
+ ? "trackingprotection-pb"
+ : "trackingprotection";
+ },
+
+ _expiryFor(browser) {
+ return lazy.PrivateBrowsingUtils.isBrowserPrivate(browser)
+ ? Ci.nsIPermissionManager.EXPIRE_SESSION
+ : Ci.nsIPermissionManager.EXPIRE_NEVER;
+ },
+
+ /**
+ * Returns false if this module cannot handle the current document loaded in
+ * the browser object. This can happen for example for about: or file:
+ * documents.
+ */
+ canHandle(browser) {
+ return this._basePrincipalForAntiTrackingCommon(browser) != null;
+ },
+
+ /**
+ * Add the given browser object to the Content Blocking allow list.
+ */
+ add(browser) {
+ // Start observing PB last-context-exit notification to do the needed cleanup.
+ this._maybeSetupLastPBContextObserver();
+
+ let prin = this._basePrincipalForAntiTrackingCommon(browser);
+ let type = this._permissionTypeFor(browser);
+ let expire = this._expiryFor(browser);
+ Services.perms.addFromPrincipal(
+ prin,
+ type,
+ Services.perms.ALLOW_ACTION,
+ expire
+ );
+ },
+
+ /**
+ * Remove the given browser object from the Content Blocking allow list.
+ */
+ remove(browser) {
+ let prin = this._basePrincipalForAntiTrackingCommon(browser);
+ let type = this._permissionTypeFor(browser);
+ Services.perms.removeFromPrincipal(prin, type);
+ },
+
+ /**
+ * Returns true if the current browser has loaded a document that is on the
+ * Content Blocking allow list.
+ */
+ includes(browser) {
+ let prin = this._basePrincipalForAntiTrackingCommon(browser);
+ let type = this._permissionTypeFor(browser);
+ return (
+ Services.perms.testExactPermissionFromPrincipal(prin, type) ==
+ Services.perms.ALLOW_ACTION
+ );
+ },
+};
diff --git a/toolkit/components/antitracking/ContentBlockingLog.cpp b/toolkit/components/antitracking/ContentBlockingLog.cpp
new file mode 100644
index 0000000000..2a8595822d
--- /dev/null
+++ b/toolkit/components/antitracking/ContentBlockingLog.cpp
@@ -0,0 +1,272 @@
+/* -*- 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 "AntiTrackingLog.h"
+#include "ContentBlockingLog.h"
+
+#include "nsIEffectiveTLDService.h"
+#include "nsITrackingDBService.h"
+#include "nsIWebProgressListener.h"
+#include "nsNetCID.h"
+#include "nsNetUtil.h"
+#include "nsServiceManagerUtils.h"
+#include "nsTArray.h"
+#include "mozilla/BasePrincipal.h"
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/HashFunctions.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/RandomNum.h"
+#include "mozilla/ReverseIterator.h"
+#include "mozilla/StaticPrefs_browser.h"
+#include "mozilla/StaticPrefs_privacy.h"
+#include "mozilla/StaticPrefs_telemetry.h"
+#include "mozilla/StaticPtr.h"
+#include "mozilla/Telemetry.h"
+#include "mozilla/XorShift128PlusRNG.h"
+
+namespace mozilla {
+
+namespace {
+
+StaticAutoPtr<nsCString> gEmailWebAppDomainsPref;
+static constexpr char kEmailWebAppDomainPrefName[] =
+ "privacy.trackingprotection.emailtracking.webapp.domains";
+
+void EmailWebAppDomainPrefChangeCallback(const char* aPrefName, void*) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(!strcmp(aPrefName, kEmailWebAppDomainPrefName));
+ MOZ_ASSERT(gEmailWebAppDomainsPref);
+
+ Preferences::GetCString(kEmailWebAppDomainPrefName, *gEmailWebAppDomainsPref);
+}
+
+} // namespace
+
+Maybe<uint32_t> ContentBlockingLog::RecordLogParent(
+ const nsACString& aOrigin, uint32_t aType, bool aBlocked,
+ const Maybe<ContentBlockingNotifier::StorageAccessPermissionGrantedReason>&
+ aReason,
+ const nsTArray<nsCString>& aTrackingFullHashes) {
+ MOZ_ASSERT(XRE_IsParentProcess());
+
+ uint32_t events = GetContentBlockingEventsInLog();
+
+ bool blockedValue = aBlocked;
+ bool unblocked = false;
+
+ switch (aType) {
+ case nsIWebProgressListener::STATE_COOKIES_LOADED:
+ MOZ_ASSERT(!aBlocked,
+ "We don't expected to see blocked STATE_COOKIES_LOADED");
+ [[fallthrough]];
+
+ case nsIWebProgressListener::STATE_COOKIES_LOADED_TRACKER:
+ MOZ_ASSERT(
+ !aBlocked,
+ "We don't expected to see blocked STATE_COOKIES_LOADED_TRACKER");
+ [[fallthrough]];
+
+ case nsIWebProgressListener::STATE_COOKIES_LOADED_SOCIALTRACKER:
+ MOZ_ASSERT(!aBlocked,
+ "We don't expected to see blocked "
+ "STATE_COOKIES_LOADED_SOCIALTRACKER");
+ // Note that the logic in these branches are the logical negation of the
+ // logic in other branches, since the Document API we have is phrased
+ // in "loaded" terms as opposed to "blocked" terms.
+ blockedValue = !aBlocked;
+ [[fallthrough]];
+
+ case nsIWebProgressListener::STATE_BLOCKED_TRACKING_CONTENT:
+ case nsIWebProgressListener::STATE_LOADED_LEVEL_1_TRACKING_CONTENT:
+ case nsIWebProgressListener::STATE_LOADED_LEVEL_2_TRACKING_CONTENT:
+ case nsIWebProgressListener::STATE_BLOCKED_FINGERPRINTING_CONTENT:
+ case nsIWebProgressListener::STATE_LOADED_FINGERPRINTING_CONTENT:
+ case nsIWebProgressListener::STATE_BLOCKED_CRYPTOMINING_CONTENT:
+ case nsIWebProgressListener::STATE_LOADED_CRYPTOMINING_CONTENT:
+ case nsIWebProgressListener::STATE_BLOCKED_SOCIALTRACKING_CONTENT:
+ case nsIWebProgressListener::STATE_LOADED_SOCIALTRACKING_CONTENT:
+ case nsIWebProgressListener::STATE_COOKIES_BLOCKED_BY_PERMISSION:
+ case nsIWebProgressListener::STATE_COOKIES_BLOCKED_ALL:
+ case nsIWebProgressListener::STATE_COOKIES_BLOCKED_FOREIGN:
+ case nsIWebProgressListener::STATE_BLOCKED_EMAILTRACKING_CONTENT:
+ case nsIWebProgressListener::STATE_LOADED_EMAILTRACKING_LEVEL_1_CONTENT:
+ case nsIWebProgressListener::STATE_LOADED_EMAILTRACKING_LEVEL_2_CONTENT:
+ RecordLogInternal(aOrigin, aType, blockedValue);
+ break;
+
+ case nsIWebProgressListener::STATE_COOKIES_BLOCKED_TRACKER:
+ case nsIWebProgressListener::STATE_COOKIES_BLOCKED_SOCIALTRACKER:
+ RecordLogInternal(aOrigin, aType, blockedValue, aReason,
+ aTrackingFullHashes);
+ break;
+
+ case nsIWebProgressListener::STATE_REPLACED_FINGERPRINTING_CONTENT:
+ case nsIWebProgressListener::STATE_ALLOWED_FINGERPRINTING_CONTENT:
+ case nsIWebProgressListener::STATE_REPLACED_TRACKING_CONTENT:
+ case nsIWebProgressListener::STATE_ALLOWED_TRACKING_CONTENT:
+ RecordLogInternal(aOrigin, aType, blockedValue);
+ break;
+
+ default:
+ // Ignore nsIWebProgressListener::STATE_BLOCKED_UNSAFE_CONTENT;
+ break;
+ }
+
+ if (!aBlocked) {
+ unblocked = (events & aType) != 0;
+ }
+
+ const uint32_t oldEvents = events;
+ if (blockedValue) {
+ events |= aType;
+ } else if (unblocked) {
+ events &= ~aType;
+ }
+
+ if (events == oldEvents
+#ifdef ANDROID
+ // GeckoView always needs to notify about blocked trackers,
+ // since the GeckoView API always needs to report the URI and
+ // type of any blocked tracker. We use a platform-dependent code
+ // path here because reporting this notification on desktop
+ // platforms isn't necessary and doing so can have a big
+ // performance cost.
+ && aType != nsIWebProgressListener::STATE_BLOCKED_TRACKING_CONTENT
+#endif
+ ) {
+ // Avoid dispatching repeated notifications when nothing has
+ // changed
+ return Nothing();
+ }
+
+ return Some(events);
+}
+
+void ContentBlockingLog::ReportLog(nsIPrincipal* aFirstPartyPrincipal) {
+ MOZ_ASSERT(XRE_IsParentProcess());
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aFirstPartyPrincipal);
+
+ if (!StaticPrefs::browser_contentblocking_database_enabled()) {
+ return;
+ }
+
+ if (mLog.IsEmpty()) {
+ return;
+ }
+
+ nsCOMPtr<nsITrackingDBService> trackingDBService =
+ do_GetService("@mozilla.org/tracking-db-service;1");
+ if (NS_WARN_IF(!trackingDBService)) {
+ return;
+ }
+
+ trackingDBService->RecordContentBlockingLog(Stringify());
+}
+
+void ContentBlockingLog::ReportEmailTrackingLog(
+ nsIPrincipal* aFirstPartyPrincipal) {
+ MOZ_ASSERT(XRE_IsParentProcess());
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aFirstPartyPrincipal);
+
+ // We don't need to report if the first party is not a content.
+ if (!BasePrincipal::Cast(aFirstPartyPrincipal)->IsContentPrincipal()) {
+ return;
+ }
+
+ nsCOMPtr<nsIEffectiveTLDService> tldService =
+ do_GetService(NS_EFFECTIVETLDSERVICE_CONTRACTID);
+
+ if (!tldService) {
+ return;
+ }
+
+ nsTHashtable<nsCStringHashKey> level1SiteSet;
+ nsTHashtable<nsCStringHashKey> level2SiteSet;
+
+ for (const auto& originEntry : mLog) {
+ if (!originEntry.mData) {
+ continue;
+ }
+
+ bool isLevel1EmailTracker = false;
+ bool isLevel2EmailTracker = false;
+
+ for (const auto& logEntry : Reversed(originEntry.mData->mLogs)) {
+ // Check if the email tracking related event had been filed for the given
+ // origin entry. Note that we currently only block level 1 email trackers,
+ // so blocking event represents the page has embedded a level 1 tracker.
+ if (logEntry.mType ==
+ nsIWebProgressListener::STATE_LOADED_EMAILTRACKING_LEVEL_2_CONTENT) {
+ isLevel2EmailTracker = true;
+ break;
+ }
+
+ if (logEntry.mType ==
+ nsIWebProgressListener::STATE_BLOCKED_EMAILTRACKING_CONTENT ||
+ logEntry.mType == nsIWebProgressListener::
+ STATE_LOADED_EMAILTRACKING_LEVEL_1_CONTENT) {
+ isLevel1EmailTracker = true;
+ break;
+ }
+ }
+
+ if (isLevel1EmailTracker || isLevel2EmailTracker) {
+ nsCOMPtr<nsIURI> uri;
+ nsresult rv = NS_NewURI(getter_AddRefs(uri), originEntry.mOrigin);
+
+ if (NS_FAILED(rv)) {
+ continue;
+ }
+
+ nsAutoCString baseDomain;
+ rv = tldService->GetBaseDomain(uri, 0, baseDomain);
+
+ if (NS_FAILED(rv)) {
+ continue;
+ }
+
+ if (isLevel1EmailTracker) {
+ Unused << level1SiteSet.EnsureInserted(baseDomain);
+ } else {
+ Unused << level2SiteSet.EnsureInserted(baseDomain);
+ }
+ }
+ }
+
+ // Cache the email webapp domains pref value and register the callback
+ // function to update the cached value when the pref changes.
+ if (!gEmailWebAppDomainsPref) {
+ gEmailWebAppDomainsPref = new nsCString();
+
+ Preferences::RegisterCallbackAndCall(EmailWebAppDomainPrefChangeCallback,
+ kEmailWebAppDomainPrefName);
+ RunOnShutdown([]() {
+ Preferences::UnregisterCallback(EmailWebAppDomainPrefChangeCallback,
+ kEmailWebAppDomainPrefName);
+ gEmailWebAppDomainsPref = nullptr;
+ });
+ }
+
+ bool isTopEmailWebApp =
+ aFirstPartyPrincipal->IsURIInList(*gEmailWebAppDomainsPref);
+ uint32_t level1Count = level1SiteSet.Count();
+ uint32_t level2Count = level2SiteSet.Count();
+
+ Telemetry::Accumulate(
+ Telemetry::EMAIL_TRACKER_EMBEDDED_PER_TAB,
+ isTopEmailWebApp ? "base_emailapp"_ns : "base_normal"_ns, level1Count);
+ Telemetry::Accumulate(
+ Telemetry::EMAIL_TRACKER_EMBEDDED_PER_TAB,
+ isTopEmailWebApp ? "content_emailapp"_ns : "content_normal"_ns,
+ level2Count);
+ Telemetry::Accumulate(Telemetry::EMAIL_TRACKER_EMBEDDED_PER_TAB,
+ isTopEmailWebApp ? "all_emailapp"_ns : "all_normal"_ns,
+ level1Count + level2Count);
+}
+
+} // namespace mozilla
diff --git a/toolkit/components/antitracking/ContentBlockingLog.h b/toolkit/components/antitracking/ContentBlockingLog.h
new file mode 100644
index 0000000000..f71b2fc0ff
--- /dev/null
+++ b/toolkit/components/antitracking/ContentBlockingLog.h
@@ -0,0 +1,427 @@
+/* -*- 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_ContentBlockingLog_h
+#define mozilla_ContentBlockingLog_h
+
+#include "mozilla/ContentBlockingNotifier.h"
+#include "mozilla/JSONStringWriteFuncs.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/StaticPrefs_browser.h"
+
+#include "mozilla/UniquePtr.h"
+#include "nsIWebProgressListener.h"
+#include "nsReadableUtils.h"
+#include "nsTArray.h"
+#include "nsWindowSizes.h"
+
+class nsIPrincipal;
+
+namespace mozilla {
+
+class ContentBlockingLog final {
+ typedef ContentBlockingNotifier::StorageAccessPermissionGrantedReason
+ StorageAccessPermissionGrantedReason;
+
+ struct LogEntry {
+ uint32_t mType;
+ uint32_t mRepeatCount;
+ bool mBlocked;
+ Maybe<ContentBlockingNotifier::StorageAccessPermissionGrantedReason>
+ mReason;
+ nsTArray<nsCString> mTrackingFullHashes;
+ };
+
+ struct OriginDataEntry {
+ OriginDataEntry()
+ : mHasLevel1TrackingContentLoaded(false),
+ mHasLevel2TrackingContentLoaded(false) {}
+
+ bool mHasLevel1TrackingContentLoaded;
+ bool mHasLevel2TrackingContentLoaded;
+ Maybe<bool> mHasCookiesLoaded;
+ Maybe<bool> mHasTrackerCookiesLoaded;
+ Maybe<bool> mHasSocialTrackerCookiesLoaded;
+ nsTArray<LogEntry> mLogs;
+ };
+
+ struct OriginEntry {
+ OriginEntry() { mData = MakeUnique<OriginDataEntry>(); }
+
+ nsCString mOrigin;
+ UniquePtr<OriginDataEntry> mData;
+ };
+
+ typedef nsTArray<OriginEntry> OriginDataTable;
+
+ struct Comparator {
+ public:
+ bool Equals(const OriginDataTable::value_type& aLeft,
+ const OriginDataTable::value_type& aRight) const {
+ return aLeft.mOrigin.Equals(aRight.mOrigin);
+ }
+
+ bool Equals(const OriginDataTable::value_type& aLeft,
+ const nsACString& aRight) const {
+ return aLeft.mOrigin.Equals(aRight);
+ }
+ };
+
+ public:
+ static const nsLiteralCString kDummyOriginHash;
+
+ ContentBlockingLog() = default;
+ ~ContentBlockingLog() = default;
+
+ // Record the log in the parent process. This should be called only in the
+ // parent process and will replace the RecordLog below after we remove the
+ // ContentBlockingLog from content processes.
+ Maybe<uint32_t> RecordLogParent(
+ const nsACString& aOrigin, uint32_t aType, bool aBlocked,
+ const Maybe<
+ ContentBlockingNotifier::StorageAccessPermissionGrantedReason>&
+ aReason,
+ const nsTArray<nsCString>& aTrackingFullHashes);
+
+ void RecordLog(
+ const nsACString& aOrigin, uint32_t aType, bool aBlocked,
+ const Maybe<
+ ContentBlockingNotifier::StorageAccessPermissionGrantedReason>&
+ aReason,
+ const nsTArray<nsCString>& aTrackingFullHashes) {
+ RecordLogInternal(aOrigin, aType, aBlocked, aReason, aTrackingFullHashes);
+ }
+
+ void ReportLog(nsIPrincipal* aFirstPartyPrincipal);
+ void ReportEmailTrackingLog(nsIPrincipal* aFirstPartyPrincipal);
+
+ nsAutoCString Stringify() {
+ nsAutoCString buffer;
+
+ JSONStringRefWriteFunc js(buffer);
+ JSONWriter w(js);
+ w.Start();
+
+ for (const OriginEntry& entry : mLog) {
+ if (!entry.mData) {
+ continue;
+ }
+
+ w.StartArrayProperty(entry.mOrigin, w.SingleLineStyle);
+
+ StringifyCustomFields(entry, w);
+ for (const LogEntry& item : entry.mData->mLogs) {
+ w.StartArrayElement(w.SingleLineStyle);
+ {
+ w.IntElement(item.mType);
+ w.BoolElement(item.mBlocked);
+ w.IntElement(item.mRepeatCount);
+ if (item.mReason.isSome()) {
+ w.IntElement(item.mReason.value());
+ }
+ }
+ w.EndArray();
+ }
+ w.EndArray();
+ }
+
+ w.End();
+
+ return buffer;
+ }
+
+ bool HasBlockedAnyOfType(uint32_t aType) const {
+ // Note: nothing inside this loop should return false, the goal for the
+ // loop is to scan the log to see if we find a matching entry, and if so
+ // we would return true, otherwise in the end of the function outside of
+ // the loop we take the common `return false;` statement.
+ for (const OriginEntry& entry : mLog) {
+ if (!entry.mData) {
+ continue;
+ }
+
+ if (aType ==
+ nsIWebProgressListener::STATE_LOADED_LEVEL_1_TRACKING_CONTENT) {
+ if (entry.mData->mHasLevel1TrackingContentLoaded) {
+ return true;
+ }
+ } else if (aType == nsIWebProgressListener::
+ STATE_LOADED_LEVEL_2_TRACKING_CONTENT) {
+ if (entry.mData->mHasLevel2TrackingContentLoaded) {
+ return true;
+ }
+ } else if (aType == nsIWebProgressListener::STATE_COOKIES_LOADED) {
+ if (entry.mData->mHasCookiesLoaded.isSome() &&
+ entry.mData->mHasCookiesLoaded.value()) {
+ return true;
+ }
+ } else if (aType ==
+ nsIWebProgressListener::STATE_COOKIES_LOADED_TRACKER) {
+ if (entry.mData->mHasTrackerCookiesLoaded.isSome() &&
+ entry.mData->mHasTrackerCookiesLoaded.value()) {
+ return true;
+ }
+ } else if (aType ==
+ nsIWebProgressListener::STATE_COOKIES_LOADED_SOCIALTRACKER) {
+ if (entry.mData->mHasSocialTrackerCookiesLoaded.isSome() &&
+ entry.mData->mHasSocialTrackerCookiesLoaded.value()) {
+ return true;
+ }
+ } else {
+ for (const auto& item : entry.mData->mLogs) {
+ if (((item.mType & aType) != 0) && item.mBlocked) {
+ return true;
+ }
+ }
+ }
+ }
+ return false;
+ }
+
+ void AddSizeOfExcludingThis(nsWindowSizes& aSizes) const {
+ aSizes.mDOMSizes.mDOMOtherSize +=
+ mLog.ShallowSizeOfExcludingThis(aSizes.mState.mMallocSizeOf);
+
+ // Now add the sizes of each origin log queue.
+ for (const OriginEntry& entry : mLog) {
+ if (entry.mData) {
+ aSizes.mDOMSizes.mDOMOtherSize +=
+ aSizes.mState.mMallocSizeOf(entry.mData.get()) +
+ entry.mData->mLogs.ShallowSizeOfExcludingThis(
+ aSizes.mState.mMallocSizeOf);
+ }
+ }
+ }
+
+ uint32_t GetContentBlockingEventsInLog() {
+ uint32_t events = 0;
+
+ // We iterate the whole log to produce the overview of blocked events.
+ for (const OriginEntry& entry : mLog) {
+ if (!entry.mData) {
+ continue;
+ }
+
+ if (entry.mData->mHasLevel1TrackingContentLoaded) {
+ events |= nsIWebProgressListener::STATE_LOADED_LEVEL_1_TRACKING_CONTENT;
+ }
+
+ if (entry.mData->mHasLevel2TrackingContentLoaded) {
+ events |= nsIWebProgressListener::STATE_LOADED_LEVEL_2_TRACKING_CONTENT;
+ }
+
+ if (entry.mData->mHasCookiesLoaded.isSome() &&
+ entry.mData->mHasCookiesLoaded.value()) {
+ events |= nsIWebProgressListener::STATE_COOKIES_LOADED;
+ }
+
+ if (entry.mData->mHasTrackerCookiesLoaded.isSome() &&
+ entry.mData->mHasTrackerCookiesLoaded.value()) {
+ events |= nsIWebProgressListener::STATE_COOKIES_LOADED_TRACKER;
+ }
+
+ if (entry.mData->mHasSocialTrackerCookiesLoaded.isSome() &&
+ entry.mData->mHasSocialTrackerCookiesLoaded.value()) {
+ events |= nsIWebProgressListener::STATE_COOKIES_LOADED_SOCIALTRACKER;
+ }
+
+ for (const auto& item : entry.mData->mLogs) {
+ if (item.mBlocked) {
+ events |= item.mType;
+ }
+ }
+ }
+
+ return events;
+ }
+
+ private:
+ void RecordLogInternal(
+ const nsACString& aOrigin, uint32_t aType, bool aBlocked,
+ const Maybe<
+ ContentBlockingNotifier::StorageAccessPermissionGrantedReason>&
+ aReason = Nothing(),
+ const nsTArray<nsCString>& aTrackingFullHashes = nsTArray<nsCString>()) {
+ DebugOnly<bool> isCookiesBlockedTracker =
+ aType == nsIWebProgressListener::STATE_COOKIES_BLOCKED_TRACKER ||
+ aType == nsIWebProgressListener::STATE_COOKIES_BLOCKED_SOCIALTRACKER;
+ MOZ_ASSERT_IF(aBlocked, aReason.isNothing());
+ MOZ_ASSERT_IF(!isCookiesBlockedTracker, aReason.isNothing());
+ MOZ_ASSERT_IF(isCookiesBlockedTracker && !aBlocked, aReason.isSome());
+
+ if (aOrigin.IsVoid()) {
+ return;
+ }
+ auto index = mLog.IndexOf(aOrigin, 0, Comparator());
+ if (index != OriginDataTable::NoIndex) {
+ OriginEntry& entry = mLog[index];
+ if (!entry.mData) {
+ return;
+ }
+
+ if (RecordLogEntryInCustomField(aType, entry, aBlocked)) {
+ return;
+ }
+ if (!entry.mData->mLogs.IsEmpty()) {
+ auto& last = entry.mData->mLogs.LastElement();
+ if (last.mType == aType && last.mBlocked == aBlocked) {
+ ++last.mRepeatCount;
+ // Don't record recorded events. This helps compress our log.
+ // We don't care about if the the reason is the same, just keep the
+ // first one.
+ // Note: {aReason, aTrackingFullHashes} are not compared here and we
+ // simply keep the first for the reason, and merge hashes to make sure
+ // they can be correctly recorded.
+ for (const auto& hash : aTrackingFullHashes) {
+ if (!last.mTrackingFullHashes.Contains(hash)) {
+ last.mTrackingFullHashes.AppendElement(hash);
+ }
+ }
+ return;
+ }
+ }
+ if (entry.mData->mLogs.Length() ==
+ std::max(1u,
+ StaticPrefs::browser_contentblocking_originlog_length())) {
+ // Cap the size at the maximum length adjustable by the pref
+ entry.mData->mLogs.RemoveElementAt(0);
+ }
+ entry.mData->mLogs.AppendElement(
+ LogEntry{aType, 1u, aBlocked, aReason, aTrackingFullHashes.Clone()});
+ return;
+ }
+
+ // The entry has not been found.
+ OriginEntry* entry = mLog.AppendElement();
+ if (NS_WARN_IF(!entry || !entry->mData)) {
+ return;
+ }
+
+ entry->mOrigin = aOrigin;
+
+ if (aType ==
+ nsIWebProgressListener::STATE_LOADED_LEVEL_1_TRACKING_CONTENT) {
+ entry->mData->mHasLevel1TrackingContentLoaded = aBlocked;
+ } else if (aType ==
+ nsIWebProgressListener::STATE_LOADED_LEVEL_2_TRACKING_CONTENT) {
+ entry->mData->mHasLevel2TrackingContentLoaded = aBlocked;
+ } else if (aType == nsIWebProgressListener::STATE_COOKIES_LOADED) {
+ MOZ_ASSERT(entry->mData->mHasCookiesLoaded.isNothing());
+ entry->mData->mHasCookiesLoaded.emplace(aBlocked);
+ } else if (aType == nsIWebProgressListener::STATE_COOKIES_LOADED_TRACKER) {
+ MOZ_ASSERT(entry->mData->mHasTrackerCookiesLoaded.isNothing());
+ entry->mData->mHasTrackerCookiesLoaded.emplace(aBlocked);
+ } else if (aType ==
+ nsIWebProgressListener::STATE_COOKIES_LOADED_SOCIALTRACKER) {
+ MOZ_ASSERT(entry->mData->mHasSocialTrackerCookiesLoaded.isNothing());
+ entry->mData->mHasSocialTrackerCookiesLoaded.emplace(aBlocked);
+ } else {
+ entry->mData->mLogs.AppendElement(
+ LogEntry{aType, 1u, aBlocked, aReason, aTrackingFullHashes.Clone()});
+ }
+ }
+
+ bool RecordLogEntryInCustomField(uint32_t aType, OriginEntry& aEntry,
+ bool aBlocked) {
+ if (aType ==
+ nsIWebProgressListener::STATE_LOADED_LEVEL_1_TRACKING_CONTENT) {
+ aEntry.mData->mHasLevel1TrackingContentLoaded = aBlocked;
+ return true;
+ }
+ if (aType ==
+ nsIWebProgressListener::STATE_LOADED_LEVEL_2_TRACKING_CONTENT) {
+ aEntry.mData->mHasLevel2TrackingContentLoaded = aBlocked;
+ return true;
+ }
+ if (aType == nsIWebProgressListener::STATE_COOKIES_LOADED) {
+ if (aEntry.mData->mHasCookiesLoaded.isSome()) {
+ aEntry.mData->mHasCookiesLoaded.ref() = aBlocked;
+ } else {
+ aEntry.mData->mHasCookiesLoaded.emplace(aBlocked);
+ }
+ return true;
+ }
+ if (aType == nsIWebProgressListener::STATE_COOKIES_LOADED_TRACKER) {
+ if (aEntry.mData->mHasTrackerCookiesLoaded.isSome()) {
+ aEntry.mData->mHasTrackerCookiesLoaded.ref() = aBlocked;
+ } else {
+ aEntry.mData->mHasTrackerCookiesLoaded.emplace(aBlocked);
+ }
+ return true;
+ }
+ if (aType == nsIWebProgressListener::STATE_COOKIES_LOADED_SOCIALTRACKER) {
+ if (aEntry.mData->mHasSocialTrackerCookiesLoaded.isSome()) {
+ aEntry.mData->mHasSocialTrackerCookiesLoaded.ref() = aBlocked;
+ } else {
+ aEntry.mData->mHasSocialTrackerCookiesLoaded.emplace(aBlocked);
+ }
+ return true;
+ }
+ return false;
+ }
+
+ void StringifyCustomFields(const OriginEntry& aEntry, JSONWriter& aWriter) {
+ if (aEntry.mData->mHasLevel1TrackingContentLoaded) {
+ aWriter.StartArrayElement(aWriter.SingleLineStyle);
+ {
+ aWriter.IntElement(
+ nsIWebProgressListener::STATE_LOADED_LEVEL_1_TRACKING_CONTENT);
+ aWriter.BoolElement(true); // blocked
+ aWriter.IntElement(1); // repeat count
+ }
+ aWriter.EndArray();
+ }
+ if (aEntry.mData->mHasLevel2TrackingContentLoaded) {
+ aWriter.StartArrayElement(aWriter.SingleLineStyle);
+ {
+ aWriter.IntElement(
+ nsIWebProgressListener::STATE_LOADED_LEVEL_2_TRACKING_CONTENT);
+ aWriter.BoolElement(true); // blocked
+ aWriter.IntElement(1); // repeat count
+ }
+ aWriter.EndArray();
+ }
+ if (aEntry.mData->mHasCookiesLoaded.isSome()) {
+ aWriter.StartArrayElement(aWriter.SingleLineStyle);
+ {
+ aWriter.IntElement(nsIWebProgressListener::STATE_COOKIES_LOADED);
+ aWriter.BoolElement(
+ aEntry.mData->mHasCookiesLoaded.value()); // blocked
+ aWriter.IntElement(1); // repeat count
+ }
+ aWriter.EndArray();
+ }
+ if (aEntry.mData->mHasTrackerCookiesLoaded.isSome()) {
+ aWriter.StartArrayElement(aWriter.SingleLineStyle);
+ {
+ aWriter.IntElement(
+ nsIWebProgressListener::STATE_COOKIES_LOADED_TRACKER);
+ aWriter.BoolElement(
+ aEntry.mData->mHasTrackerCookiesLoaded.value()); // blocked
+ aWriter.IntElement(1); // repeat count
+ }
+ aWriter.EndArray();
+ }
+ if (aEntry.mData->mHasSocialTrackerCookiesLoaded.isSome()) {
+ aWriter.StartArrayElement(aWriter.SingleLineStyle);
+ {
+ aWriter.IntElement(
+ nsIWebProgressListener::STATE_COOKIES_LOADED_SOCIALTRACKER);
+ aWriter.BoolElement(
+ aEntry.mData->mHasSocialTrackerCookiesLoaded.value()); // blocked
+ aWriter.IntElement(1); // repeat count
+ }
+ aWriter.EndArray();
+ }
+ }
+
+ private:
+ OriginDataTable mLog;
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/toolkit/components/antitracking/ContentBlockingNotifier.cpp b/toolkit/components/antitracking/ContentBlockingNotifier.cpp
new file mode 100644
index 0000000000..5de57f91b2
--- /dev/null
+++ b/toolkit/components/antitracking/ContentBlockingNotifier.cpp
@@ -0,0 +1,567 @@
+/* -*- 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 "AntiTrackingLog.h"
+#include "ContentBlockingNotifier.h"
+#include "AntiTrackingUtils.h"
+
+#include "mozilla/EventQueue.h"
+#include "mozilla/StaticPrefs_privacy.h"
+#include "mozilla/dom/BrowserChild.h"
+#include "mozilla/dom/BrowsingContext.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/ContentParent.h"
+#include "mozilla/dom/WindowGlobalParent.h"
+#include "nsIClassifiedChannel.h"
+#include "nsIRunnable.h"
+#include "nsIScriptError.h"
+#include "nsIURI.h"
+#include "nsIOService.h"
+#include "nsGlobalWindowInner.h"
+#include "nsJSUtils.h"
+#include "mozIThirdPartyUtil.h"
+
+using namespace mozilla;
+using namespace mozilla::dom;
+using mozilla::dom::BrowsingContext;
+using mozilla::dom::ContentChild;
+using mozilla::dom::Document;
+
+static const uint32_t kMaxConsoleOutputDelayMs = 100;
+
+namespace {
+
+void RunConsoleReportingRunnable(already_AddRefed<nsIRunnable>&& aRunnable) {
+ if (StaticPrefs::privacy_restrict3rdpartystorage_console_lazy()) {
+ nsresult rv = NS_DispatchToCurrentThreadQueue(std::move(aRunnable),
+ kMaxConsoleOutputDelayMs,
+ EventQueuePriority::Idle);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+ } else {
+ nsCOMPtr<nsIRunnable> runnable(std::move(aRunnable));
+ nsresult rv = runnable->Run();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+ }
+}
+
+void ReportUnblockingToConsole(
+ uint64_t aWindowID, nsIPrincipal* aPrincipal,
+ const nsAString& aTrackingOrigin,
+ ContentBlockingNotifier::StorageAccessPermissionGrantedReason aReason) {
+ MOZ_ASSERT(aWindowID);
+ MOZ_ASSERT(aPrincipal);
+
+ nsAutoString sourceLine;
+ uint32_t lineNumber = 0, columnNumber = 0;
+ JSContext* cx = nsContentUtils::GetCurrentJSContext();
+ if (cx) {
+ nsJSUtils::GetCallingLocation(cx, sourceLine, &lineNumber, &columnNumber);
+ }
+
+ nsCOMPtr<nsIPrincipal> principal(aPrincipal);
+ nsAutoString trackingOrigin(aTrackingOrigin);
+
+ RefPtr<Runnable> runnable = NS_NewRunnableFunction(
+ "ReportUnblockingToConsoleDelayed",
+ [aWindowID, sourceLine, lineNumber, columnNumber, principal,
+ trackingOrigin, aReason]() {
+ const char* messageWithSameOrigin = nullptr;
+
+ switch (aReason) {
+ case ContentBlockingNotifier::eStorageAccessAPI:
+ case ContentBlockingNotifier::ePrivilegeStorageAccessForOriginAPI:
+ messageWithSameOrigin = "CookieAllowedForOriginByStorageAccessAPI";
+ break;
+
+ case ContentBlockingNotifier::eOpenerAfterUserInteraction:
+ [[fallthrough]];
+ case ContentBlockingNotifier::eOpener:
+ messageWithSameOrigin = "CookieAllowedForOriginByHeuristic";
+ break;
+ }
+
+ nsAutoString origin;
+ nsresult rv = nsContentUtils::GetUTFOrigin(principal, origin);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+
+ // Not adding grantedOrigin yet because we may not want it later.
+ AutoTArray<nsString, 2> params = {origin, trackingOrigin};
+
+ nsAutoString errorText;
+ rv = nsContentUtils::FormatLocalizedString(
+ nsContentUtils::eNECKO_PROPERTIES, messageWithSameOrigin, params,
+ errorText);
+ NS_ENSURE_SUCCESS_VOID(rv);
+
+ nsContentUtils::ReportToConsoleByWindowID(
+ errorText, nsIScriptError::warningFlag,
+ ANTITRACKING_CONSOLE_CATEGORY, aWindowID, nullptr, sourceLine,
+ lineNumber, columnNumber);
+ });
+
+ RunConsoleReportingRunnable(runnable.forget());
+}
+
+void ReportBlockingToConsole(uint64_t aWindowID, nsIURI* aURI,
+ uint32_t aRejectedReason) {
+ MOZ_ASSERT(aWindowID);
+ MOZ_ASSERT(aURI);
+ MOZ_ASSERT(
+ aRejectedReason == 0 ||
+ aRejectedReason ==
+ static_cast<uint32_t>(
+ nsIWebProgressListener::STATE_COOKIES_BLOCKED_BY_PERMISSION) ||
+ aRejectedReason ==
+ static_cast<uint32_t>(
+ nsIWebProgressListener::STATE_COOKIES_BLOCKED_TRACKER) ||
+ aRejectedReason ==
+ static_cast<uint32_t>(
+ nsIWebProgressListener::STATE_COOKIES_BLOCKED_SOCIALTRACKER) ||
+ aRejectedReason ==
+ static_cast<uint32_t>(
+ nsIWebProgressListener::STATE_COOKIES_PARTITIONED_FOREIGN) ||
+ aRejectedReason ==
+ static_cast<uint32_t>(
+ nsIWebProgressListener::STATE_COOKIES_BLOCKED_ALL) ||
+ aRejectedReason ==
+ static_cast<uint32_t>(
+ nsIWebProgressListener::STATE_COOKIES_BLOCKED_FOREIGN));
+
+ if (aURI->SchemeIs("chrome") || aURI->SchemeIs("about")) {
+ return;
+ }
+ bool hasFlags;
+ nsresult rv = NS_URIChainHasFlags(
+ aURI, nsIProtocolHandler::URI_FORBIDS_COOKIE_ACCESS, &hasFlags);
+ if (NS_FAILED(rv) || hasFlags) {
+ // If the protocol doesn't support cookies, no need to report them blocked.
+ return;
+ }
+
+ nsAutoString sourceLine;
+ uint32_t lineNumber = 0, columnNumber = 0;
+ JSContext* cx = nsContentUtils::GetCurrentJSContext();
+ if (cx) {
+ nsJSUtils::GetCallingLocation(cx, sourceLine, &lineNumber, &columnNumber);
+ }
+
+ nsCOMPtr<nsIURI> uri(aURI);
+
+ RefPtr<Runnable> runnable = NS_NewRunnableFunction(
+ "ReportBlockingToConsoleDelayed", [aWindowID, sourceLine, lineNumber,
+ columnNumber, uri, aRejectedReason]() {
+ const char* message = nullptr;
+ nsAutoCString category;
+ // When changing this list, please make sure to update the corresponding
+ // code in antitracking_head.js (inside _createTask).
+ // XXX: The nsIWebProgressListener constants below are interpreted as
+ // signed integers on Windows and the compiler complains that they can't
+ // be narrowed to uint32_t. To prevent this, we cast them to uint32_t.
+ switch (aRejectedReason) {
+ case uint32_t(
+ nsIWebProgressListener::STATE_COOKIES_BLOCKED_BY_PERMISSION):
+ message = "CookieBlockedByPermission";
+ category = "cookieBlockedPermission"_ns;
+ break;
+
+ case uint32_t(nsIWebProgressListener::STATE_COOKIES_BLOCKED_TRACKER):
+ message = "CookieBlockedTracker";
+ category = "cookieBlockedTracker"_ns;
+ break;
+
+ case uint32_t(nsIWebProgressListener::STATE_COOKIES_BLOCKED_ALL):
+ message = "CookieBlockedAll";
+ category = "cookieBlockedAll"_ns;
+ break;
+
+ case uint32_t(nsIWebProgressListener::STATE_COOKIES_BLOCKED_FOREIGN):
+ message = "CookieBlockedForeign";
+ category = "cookieBlockedForeign"_ns;
+ break;
+
+ case uint32_t(
+ nsIWebProgressListener::STATE_COOKIES_PARTITIONED_FOREIGN):
+ message = "CookiePartitionedForeign2";
+ category = "cookiePartitionedForeign"_ns;
+ break;
+
+ default:
+ return;
+ }
+
+ MOZ_ASSERT(message);
+
+ // Strip the URL of any possible username/password and make it ready
+ // to be presented in the UI.
+ nsCOMPtr<nsIURI> exposableURI =
+ net::nsIOService::CreateExposableURI(uri);
+ AutoTArray<nsString, 1> params;
+ CopyUTF8toUTF16(exposableURI->GetSpecOrDefault(),
+ *params.AppendElement());
+
+ nsAutoString errorText;
+ nsresult rv = nsContentUtils::FormatLocalizedString(
+ nsContentUtils::eNECKO_PROPERTIES, message, params, errorText);
+ NS_ENSURE_SUCCESS_VOID(rv);
+
+ nsContentUtils::ReportToConsoleByWindowID(
+ errorText, nsIScriptError::warningFlag, category, aWindowID,
+ nullptr, sourceLine, lineNumber, columnNumber);
+ });
+
+ RunConsoleReportingRunnable(runnable.forget());
+}
+
+void ReportBlockingToConsole(nsIChannel* aChannel, nsIURI* aURI,
+ uint32_t aRejectedReason) {
+ MOZ_ASSERT(aChannel && aURI);
+ uint64_t windowID = nsContentUtils::GetInnerWindowID(aChannel);
+ if (!windowID) {
+ // Get the window ID from the target BrowsingContext
+ nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo();
+
+ RefPtr<dom::BrowsingContext> targetBrowsingContext;
+ loadInfo->GetTargetBrowsingContext(getter_AddRefs(targetBrowsingContext));
+
+ if (!targetBrowsingContext) {
+ return;
+ }
+
+ WindowContext* windowContext =
+ targetBrowsingContext->GetCurrentWindowContext();
+ if (!windowContext) {
+ return;
+ }
+
+ windowID = windowContext->InnerWindowId();
+ }
+ ReportBlockingToConsole(windowID, aURI, aRejectedReason);
+}
+
+void NotifyBlockingDecision(nsIChannel* aTrackingChannel,
+ ContentBlockingNotifier::BlockingDecision aDecision,
+ uint32_t aRejectedReason, nsIURI* aURI) {
+ MOZ_ASSERT(aTrackingChannel);
+
+ // This can be called in either the parent process or the child processes.
+ // When this is called in the child processes, we must have a window.
+ if (XRE_IsContentProcess()) {
+ nsCOMPtr<nsILoadContext> loadContext;
+ NS_QueryNotificationCallbacks(aTrackingChannel, loadContext);
+ if (!loadContext) {
+ return;
+ }
+
+ nsCOMPtr<mozIDOMWindowProxy> window;
+ loadContext->GetAssociatedWindow(getter_AddRefs(window));
+ if (!window) {
+ return;
+ }
+
+ nsCOMPtr<nsPIDOMWindowOuter> outer = nsPIDOMWindowOuter::From(window);
+ if (!outer) {
+ return;
+ }
+
+ // When this is called in the child processes with system privileges,
+ // the decision should always be ALLOW. We can stop here because both
+ // UI and content blocking log don't care this event.
+ if (nsGlobalWindowOuter::Cast(outer)->GetPrincipal() ==
+ nsContentUtils::GetSystemPrincipal()) {
+ MOZ_DIAGNOSTIC_ASSERT(aDecision ==
+ ContentBlockingNotifier::BlockingDecision::eAllow);
+ return;
+ }
+ }
+
+ nsAutoCString trackingOrigin;
+ if (aURI) {
+ Unused << nsContentUtils::GetASCIIOrigin(aURI, trackingOrigin);
+ }
+
+ if (aDecision == ContentBlockingNotifier::BlockingDecision::eBlock) {
+ ContentBlockingNotifier::OnEvent(aTrackingChannel, true, aRejectedReason,
+ trackingOrigin);
+
+ ReportBlockingToConsole(aTrackingChannel, aURI, aRejectedReason);
+ }
+
+ // Now send the generic "cookies loaded" notifications, from the most generic
+ // to the most specific.
+ ContentBlockingNotifier::OnEvent(aTrackingChannel, false,
+ nsIWebProgressListener::STATE_COOKIES_LOADED,
+ trackingOrigin);
+
+ nsCOMPtr<nsIClassifiedChannel> classifiedChannel =
+ do_QueryInterface(aTrackingChannel);
+ if (!classifiedChannel) {
+ return;
+ }
+
+ uint32_t classificationFlags =
+ classifiedChannel->GetThirdPartyClassificationFlags();
+ if (classificationFlags &
+ nsIClassifiedChannel::ClassificationFlags::CLASSIFIED_TRACKING) {
+ ContentBlockingNotifier::OnEvent(
+ aTrackingChannel, false,
+ nsIWebProgressListener::STATE_COOKIES_LOADED_TRACKER, trackingOrigin);
+ }
+
+ if (classificationFlags &
+ nsIClassifiedChannel::ClassificationFlags::CLASSIFIED_SOCIALTRACKING) {
+ ContentBlockingNotifier::OnEvent(
+ aTrackingChannel, false,
+ nsIWebProgressListener::STATE_COOKIES_LOADED_SOCIALTRACKER,
+ trackingOrigin);
+ }
+}
+
+// Send a message to notify OnContentBlockingEvent in the parent, which will
+// update the ContentBlockingLog in the parent.
+void NotifyEventInChild(
+ nsIChannel* aTrackingChannel, bool aBlocked, uint32_t aRejectedReason,
+ const nsACString& aTrackingOrigin,
+ const Maybe<ContentBlockingNotifier::StorageAccessPermissionGrantedReason>&
+ aReason) {
+ MOZ_ASSERT(XRE_IsContentProcess());
+
+ // We don't need to find the top-level window here because the
+ // parent will do that for us.
+ nsCOMPtr<nsILoadContext> loadContext;
+ NS_QueryNotificationCallbacks(aTrackingChannel, loadContext);
+ if (!loadContext) {
+ return;
+ }
+
+ nsCOMPtr<mozIDOMWindowProxy> window;
+ loadContext->GetAssociatedWindow(getter_AddRefs(window));
+ if (!window) {
+ return;
+ }
+
+ RefPtr<dom::BrowserChild> browserChild = dom::BrowserChild::GetFrom(window);
+ NS_ENSURE_TRUE_VOID(browserChild);
+
+ nsTArray<nsCString> trackingFullHashes;
+ nsCOMPtr<nsIClassifiedChannel> classifiedChannel =
+ do_QueryInterface(aTrackingChannel);
+
+ if (classifiedChannel) {
+ Unused << classifiedChannel->GetMatchedTrackingFullHashes(
+ trackingFullHashes);
+ }
+
+ browserChild->NotifyContentBlockingEvent(aRejectedReason, aTrackingChannel,
+ aBlocked, aTrackingOrigin,
+ trackingFullHashes, aReason);
+}
+
+// Update the ContentBlockingLog of the top-level WindowGlobalParent of
+// the tracking channel.
+void NotifyEventInParent(
+ nsIChannel* aTrackingChannel, bool aBlocked, uint32_t aRejectedReason,
+ const nsACString& aTrackingOrigin,
+ const Maybe<ContentBlockingNotifier::StorageAccessPermissionGrantedReason>&
+ aReason) {
+ MOZ_ASSERT(XRE_IsParentProcess());
+
+ nsCOMPtr<nsILoadInfo> loadInfo = aTrackingChannel->LoadInfo();
+ RefPtr<dom::BrowsingContext> bc;
+ loadInfo->GetBrowsingContext(getter_AddRefs(bc));
+
+ if (!bc || bc->IsDiscarded()) {
+ return;
+ }
+
+ bc = bc->Top();
+ RefPtr<dom::WindowGlobalParent> wgp =
+ bc->Canonical()->GetCurrentWindowGlobal();
+ NS_ENSURE_TRUE_VOID(wgp);
+
+ nsTArray<nsCString> trackingFullHashes;
+ nsCOMPtr<nsIClassifiedChannel> classifiedChannel =
+ do_QueryInterface(aTrackingChannel);
+
+ if (classifiedChannel) {
+ Unused << classifiedChannel->GetMatchedTrackingFullHashes(
+ trackingFullHashes);
+ }
+
+ wgp->NotifyContentBlockingEvent(aRejectedReason, aTrackingChannel, aBlocked,
+ aTrackingOrigin, trackingFullHashes, aReason);
+}
+
+} // namespace
+
+/* static */
+void ContentBlockingNotifier::ReportUnblockingToConsole(
+ BrowsingContext* aBrowsingContext, const nsAString& aTrackingOrigin,
+ ContentBlockingNotifier::StorageAccessPermissionGrantedReason aReason) {
+ MOZ_ASSERT(aBrowsingContext);
+ MOZ_ASSERT_IF(XRE_IsContentProcess(), aBrowsingContext->Top()->IsInProcess());
+
+ uint64_t windowID = aBrowsingContext->GetCurrentInnerWindowId();
+
+ // The storage permission is granted under the top-level origin.
+ nsCOMPtr<nsIPrincipal> principal =
+ AntiTrackingUtils::GetPrincipal(aBrowsingContext->Top());
+ if (NS_WARN_IF(!principal)) {
+ return;
+ }
+
+ ::ReportUnblockingToConsole(windowID, principal, aTrackingOrigin, aReason);
+}
+
+/* static */
+void ContentBlockingNotifier::OnDecision(nsIChannel* aChannel,
+ BlockingDecision aDecision,
+ uint32_t aRejectedReason) {
+ MOZ_ASSERT(
+ aRejectedReason == 0 ||
+ aRejectedReason ==
+ static_cast<uint32_t>(
+ nsIWebProgressListener::STATE_COOKIES_BLOCKED_BY_PERMISSION) ||
+ aRejectedReason ==
+ static_cast<uint32_t>(
+ nsIWebProgressListener::STATE_COOKIES_BLOCKED_TRACKER) ||
+ aRejectedReason ==
+ static_cast<uint32_t>(
+ nsIWebProgressListener::STATE_COOKIES_BLOCKED_SOCIALTRACKER) ||
+ aRejectedReason ==
+ static_cast<uint32_t>(
+ nsIWebProgressListener::STATE_COOKIES_PARTITIONED_FOREIGN) ||
+ aRejectedReason ==
+ static_cast<uint32_t>(
+ nsIWebProgressListener::STATE_COOKIES_BLOCKED_ALL) ||
+ aRejectedReason ==
+ static_cast<uint32_t>(
+ nsIWebProgressListener::STATE_COOKIES_BLOCKED_FOREIGN));
+ MOZ_ASSERT(aDecision == BlockingDecision::eBlock ||
+ aDecision == BlockingDecision::eAllow);
+
+ if (!aChannel) {
+ return;
+ }
+
+ nsCOMPtr<nsIURI> uri;
+ aChannel->GetURI(getter_AddRefs(uri));
+
+ // Can be called in EITHER the parent or child process.
+ NotifyBlockingDecision(aChannel, aDecision, aRejectedReason, uri);
+}
+
+/* static */
+void ContentBlockingNotifier::OnDecision(nsPIDOMWindowInner* aWindow,
+ BlockingDecision aDecision,
+ uint32_t aRejectedReason) {
+ MOZ_ASSERT(aWindow);
+ MOZ_ASSERT(
+ aRejectedReason == 0 ||
+ aRejectedReason ==
+ static_cast<uint32_t>(
+ nsIWebProgressListener::STATE_COOKIES_BLOCKED_BY_PERMISSION) ||
+ aRejectedReason ==
+ static_cast<uint32_t>(
+ nsIWebProgressListener::STATE_COOKIES_BLOCKED_TRACKER) ||
+ aRejectedReason ==
+ static_cast<uint32_t>(
+ nsIWebProgressListener::STATE_COOKIES_BLOCKED_SOCIALTRACKER) ||
+ aRejectedReason ==
+ static_cast<uint32_t>(
+ nsIWebProgressListener::STATE_COOKIES_PARTITIONED_FOREIGN) ||
+ aRejectedReason ==
+ static_cast<uint32_t>(
+ nsIWebProgressListener::STATE_COOKIES_BLOCKED_ALL) ||
+ aRejectedReason ==
+ static_cast<uint32_t>(
+ nsIWebProgressListener::STATE_COOKIES_BLOCKED_FOREIGN));
+ MOZ_ASSERT(aDecision == BlockingDecision::eBlock ||
+ aDecision == BlockingDecision::eAllow);
+
+ Document* document = aWindow->GetExtantDoc();
+ if (!document) {
+ return;
+ }
+
+ nsIChannel* channel = document->GetChannel();
+ if (!channel) {
+ return;
+ }
+
+ nsIURI* uri = document->GetDocumentURI();
+
+ NotifyBlockingDecision(channel, aDecision, aRejectedReason, uri);
+}
+
+/* static */
+void ContentBlockingNotifier::OnDecision(BrowsingContext* aBrowsingContext,
+ BlockingDecision aDecision,
+ uint32_t aRejectedReason) {
+ MOZ_ASSERT(aBrowsingContext);
+ MOZ_ASSERT_IF(XRE_IsContentProcess(), aBrowsingContext->IsInProcess());
+
+ if (aBrowsingContext->IsInProcess()) {
+ nsCOMPtr<nsPIDOMWindowOuter> outer = aBrowsingContext->GetDOMWindow();
+ if (NS_WARN_IF(!outer)) {
+ return;
+ }
+
+ nsCOMPtr<nsPIDOMWindowInner> inner = outer->GetCurrentInnerWindow();
+ if (NS_WARN_IF(!inner)) {
+ return;
+ }
+
+ ContentBlockingNotifier::OnDecision(inner, aDecision, aRejectedReason);
+ } else {
+ // we send an IPC to the content process when we don't have an in-process
+ // browsing context. This is not smart because this should be able to be
+ // done directly in the parent. The reason we are doing this is because we
+ // need the channel, which is not accessible in the parent when you only
+ // have a browsing context.
+ MOZ_ASSERT(XRE_IsParentProcess());
+
+ ContentParent* cp = aBrowsingContext->Canonical()->GetContentParent();
+ Unused << cp->SendOnContentBlockingDecision(aBrowsingContext, aDecision,
+ aRejectedReason);
+ }
+}
+
+/* static */
+void ContentBlockingNotifier::OnEvent(nsIChannel* aTrackingChannel,
+ uint32_t aRejectedReason, bool aBlocked) {
+ MOZ_ASSERT(XRE_IsParentProcess() && aTrackingChannel);
+
+ nsCOMPtr<nsIURI> uri;
+ aTrackingChannel->GetURI(getter_AddRefs(uri));
+
+ nsAutoCString trackingOrigin;
+ if (uri) {
+ Unused << nsContentUtils::GetASCIIOrigin(uri, trackingOrigin);
+ }
+
+ return ContentBlockingNotifier::OnEvent(aTrackingChannel, aBlocked,
+ aRejectedReason, trackingOrigin);
+}
+
+/* static */
+void ContentBlockingNotifier::OnEvent(
+ nsIChannel* aTrackingChannel, bool aBlocked, uint32_t aRejectedReason,
+ const nsACString& aTrackingOrigin,
+ const Maybe<StorageAccessPermissionGrantedReason>& aReason) {
+ if (XRE_IsParentProcess()) {
+ NotifyEventInParent(aTrackingChannel, aBlocked, aRejectedReason,
+ aTrackingOrigin, aReason);
+ } else {
+ NotifyEventInChild(aTrackingChannel, aBlocked, aRejectedReason,
+ aTrackingOrigin, aReason);
+ }
+}
diff --git a/toolkit/components/antitracking/ContentBlockingNotifier.h b/toolkit/components/antitracking/ContentBlockingNotifier.h
new file mode 100644
index 0000000000..20f32d7f94
--- /dev/null
+++ b/toolkit/components/antitracking/ContentBlockingNotifier.h
@@ -0,0 +1,74 @@
+/* -*- 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_contentblockingnotifier_h
+#define mozilla_contentblockingnotifier_h
+
+#include "nsStringFwd.h"
+#include "mozilla/Maybe.h"
+
+#define ANTITRACKING_CONSOLE_CATEGORY "Content Blocking"_ns
+
+class nsIChannel;
+class nsPIDOMWindowInner;
+class nsPIDOMWindowOuter;
+
+namespace mozilla {
+namespace dom {
+class BrowsingContext;
+} // namespace dom
+
+class ContentBlockingNotifier final {
+ public:
+ enum class BlockingDecision {
+ eBlock,
+ eAllow,
+ };
+ enum StorageAccessPermissionGrantedReason {
+ eStorageAccessAPI,
+ eOpenerAfterUserInteraction,
+ eOpener,
+ ePrivilegeStorageAccessForOriginAPI,
+ };
+
+ // This method can be called on the parent process or on the content process.
+ // The notification is propagated to the child channel if aChannel is a parent
+ // channel proxy.
+ //
+ // aDecision can be eBlock if we have decided to block some content, or eAllow
+ // if we have decided to allow the content through.
+ //
+ // aRejectedReason must be one of these values:
+ // * nsIWebProgressListener::STATE_COOKIES_BLOCKED_BY_PERMISSION
+ // * nsIWebProgressListener::STATE_COOKIES_BLOCKED_TRACKER
+ // * nsIWebProgressListener::STATE_COOKIES_BLOCKED_SOCIALTRACKER
+ // * nsIWebProgressListener::STATE_COOKIES_BLOCKED_ALL
+ // * nsIWebProgressListener::STATE_COOKIES_BLOCKED_FOREIGN
+ static void OnDecision(nsIChannel* aChannel, BlockingDecision aDecision,
+ uint32_t aRejectedReason);
+
+ static void OnDecision(nsPIDOMWindowInner* aWindow,
+ BlockingDecision aDecision, uint32_t aRejectedReason);
+
+ static void OnDecision(dom::BrowsingContext* aBrowsingContext,
+ BlockingDecision aDecision, uint32_t aRejectedReason);
+
+ static void OnEvent(nsIChannel* aChannel, uint32_t aRejectedReason,
+ bool aBlocked = true);
+
+ static void OnEvent(
+ nsIChannel* aChannel, bool aBlocked, uint32_t aRejectedReason,
+ const nsACString& aTrackingOrigin,
+ const Maybe<StorageAccessPermissionGrantedReason>& aReason = Nothing());
+
+ static void ReportUnblockingToConsole(
+ dom::BrowsingContext* aBrowsingContext, const nsAString& aTrackingOrigin,
+ StorageAccessPermissionGrantedReason aReason);
+};
+
+} // namespace mozilla
+
+#endif // mozilla_contentblockingnotifier_h
diff --git a/toolkit/components/antitracking/ContentBlockingTelemetryService.cpp b/toolkit/components/antitracking/ContentBlockingTelemetryService.cpp
new file mode 100644
index 0000000000..fd19904309
--- /dev/null
+++ b/toolkit/components/antitracking/ContentBlockingTelemetryService.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 "ContentBlockingTelemetryService.h"
+
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/PermissionManager.h"
+#include "mozilla/Services.h"
+#include "mozilla/StaticPtr.h"
+#include "mozilla/Telemetry.h"
+
+#include "AntiTrackingLog.h"
+#include "prtime.h"
+
+#include "nsIObserverService.h"
+#include "nsIPermission.h"
+#include "nsTArray.h"
+
+using namespace mozilla;
+
+NS_IMPL_ISUPPORTS(ContentBlockingTelemetryService, nsIObserver)
+
+static StaticRefPtr<ContentBlockingTelemetryService>
+ sContentBlockingTelemetryService;
+
+/* static */
+already_AddRefed<ContentBlockingTelemetryService>
+ContentBlockingTelemetryService::GetSingleton() {
+ if (!sContentBlockingTelemetryService) {
+ sContentBlockingTelemetryService = new ContentBlockingTelemetryService();
+ ClearOnShutdown(&sContentBlockingTelemetryService);
+ }
+
+ RefPtr<ContentBlockingTelemetryService> service =
+ sContentBlockingTelemetryService;
+
+ return service.forget();
+}
+
+NS_IMETHODIMP
+ContentBlockingTelemetryService::Observe(nsISupports* aSubject,
+ const char* aTopic,
+ const char16_t* aData) {
+ if (strcmp(aTopic, "idle-daily") == 0) {
+ ReportStoragePermissionExpire();
+ return NS_OK;
+ }
+
+ return NS_OK;
+}
+
+void ContentBlockingTelemetryService::ReportStoragePermissionExpire() {
+ MOZ_ASSERT(XRE_IsParentProcess());
+
+ LOG(("Start to report storage permission expire."));
+
+ PermissionManager* permManager = PermissionManager::GetInstance();
+ if (NS_WARN_IF(!permManager)) {
+ LOG(("Permission manager is null, bailing out early"));
+ return;
+ }
+
+ nsTArray<RefPtr<nsIPermission>> permissions;
+ nsresult rv =
+ permManager->GetAllWithTypePrefix("3rdPartyStorage"_ns, permissions);
+
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ LOG(("Fail to get all storage access permissions."));
+ return;
+ }
+
+ nsTArray<uint32_t> records;
+
+ for (const auto& permission : permissions) {
+ if (!permission) {
+ LOG(("Couldn't get the permission for unknown reasons"));
+ continue;
+ }
+
+ uint32_t expireType;
+ rv = permission->GetExpireType(&expireType);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ LOG(("Couldn't get the expire type."));
+ continue;
+ }
+
+ // We only care about permissions that have a EXPIRE_TIME as the expire
+ // type.
+ if (expireType != nsIPermissionManager::EXPIRE_TIME) {
+ continue;
+ }
+
+ // Collect how much longer the storage permission will be valid for, in
+ // days.
+ int64_t expirationTime = 0;
+ rv = permission->GetExpireTime(&expirationTime);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ LOG(("Couldn't get the expire time."));
+ continue;
+ }
+
+ expirationTime -= (PR_Now() / PR_USEC_PER_MSEC);
+
+ // Skip expired permissions.
+ if (expirationTime <= 0) {
+ continue;
+ }
+
+ int64_t expireDays = expirationTime / 1000 / 60 / 60 / 24;
+
+ records.AppendElement(expireDays);
+ }
+
+ if (!records.IsEmpty()) {
+ Telemetry::Accumulate(Telemetry::STORAGE_ACCESS_REMAINING_DAYS, records);
+ }
+}
diff --git a/toolkit/components/antitracking/ContentBlockingTelemetryService.h b/toolkit/components/antitracking/ContentBlockingTelemetryService.h
new file mode 100644
index 0000000000..24fd57faec
--- /dev/null
+++ b/toolkit/components/antitracking/ContentBlockingTelemetryService.h
@@ -0,0 +1,31 @@
+/* -*- 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_contentblockingtelemetryservice_h
+#define mozilla_contentblockingtelemetryservice_h
+
+#include "nsIObserver.h"
+
+namespace mozilla {
+
+class ContentBlockingTelemetryService final : public nsIObserver {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIOBSERVER
+
+ static already_AddRefed<ContentBlockingTelemetryService> GetSingleton();
+
+ private:
+ ContentBlockingTelemetryService() = default;
+
+ ~ContentBlockingTelemetryService() = default;
+
+ void ReportStoragePermissionExpire();
+};
+
+} // namespace mozilla
+
+#endif // mozilla_contentblockingtelemetryservice_h
diff --git a/toolkit/components/antitracking/ContentBlockingUserInteraction.cpp b/toolkit/components/antitracking/ContentBlockingUserInteraction.cpp
new file mode 100644
index 0000000000..fa742b5b5e
--- /dev/null
+++ b/toolkit/components/antitracking/ContentBlockingUserInteraction.cpp
@@ -0,0 +1,89 @@
+/* -*- 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 "AntiTrackingLog.h"
+#include "ContentBlockingUserInteraction.h"
+#include "AntiTrackingUtils.h"
+
+#include "mozilla/dom/ContentChild.h"
+#include "mozilla/PermissionManager.h"
+#include "nsIPrincipal.h"
+#include "nsXULAppAPI.h"
+#include "prtime.h"
+
+namespace mozilla {
+
+/* static */
+void ContentBlockingUserInteraction::Observe(nsIPrincipal* aPrincipal) {
+ if (!aPrincipal || aPrincipal->IsSystemPrincipal()) {
+ // The content process may have sent us garbage data.
+ return;
+ }
+
+ if (XRE_IsParentProcess()) {
+ LOG_PRIN(("Saving the userInteraction for %s", _spec), aPrincipal);
+
+ PermissionManager* permManager = PermissionManager::GetInstance();
+ if (NS_WARN_IF(!permManager)) {
+ LOG(("Permission manager is null, bailing out early"));
+ return;
+ }
+
+ // Remember that this pref is stored in seconds!
+ uint32_t expirationType = nsIPermissionManager::EXPIRE_TIME;
+ uint32_t expirationTime =
+ StaticPrefs::privacy_userInteraction_expiration() * 1000;
+ int64_t when = (PR_Now() / PR_USEC_PER_MSEC) + expirationTime;
+
+ uint32_t privateBrowsingId = 0;
+ nsresult rv = aPrincipal->GetPrivateBrowsingId(&privateBrowsingId);
+ if (!NS_WARN_IF(NS_FAILED(rv)) && privateBrowsingId > 0) {
+ // If we are coming from a private window, make sure to store a
+ // session-only permission which won't get persisted to disk.
+ expirationType = nsIPermissionManager::EXPIRE_SESSION;
+ when = 0;
+ }
+
+ rv = permManager->AddFromPrincipal(aPrincipal, USER_INTERACTION_PERM,
+ nsIPermissionManager::ALLOW_ACTION,
+ expirationType, when);
+ Unused << NS_WARN_IF(NS_FAILED(rv));
+
+ if (StaticPrefs::privacy_antitracking_testing()) {
+ nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
+ obs->NotifyObservers(
+ nullptr, "antitracking-test-user-interaction-perm-added", nullptr);
+ }
+ return;
+ }
+
+ dom::ContentChild* cc = dom::ContentChild::GetSingleton();
+ MOZ_ASSERT(cc);
+
+ LOG_PRIN(("Asking the parent process to save the user-interaction for us: %s",
+ _spec),
+ aPrincipal);
+ cc->SendStoreUserInteractionAsPermission(aPrincipal);
+}
+
+/* static */
+bool ContentBlockingUserInteraction::Exists(nsIPrincipal* aPrincipal) {
+ PermissionManager* permManager = PermissionManager::GetInstance();
+ if (NS_WARN_IF(!permManager)) {
+ return false;
+ }
+
+ uint32_t result = 0;
+ nsresult rv = permManager->TestPermissionWithoutDefaultsFromPrincipal(
+ aPrincipal, USER_INTERACTION_PERM, &result);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return false;
+ }
+
+ return result == nsIPermissionManager::ALLOW_ACTION;
+}
+
+} // namespace mozilla
diff --git a/toolkit/components/antitracking/ContentBlockingUserInteraction.h b/toolkit/components/antitracking/ContentBlockingUserInteraction.h
new file mode 100644
index 0000000000..503990ae82
--- /dev/null
+++ b/toolkit/components/antitracking/ContentBlockingUserInteraction.h
@@ -0,0 +1,29 @@
+/* -*- 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_contentblockinguserinteraction_h
+#define mozilla_contentblockinguserinteraction_h
+
+#define USER_INTERACTION_PERM "storageAccessAPI"_ns
+
+class nsIPrincipal;
+
+namespace mozilla {
+
+class ContentBlockingUserInteraction final {
+ public:
+ // Used to remember that we observed a user interaction that is significant
+ // for content blocking.
+ static void Observe(nsIPrincipal* aPrincipal);
+
+ // Used to query whether we've observed a user interaction that is significant
+ // for content blocking for the given principal in the past.
+ static bool Exists(nsIPrincipal* aPrincipal);
+};
+
+} // namespace mozilla
+
+#endif // mozilla_contentblockinguserinteraction_h
diff --git a/toolkit/components/antitracking/DynamicFpiRedirectHeuristic.cpp b/toolkit/components/antitracking/DynamicFpiRedirectHeuristic.cpp
new file mode 100644
index 0000000000..c4b3ea3d2e
--- /dev/null
+++ b/toolkit/components/antitracking/DynamicFpiRedirectHeuristic.cpp
@@ -0,0 +1,343 @@
+/* -*- 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 "AntiTrackingLog.h"
+#include "DynamicFpiRedirectHeuristic.h"
+#include "ContentBlockingAllowList.h"
+#include "ContentBlockingUserInteraction.h"
+#include "StorageAccessAPIHelper.h"
+
+#include "mozilla/net/HttpBaseChannel.h"
+#include "mozilla/net/UrlClassifierCommon.h"
+#include "mozilla/Telemetry.h"
+#include "nsContentUtils.h"
+#include "nsIChannel.h"
+#include "nsICookieJarSettings.h"
+#include "nsICookieService.h"
+#include "nsIEffectiveTLDService.h"
+#include "nsINavHistoryService.h"
+#include "nsIRedirectHistoryEntry.h"
+#include "nsIScriptError.h"
+#include "nsIURI.h"
+#include "nsNetCID.h"
+#include "nsNetUtil.h"
+#include "nsScriptSecurityManager.h"
+#include "nsToolkitCompsCID.h"
+
+namespace mozilla {
+
+namespace {
+
+nsresult GetBaseDomain(nsIURI* aURI, nsACString& aBaseDomain) {
+ nsCOMPtr<nsIEffectiveTLDService> tldService =
+ do_GetService(NS_EFFECTIVETLDSERVICE_CONTRACTID);
+
+ if (!tldService) {
+ return NS_ERROR_FAILURE;
+ }
+
+ return tldService->GetBaseDomain(aURI, 0, aBaseDomain);
+}
+
+// check if there's any interacting visit within the given seconds
+bool HasEligibleVisit(
+ nsIURI* aURI,
+ int64_t aSinceInSec = StaticPrefs::
+ privacy_restrict3rdpartystorage_heuristic_recently_visited_time()) {
+ nsresult rv;
+
+ nsAutoCString baseDomain;
+ rv = GetBaseDomain(aURI, baseDomain);
+ NS_ENSURE_SUCCESS(rv, false);
+
+ nsCOMPtr<nsINavHistoryService> histSrv =
+ do_GetService(NS_NAVHISTORYSERVICE_CONTRACTID);
+ if (!histSrv) {
+ return false;
+ }
+ nsCOMPtr<nsINavHistoryQuery> histQuery;
+ rv = histSrv->GetNewQuery(getter_AddRefs(histQuery));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return false;
+ }
+
+ rv = histQuery->SetDomain(baseDomain);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return false;
+ }
+
+ rv = histQuery->SetDomainIsHost(false);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return false;
+ }
+
+ PRTime beginTime = PR_Now() - PRTime(PR_USEC_PER_SEC) * aSinceInSec;
+ rv = histQuery->SetBeginTime(beginTime);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return false;
+ }
+
+ nsCOMPtr<nsINavHistoryQueryOptions> histQueryOpts;
+ rv = histSrv->GetNewQueryOptions(getter_AddRefs(histQueryOpts));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return false;
+ }
+
+ rv =
+ histQueryOpts->SetResultType(nsINavHistoryQueryOptions::RESULTS_AS_VISIT);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return false;
+ }
+
+ rv = histQueryOpts->SetMaxResults(1);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return false;
+ }
+
+ rv = histQueryOpts->SetQueryType(
+ nsINavHistoryQueryOptions::QUERY_TYPE_HISTORY);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return false;
+ }
+
+ nsCOMPtr<nsINavHistoryResult> histResult;
+ rv = histSrv->ExecuteQuery(histQuery, histQueryOpts,
+ getter_AddRefs(histResult));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return false;
+ }
+
+ nsCOMPtr<nsINavHistoryContainerResultNode> histResultContainer;
+ rv = histResult->GetRoot(getter_AddRefs(histResultContainer));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return false;
+ }
+
+ rv = histResultContainer->SetContainerOpen(true);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return false;
+ }
+
+ uint32_t childCount = 0;
+ rv = histResultContainer->GetChildCount(&childCount);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return false;
+ }
+
+ rv = histResultContainer->SetContainerOpen(false);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return false;
+ }
+
+ return childCount > 0;
+}
+
+void AddConsoleReport(nsIChannel* aNewChannel, nsIURI* aNewURI,
+ const nsACString& aOldOrigin,
+ const nsACString& aNewOrigin) {
+ nsCOMPtr<net::HttpBaseChannel> httpChannel = do_QueryInterface(aNewChannel);
+ if (!httpChannel) {
+ return;
+ }
+
+ nsAutoCString uri;
+ nsresult rv = aNewURI->GetSpec(uri);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+
+ AutoTArray<nsString, 2> params = {NS_ConvertUTF8toUTF16(aNewOrigin),
+ NS_ConvertUTF8toUTF16(aOldOrigin)};
+
+ httpChannel->AddConsoleReport(nsIScriptError::warningFlag,
+ ANTITRACKING_CONSOLE_CATEGORY,
+ nsContentUtils::eNECKO_PROPERTIES, uri, 0, 0,
+ "CookieAllowedForFpiByHeuristic"_ns, params);
+}
+
+bool ShouldRedirectHeuristicApplyTrackingResource(nsIChannel* aOldChannel,
+ nsIURI* aOldURI,
+ nsIChannel* aNewChannel,
+ nsIURI* aNewURI) {
+ nsCOMPtr<nsIClassifiedChannel> classifiedOldChannel =
+ do_QueryInterface(aOldChannel);
+ if (!classifiedOldChannel) {
+ LOG_SPEC2(("Ignoring redirect for %s to %s because there is not "
+ "nsIClassifiedChannel interface",
+ _spec1, _spec2),
+ aOldURI, aNewURI);
+ return false;
+ }
+
+ // We're looking at the first-party classification flags because we're
+ // interested in first-party redirects.
+ uint32_t oldClassificationFlags =
+ classifiedOldChannel->GetFirstPartyClassificationFlags();
+
+ if (net::UrlClassifierCommon::IsTrackingClassificationFlag(
+ oldClassificationFlags, NS_UsePrivateBrowsing(aOldChannel))) {
+ // This is a redirect from tracking.
+ LOG_SPEC2(("Ignoring redirect for %s to %s because it's from tracking ",
+ _spec1, _spec2),
+ aOldURI, aNewURI);
+ return false;
+ }
+
+ return true;
+}
+
+} // namespace
+
+void DynamicFpiRedirectHeuristic(nsIChannel* aOldChannel, nsIURI* aOldURI,
+ nsIChannel* aNewChannel, nsIURI* aNewURI) {
+ MOZ_ASSERT(aOldChannel);
+ MOZ_ASSERT(aOldURI);
+ MOZ_ASSERT(aNewChannel);
+ MOZ_ASSERT(aNewURI);
+
+ nsresult rv;
+
+ if (!StaticPrefs::privacy_antitracking_enableWebcompat() ||
+ !StaticPrefs::
+ privacy_restrict3rdpartystorage_heuristic_recently_visited()) {
+ return;
+ }
+
+ nsCOMPtr<nsIHttpChannel> oldChannel = do_QueryInterface(aOldChannel);
+ nsCOMPtr<nsIHttpChannel> newChannel = do_QueryInterface(aNewChannel);
+ if (!oldChannel || !newChannel) {
+ return;
+ }
+
+ LOG_SPEC(("Checking dfpi redirect-heuristic for %s", _spec), aOldURI);
+
+ nsCOMPtr<nsILoadInfo> oldLoadInfo = aOldChannel->LoadInfo();
+ MOZ_ASSERT(oldLoadInfo);
+
+ nsCOMPtr<nsILoadInfo> newLoadInfo = aNewChannel->LoadInfo();
+ MOZ_ASSERT(newLoadInfo);
+
+ ExtContentPolicyType contentType =
+ oldLoadInfo->GetExternalContentPolicyType();
+ if (contentType != ExtContentPolicy::TYPE_DOCUMENT ||
+ !aOldChannel->IsDocument()) {
+ LOG_SPEC(("Ignoring redirect for %s because it's not a document", _spec),
+ aOldURI);
+ // We care about document redirects only.
+ return;
+ }
+
+ nsCOMPtr<nsICookieJarSettings> cookieJarSettings;
+ rv = oldLoadInfo->GetCookieJarSettings(getter_AddRefs(cookieJarSettings));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ LOG(("Can't get the cookieJarSettings"));
+ return;
+ }
+
+ int32_t behavior = cookieJarSettings->GetCookieBehavior();
+ if (behavior !=
+ nsICookieService::BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN) {
+ LOG(
+ ("Disabled by network.cookie.cookieBehavior pref (%d), bailing out "
+ "early",
+ behavior));
+ return;
+ }
+
+ nsIScriptSecurityManager* ssm =
+ nsScriptSecurityManager::GetScriptSecurityManager();
+ MOZ_ASSERT(ssm);
+
+ nsCOMPtr<nsIPrincipal> oldPrincipal;
+ const nsTArray<nsCOMPtr<nsIRedirectHistoryEntry>>& chain =
+ oldLoadInfo->RedirectChain();
+ if (!chain.IsEmpty()) {
+ rv = chain[0]->GetPrincipal(getter_AddRefs(oldPrincipal));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ LOG(("Can't obtain the principal from the redirect chain"));
+ return;
+ }
+ } else {
+ rv = ssm->GetChannelResultPrincipal(aOldChannel,
+ getter_AddRefs(oldPrincipal));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ LOG(("Can't obtain the principal from the old channel"));
+ return;
+ }
+ }
+
+ nsCOMPtr<nsIPrincipal> newPrincipal;
+ rv =
+ ssm->GetChannelResultPrincipal(aNewChannel, getter_AddRefs(newPrincipal));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ LOG(("Can't obtain the principal from the new channel"));
+ return;
+ }
+
+ if (oldPrincipal->Equals(newPrincipal)) {
+ LOG(("No permission needed for same principals."));
+ return;
+ }
+
+ if (!ShouldRedirectHeuristicApplyTrackingResource(aOldChannel, aOldURI,
+ aNewChannel, aNewURI)) {
+ LOG_SPEC2(("Ignoring redirect for %s to %s because tracking test failed",
+ _spec1, _spec2),
+ aOldURI, aNewURI);
+ return;
+ }
+
+ if (!ContentBlockingUserInteraction::Exists(oldPrincipal) ||
+ !ContentBlockingUserInteraction::Exists(newPrincipal)) {
+ LOG_SPEC2(("Ignoring redirect for %s to %s because no user-interaction on "
+ "both pages",
+ _spec1, _spec2),
+ aOldURI, aNewURI);
+ return;
+ }
+
+ nsAutoCString oldOrigin;
+ rv = oldPrincipal->GetOrigin(oldOrigin);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ LOG(("Can't get the origin from the Principal"));
+ return;
+ }
+
+ nsAutoCString newOrigin;
+ rv = nsContentUtils::GetASCIIOrigin(aNewURI, newOrigin);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ LOG(("Can't get the origin from the URI"));
+ return;
+ }
+
+ if (!HasEligibleVisit(aOldURI) || !HasEligibleVisit(aNewURI)) {
+ LOG(("No previous visit record, bailing out early."));
+ return;
+ }
+
+ LOG(("Adding a first-party storage exception for %s...",
+ PromiseFlatCString(newOrigin).get()));
+
+ LOG(("Saving the permission: oldOrigin=%s, grantedOrigin=%s", oldOrigin.get(),
+ newOrigin.get()));
+
+ AddConsoleReport(aNewChannel, aNewURI, oldOrigin, newOrigin);
+
+ Telemetry::AccumulateCategorical(
+ Telemetry::LABELS_STORAGE_ACCESS_GRANTED_COUNT::StorageGranted);
+ Telemetry::AccumulateCategorical(
+ Telemetry::LABELS_STORAGE_ACCESS_GRANTED_COUNT::Redirect);
+
+ // We don't care about this promise because the operation is actually sync.
+ RefPtr<StorageAccessAPIHelper::ParentAccessGrantPromise> promise =
+ StorageAccessAPIHelper::SaveAccessForOriginOnParentProcess(
+ newPrincipal, oldPrincipal,
+ StorageAccessAPIHelper::StorageAccessPromptChoices::eAllow,
+ StaticPrefs::privacy_restrict3rdpartystorage_expiration_visited());
+ Unused << promise;
+}
+
+} // namespace mozilla
diff --git a/toolkit/components/antitracking/DynamicFpiRedirectHeuristic.h b/toolkit/components/antitracking/DynamicFpiRedirectHeuristic.h
new file mode 100644
index 0000000000..cedb438cd1
--- /dev/null
+++ b/toolkit/components/antitracking/DynamicFpiRedirectHeuristic.h
@@ -0,0 +1,20 @@
+/* -*- 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_dynamicfpiredirectheuristic_h
+#define mozilla_dynamicfpiredirectheuristic_h
+
+class nsIChannel;
+class nsIURI;
+
+namespace mozilla {
+
+void DynamicFpiRedirectHeuristic(nsIChannel* aOldChannel, nsIURI* aOldURI,
+ nsIChannel* aNewChannel, nsIURI* aNewURI);
+
+} // namespace mozilla
+
+#endif // mozilla_dynamicfpiredirectheuristic_h
diff --git a/toolkit/components/antitracking/PartitioningExceptionList.cpp b/toolkit/components/antitracking/PartitioningExceptionList.cpp
new file mode 100644
index 0000000000..91cc40133c
--- /dev/null
+++ b/toolkit/components/antitracking/PartitioningExceptionList.cpp
@@ -0,0 +1,221 @@
+/* -*- 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 "PartitioningExceptionList.h"
+
+#include "AntiTrackingLog.h"
+#include "nsContentUtils.h"
+#include "nsServiceManagerUtils.h"
+
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/StaticPtr.h"
+
+namespace mozilla {
+
+namespace {
+
+static constexpr std::array<nsLiteralCString, 2> kSupportedSchemes = {
+ {"https://"_ns, "http://"_ns}};
+
+StaticRefPtr<PartitioningExceptionList> gPartitioningExceptionList;
+
+} // namespace
+
+NS_IMPL_ISUPPORTS(PartitioningExceptionList,
+ nsIPartitioningExceptionListObserver)
+
+bool PartitioningExceptionList::Check(const nsACString& aFirstPartyOrigin,
+ const nsACString& aThirdPartyOrigin) {
+ if (!StaticPrefs::privacy_antitracking_enableWebcompat()) {
+ LOG(("Partition exception list disabled via pref"));
+ return false;
+ }
+
+ if (aFirstPartyOrigin.IsEmpty() || aFirstPartyOrigin == "null" ||
+ aThirdPartyOrigin.IsEmpty() || aThirdPartyOrigin == "null") {
+ return false;
+ }
+
+ LOG(("Check partitioning exception list for url %s and %s",
+ PromiseFlatCString(aFirstPartyOrigin).get(),
+ PromiseFlatCString(aThirdPartyOrigin).get()));
+
+ for (PartitionExceptionListEntry& entry : GetOrCreate()->mExceptionList) {
+ if (OriginMatchesPattern(aFirstPartyOrigin, entry.mFirstParty) &&
+ OriginMatchesPattern(aThirdPartyOrigin, entry.mThirdParty)) {
+ LOG(("Found partitioning exception list entry for %s and %s",
+ PromiseFlatCString(aFirstPartyOrigin).get(),
+ PromiseFlatCString(aThirdPartyOrigin).get()));
+
+ return true;
+ }
+ }
+
+ return false;
+}
+
+PartitioningExceptionList* PartitioningExceptionList::GetOrCreate() {
+ if (!gPartitioningExceptionList) {
+ gPartitioningExceptionList = new PartitioningExceptionList();
+ gPartitioningExceptionList->Init();
+
+ RunOnShutdown([&] {
+ gPartitioningExceptionList->Shutdown();
+ gPartitioningExceptionList = nullptr;
+ });
+ }
+
+ return gPartitioningExceptionList;
+}
+
+nsresult PartitioningExceptionList::Init() {
+ mService =
+ do_GetService("@mozilla.org/partitioning/exception-list-service;1");
+ if (NS_WARN_IF(!mService)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ mService->RegisterAndRunExceptionListObserver(this);
+ return NS_OK;
+}
+
+void PartitioningExceptionList::Shutdown() {
+ if (mService) {
+ mService->UnregisterExceptionListObserver(this);
+ mService = nullptr;
+ }
+
+ mExceptionList.Clear();
+}
+
+NS_IMETHODIMP
+PartitioningExceptionList::OnExceptionListUpdate(const nsACString& aList) {
+ mExceptionList.Clear();
+
+ nsresult rv;
+ for (const nsACString& item : aList.Split(';')) {
+ auto origins = item.Split(',');
+ auto originsIt = origins.begin();
+
+ if (originsIt == origins.end()) {
+ LOG(("Ignoring empty exception entry"));
+ continue;
+ }
+
+ PartitionExceptionListEntry entry;
+
+ rv = GetExceptionListPattern(*originsIt, entry.mFirstParty);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ continue;
+ }
+
+ ++originsIt;
+
+ if (originsIt == origins.end()) {
+ LOG(("Ignoring incomplete exception entry"));
+ continue;
+ }
+
+ rv = GetExceptionListPattern(*originsIt, entry.mThirdParty);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ continue;
+ }
+
+ if (entry.mFirstParty.mSuffix == "*" && entry.mThirdParty.mSuffix == "*") {
+ LOG(("Ignoring *,* exception entry"));
+ continue;
+ }
+
+ LOG(("onExceptionListUpdate: %s%s - %s%s", entry.mFirstParty.mScheme.get(),
+ entry.mFirstParty.mSuffix.get(), entry.mThirdParty.mScheme.get(),
+ entry.mThirdParty.mSuffix.get()));
+
+ mExceptionList.AppendElement(entry);
+ }
+
+ return NS_OK;
+}
+
+nsresult PartitioningExceptionList::GetSchemeFromOrigin(
+ const nsACString& aOrigin, nsACString& aScheme,
+ nsACString& aOriginNoScheme) {
+ NS_ENSURE_FALSE(aOrigin.IsEmpty(), NS_ERROR_INVALID_ARG);
+
+ for (const auto& scheme : kSupportedSchemes) {
+ if (aOrigin.Length() <= scheme.Length() ||
+ !StringBeginsWith(aOrigin, scheme)) {
+ continue;
+ }
+ aScheme = Substring(aOrigin, 0, scheme.Length());
+ aOriginNoScheme = Substring(aOrigin, scheme.Length());
+ return NS_OK;
+ }
+
+ return NS_ERROR_FAILURE;
+}
+
+bool PartitioningExceptionList::OriginMatchesPattern(
+ const nsACString& aOrigin, const PartitionExceptionListPattern& aPattern) {
+ if (NS_WARN_IF(aOrigin.IsEmpty())) {
+ return false;
+ }
+
+ if (aPattern.mSuffix == "*") {
+ return true;
+ }
+
+ nsAutoCString scheme, originNoScheme;
+ nsresult rv = GetSchemeFromOrigin(aOrigin, scheme, originNoScheme);
+ NS_ENSURE_SUCCESS(rv, false);
+
+ // Always strict match scheme.
+ if (scheme != aPattern.mScheme) {
+ return false;
+ }
+
+ if (!aPattern.mIsWildCard) {
+ // aPattern is not a wildcard, match strict.
+ return originNoScheme == aPattern.mSuffix;
+ }
+
+ // For wildcard patterns, check if origin suffix matches pattern suffix.
+ return StringEndsWith(originNoScheme, aPattern.mSuffix);
+}
+
+// Parses a string with an origin or an origin-pattern into a
+// PartitionExceptionListPattern.
+nsresult PartitioningExceptionList::GetExceptionListPattern(
+ const nsACString& aOriginPattern, PartitionExceptionListPattern& aPattern) {
+ NS_ENSURE_FALSE(aOriginPattern.IsEmpty(), NS_ERROR_INVALID_ARG);
+
+ if (aOriginPattern == "*") {
+ aPattern.mIsWildCard = true;
+ aPattern.mSuffix = "*";
+
+ return NS_OK;
+ }
+
+ nsAutoCString originPatternNoScheme;
+ nsresult rv = GetSchemeFromOrigin(aOriginPattern, aPattern.mScheme,
+ originPatternNoScheme);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (StringBeginsWith(originPatternNoScheme, "*"_ns)) {
+ NS_ENSURE_TRUE(originPatternNoScheme.Length() > 2, NS_ERROR_INVALID_ARG);
+
+ aPattern.mIsWildCard = true;
+ aPattern.mSuffix = Substring(originPatternNoScheme, 1);
+
+ return NS_OK;
+ }
+
+ aPattern.mIsWildCard = false;
+ aPattern.mSuffix = originPatternNoScheme;
+
+ return NS_OK;
+}
+
+} // namespace mozilla
diff --git a/toolkit/components/antitracking/PartitioningExceptionList.h b/toolkit/components/antitracking/PartitioningExceptionList.h
new file mode 100644
index 0000000000..250a636340
--- /dev/null
+++ b/toolkit/components/antitracking/PartitioningExceptionList.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_PartitioningExceptionList_h
+#define mozilla_PartitioningExceptionList_h
+
+#include "nsCOMPtr.h"
+#include "nsIPartitioningExceptionListService.h"
+#include "nsTArray.h"
+#include "nsString.h"
+
+class nsIChannel;
+class nsIPrincipal;
+
+namespace mozilla {
+
+class PartitioningExceptionList : public nsIPartitioningExceptionListObserver {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIPARTITIONINGEXCEPTIONLISTOBSERVER
+
+ static bool Check(const nsACString& aFirstPartyOrigin,
+ const nsACString& aThirdPartyOrigin);
+
+ private:
+ static PartitioningExceptionList* GetOrCreate();
+
+ PartitioningExceptionList() = default;
+ virtual ~PartitioningExceptionList() = default;
+
+ nsresult Init();
+ void Shutdown();
+
+ struct PartitionExceptionListPattern {
+ nsCString mScheme;
+ nsCString mSuffix;
+ bool mIsWildCard = false;
+ };
+
+ struct PartitionExceptionListEntry {
+ PartitionExceptionListPattern mFirstParty;
+ PartitionExceptionListPattern mThirdParty;
+ };
+
+ static nsresult GetSchemeFromOrigin(const nsACString& aOrigin,
+ nsACString& aScheme,
+ nsACString& aOriginNoScheme);
+
+ static bool OriginMatchesPattern(
+ const nsACString& aOrigin, const PartitionExceptionListPattern& aPattern);
+
+ static nsresult GetExceptionListPattern(
+ const nsACString& aOriginPattern,
+ PartitionExceptionListPattern& aPattern);
+
+ nsCOMPtr<nsIPartitioningExceptionListService> mService;
+ nsTArray<PartitionExceptionListEntry> mExceptionList;
+};
+
+} // namespace mozilla
+
+#endif // mozilla_PartitioningExceptionList_h
diff --git a/toolkit/components/antitracking/PartitioningExceptionListService.sys.mjs b/toolkit/components/antitracking/PartitioningExceptionListService.sys.mjs
new file mode 100644
index 0000000000..8df2bc651d
--- /dev/null
+++ b/toolkit/components/antitracking/PartitioningExceptionListService.sys.mjs
@@ -0,0 +1,142 @@
+/* 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/. */
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ RemoteSettings: "resource://services-settings/remote-settings.sys.mjs",
+});
+
+const COLLECTION_NAME = "partitioning-exempt-urls";
+const PREF_NAME = "privacy.restrict3rdpartystorage.skip_list";
+
+class Feature {
+ constructor() {
+ this.prefName = PREF_NAME;
+ this.observers = new Set();
+ this.prefValue = [];
+ this.remoteEntries = [];
+
+ if (this.prefName) {
+ let prefValue = Services.prefs.getStringPref(this.prefName, null);
+ this.prefValue = prefValue ? prefValue.split(";") : [];
+ Services.prefs.addObserver(this.prefName, this);
+ }
+ }
+
+ async addAndRunObserver(observer) {
+ this.observers.add(observer);
+ this.notifyObservers(observer);
+ }
+
+ removeObserver(observer) {
+ this.observers.delete(observer);
+ }
+
+ observe(subject, topic, data) {
+ if (topic != "nsPref:changed" || data != this.prefName) {
+ console.error(`Unexpected event ${topic} with ${data}`);
+ return;
+ }
+
+ let prefValue = Services.prefs.getStringPref(this.prefName, null);
+ this.prefValue = prefValue ? prefValue.split(";") : [];
+ this.notifyObservers();
+ }
+
+ onRemoteSettingsUpdate(entries) {
+ this.remoteEntries = [];
+
+ for (let entry of entries) {
+ this.remoteEntries.push(
+ `${entry.firstPartyOrigin},${entry.thirdPartyOrigin}`
+ );
+ }
+ }
+
+ notifyObservers(observer = null) {
+ let entries = this.prefValue.concat(this.remoteEntries);
+ let entriesAsString = entries.join(";").toLowerCase();
+ if (observer) {
+ observer.onExceptionListUpdate(entriesAsString);
+ } else {
+ for (let obs of this.observers) {
+ obs.onExceptionListUpdate(entriesAsString);
+ }
+ }
+ }
+}
+
+export function PartitioningExceptionListService() {}
+
+PartitioningExceptionListService.prototype = {
+ classID: Components.ID("{ab94809d-33f0-4f28-af38-01efbd3baf22}"),
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIPartitioningExceptionListService",
+ ]),
+
+ _initialized: false,
+
+ async lazyInit() {
+ if (this._initialized) {
+ return;
+ }
+
+ this.feature = new Feature();
+
+ let rs = lazy.RemoteSettings(COLLECTION_NAME);
+ rs.on("sync", event => {
+ let {
+ data: { current },
+ } = event;
+ this.onUpdateEntries(current);
+ });
+
+ this._initialized = true;
+
+ let entries;
+ // If the remote settings list hasn't been populated yet we have to make sure
+ // to do it before firing the first notification.
+ // This has to be run after _initialized is set because we'll be
+ // blocked while getting entries from RemoteSetting, and we don't want
+ // LazyInit is executed again.
+ try {
+ // The data will be initially available from the local DB (via a
+ // resource:// URI).
+ entries = await rs.get();
+ } catch (e) {}
+
+ // RemoteSettings.get() could return null, ensure passing a list to
+ // onUpdateEntries.
+ this.onUpdateEntries(entries || []);
+ },
+
+ onUpdateEntries(entries) {
+ if (!this.feature) {
+ return;
+ }
+ this.feature.onRemoteSettingsUpdate(entries);
+ this.feature.notifyObservers();
+ },
+
+ registerAndRunExceptionListObserver(observer) {
+ // We don't await this; the caller is C++ and won't await this function,
+ // and because we prevent re-entering into this method, once it's been
+ // called once any subsequent calls will early-return anyway - so
+ // awaiting that would be meaningless. Instead, `Feature` implementations
+ // make sure not to call into observers until they have data, and we
+ // make sure to let feature instances know whether we have data
+ // immediately.
+ this.lazyInit();
+
+ this.feature.addAndRunObserver(observer);
+ },
+
+ unregisterExceptionListObserver(observer) {
+ if (!this.feature) {
+ return;
+ }
+ this.feature.removeObserver(observer);
+ },
+};
diff --git a/toolkit/components/antitracking/PurgeTrackerService.sys.mjs b/toolkit/components/antitracking/PurgeTrackerService.sys.mjs
new file mode 100644
index 0000000000..42314954a7
--- /dev/null
+++ b/toolkit/components/antitracking/PurgeTrackerService.sys.mjs
@@ -0,0 +1,471 @@
+/* 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/. */
+
+import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
+
+const THREE_DAYS_MS = 3 * 24 * 60 * 1000;
+
+const lazy = {};
+
+XPCOMUtils.defineLazyServiceGetter(
+ lazy,
+ "gClassifier",
+ "@mozilla.org/url-classifier/dbservice;1",
+ "nsIURIClassifier"
+);
+XPCOMUtils.defineLazyServiceGetter(
+ lazy,
+ "gStorageActivityService",
+ "@mozilla.org/storage/activity-service;1",
+ "nsIStorageActivityService"
+);
+
+XPCOMUtils.defineLazyGetter(lazy, "gClassifierFeature", () => {
+ return lazy.gClassifier.getFeatureByName("tracking-annotation");
+});
+
+XPCOMUtils.defineLazyGetter(lazy, "logger", () => {
+ return console.createInstance({
+ prefix: "*** PurgeTrackerService:",
+ maxLogLevelPref: "privacy.purge_trackers.logging.level",
+ });
+});
+
+XPCOMUtils.defineLazyPreferenceGetter(
+ lazy,
+ "gConsiderEntityList",
+ "privacy.purge_trackers.consider_entity_list"
+);
+
+export function PurgeTrackerService() {}
+
+PurgeTrackerService.prototype = {
+ classID: Components.ID("{90d1fd17-2018-4e16-b73c-a04a26fa6dd4}"),
+ QueryInterface: ChromeUtils.generateQI(["nsIPurgeTrackerService"]),
+
+ // Purging is batched for cookies to avoid clearing too much data
+ // at once. This flag tells us whether this is the first daily iteration.
+ _firstIteration: true,
+
+ // We can only know asynchronously if a host is matched by the tracking
+ // protection list, so we cache the result for faster future lookups.
+ _trackingState: new Map(),
+
+ observe(aSubject, aTopic, aData) {
+ switch (aTopic) {
+ case "idle-daily":
+ // only allow one idle-daily listener to trigger until the list has been fully parsed.
+ Services.obs.removeObserver(this, "idle-daily");
+ this.purgeTrackingCookieJars();
+ break;
+ case "profile-after-change":
+ Services.obs.addObserver(this, "idle-daily");
+ break;
+ }
+ },
+
+ async isTracker(principal) {
+ if (principal.isNullPrincipal || principal.isSystemPrincipal) {
+ return false;
+ }
+ let host;
+ try {
+ host = principal.asciiHost;
+ } catch (error) {
+ return false;
+ }
+
+ if (!this._trackingState.has(host)) {
+ // Temporarily set to false to avoid doing several lookups if a site has
+ // several subframes on the same domain.
+ this._trackingState.set(host, false);
+
+ await new Promise(resolve => {
+ try {
+ lazy.gClassifier.asyncClassifyLocalWithFeatures(
+ principal.URI,
+ [lazy.gClassifierFeature],
+ Ci.nsIUrlClassifierFeature.blocklist,
+ list => {
+ if (list.length) {
+ this._trackingState.set(host, true);
+ }
+ resolve();
+ }
+ );
+ } catch {
+ // Error in asyncClassifyLocalWithFeatures, it is not a tracker.
+ this._trackingState.set(host, false);
+ resolve();
+ }
+ });
+ }
+
+ return this._trackingState.get(host);
+ },
+
+ isAllowedThirdParty(firstPartyOriginNoSuffix, thirdPartyHost) {
+ let uri = Services.io.newURI(
+ `${firstPartyOriginNoSuffix}/?resource=${thirdPartyHost}`
+ );
+ lazy.logger.debug(`Checking entity list state for`, uri.spec);
+ return new Promise(resolve => {
+ try {
+ lazy.gClassifier.asyncClassifyLocalWithFeatures(
+ uri,
+ [lazy.gClassifierFeature],
+ Ci.nsIUrlClassifierFeature.entitylist,
+ list => {
+ let sameList = !!list.length;
+ lazy.logger.debug(`Is ${uri.spec} on the entity list?`, sameList);
+ resolve(sameList);
+ }
+ );
+ } catch {
+ resolve(false);
+ }
+ });
+ },
+
+ async maybePurgePrincipal(principal) {
+ let origin = principal.origin;
+ lazy.logger.debug(`Maybe purging ${origin}.`);
+
+ // First, check if any site with that base domain had received
+ // user interaction in the last N days.
+ let hasInteraction = this._baseDomainsWithInteraction.has(
+ principal.baseDomain
+ );
+ // Exit early unless we want to see if we're dealing with a tracker,
+ // for telemetry.
+ if (hasInteraction && !Services.telemetry.canRecordPrereleaseData) {
+ lazy.logger.debug(`${origin} has user interaction, exiting.`);
+ return;
+ }
+
+ // Second, confirm that we're looking at a tracker.
+ let isTracker = await this.isTracker(principal);
+ if (!isTracker) {
+ lazy.logger.debug(`${origin} is not a tracker, exiting.`);
+ return;
+ }
+
+ if (hasInteraction) {
+ let expireTimeMs = this._baseDomainsWithInteraction.get(
+ principal.baseDomain
+ );
+
+ // Collect how much longer the user interaction will be valid for, in hours.
+ let timeRemaining = Math.floor(
+ (expireTimeMs - Date.now()) / 1000 / 60 / 60 / 24
+ );
+ let permissionAgeHistogram = Services.telemetry.getHistogramById(
+ "COOKIE_PURGING_TRACKERS_USER_INTERACTION_REMAINING_DAYS"
+ );
+ permissionAgeHistogram.add(timeRemaining);
+
+ this._telemetryData.notPurged.add(principal.baseDomain);
+
+ lazy.logger.debug(`${origin} is a tracker with interaction, exiting.`);
+ return;
+ }
+
+ let isAllowedThirdParty = false;
+ if (
+ lazy.gConsiderEntityList ||
+ Services.telemetry.canRecordPrereleaseData
+ ) {
+ for (let firstPartyPrincipal of this._principalsWithInteraction) {
+ if (
+ await this.isAllowedThirdParty(
+ firstPartyPrincipal.originNoSuffix,
+ principal.asciiHost
+ )
+ ) {
+ isAllowedThirdParty = true;
+ break;
+ }
+ }
+ }
+
+ if (isAllowedThirdParty && lazy.gConsiderEntityList) {
+ lazy.logger.debug(
+ `${origin} has interaction on the entity list, exiting.`
+ );
+ return;
+ }
+
+ lazy.logger.log("Deleting data from:", origin);
+
+ await new Promise(resolve => {
+ Services.clearData.deleteDataFromPrincipal(
+ principal,
+ false,
+ Ci.nsIClearDataService.CLEAR_ALL_CACHES |
+ Ci.nsIClearDataService.CLEAR_COOKIES |
+ Ci.nsIClearDataService.CLEAR_DOM_STORAGES |
+ Ci.nsIClearDataService.CLEAR_CLIENT_AUTH_REMEMBER_SERVICE |
+ Ci.nsIClearDataService.CLEAR_EME |
+ Ci.nsIClearDataService.CLEAR_MEDIA_DEVICES |
+ Ci.nsIClearDataService.CLEAR_STORAGE_ACCESS |
+ Ci.nsIClearDataService.CLEAR_AUTH_TOKENS |
+ Ci.nsIClearDataService.CLEAR_AUTH_CACHE,
+ resolve
+ );
+ });
+ lazy.logger.log(`Data deleted from:`, origin);
+
+ this._telemetryData.purged.add(principal.baseDomain);
+ },
+
+ resetPurgeList() {
+ // We've reached the end of the cookies.
+ // Restore the idle-daily listener so it will purge again tomorrow.
+ Services.obs.addObserver(this, "idle-daily");
+ // Set the date to 0 so we will start at the beginning of the list next time.
+ Services.prefs.setStringPref(
+ "privacy.purge_trackers.date_in_cookie_database",
+ "0"
+ );
+ },
+
+ submitTelemetry() {
+ let { purged, notPurged, durationIntervals } = this._telemetryData;
+ let now = Date.now();
+ let lastPurge = Number(
+ Services.prefs.getStringPref("privacy.purge_trackers.last_purge", now)
+ );
+
+ let intervalHistogram = Services.telemetry.getHistogramById(
+ "COOKIE_PURGING_INTERVAL_HOURS"
+ );
+ let hoursBetween = Math.floor((now - lastPurge) / 1000 / 60 / 60);
+ intervalHistogram.add(hoursBetween);
+
+ Services.prefs.setStringPref(
+ "privacy.purge_trackers.last_purge",
+ now.toString()
+ );
+
+ let purgedHistogram = Services.telemetry.getHistogramById(
+ "COOKIE_PURGING_ORIGINS_PURGED"
+ );
+ purgedHistogram.add(purged.size);
+
+ let notPurgedHistogram = Services.telemetry.getHistogramById(
+ "COOKIE_PURGING_TRACKERS_WITH_USER_INTERACTION"
+ );
+ notPurgedHistogram.add(notPurged.size);
+
+ let duration = durationIntervals
+ .map(([start, end]) => end - start)
+ .reduce((acc, cur) => acc + cur, 0);
+
+ let durationHistogram = Services.telemetry.getHistogramById(
+ "COOKIE_PURGING_DURATION_MS"
+ );
+ durationHistogram.add(duration);
+ },
+
+ /**
+ * This loops through all cookies saved in the database and checks if they are a tracking cookie, if it is it checks
+ * that they have an interaction permission which is still valid. If the Permission is not valid we delete all data
+ * associated with the site that owns that cookie.
+ */
+ async purgeTrackingCookieJars() {
+ let purgeEnabled = Services.prefs.getBoolPref(
+ "privacy.purge_trackers.enabled",
+ false
+ );
+
+ let sanitizeOnShutdownEnabled = Services.prefs.getBoolPref(
+ "privacy.sanitize.sanitizeOnShutdown",
+ false
+ );
+
+ let clearHistoryOnShutdown = Services.prefs.getBoolPref(
+ "privacy.clearOnShutdown.history",
+ false
+ );
+
+ let clearSiteSettingsOnShutdown = Services.prefs.getBoolPref(
+ "privacy.clearOnShutdown.siteSettings",
+ false
+ );
+
+ // This is a hotfix for bug 1672394. It avoids purging if the user has enabled mechanisms
+ // that regularly clear the storageAccessAPI permission, such as clearing history or
+ // "site settings" (permissions) on shutdown.
+ if (
+ sanitizeOnShutdownEnabled &&
+ (clearHistoryOnShutdown || clearSiteSettingsOnShutdown)
+ ) {
+ lazy.logger.log(
+ `
+ Purging canceled because interaction permissions are cleared on shutdown.
+ sanitizeOnShutdownEnabled: ${sanitizeOnShutdownEnabled},
+ clearHistoryOnShutdown: ${clearHistoryOnShutdown},
+ clearSiteSettingsOnShutdown: ${clearSiteSettingsOnShutdown},
+ `
+ );
+ this.resetPurgeList();
+ return;
+ }
+
+ // Purge cookie jars for following cookie behaviors.
+ // * BEHAVIOR_REJECT_FOREIGN
+ // * BEHAVIOR_LIMIT_FOREIGN
+ // * BEHAVIOR_REJECT_TRACKER (ETP)
+ // * BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN (dFPI)
+ let cookieBehavior = Services.cookies.getCookieBehavior(false);
+
+ let activeWithCookieBehavior =
+ cookieBehavior == Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN ||
+ cookieBehavior == Ci.nsICookieService.BEHAVIOR_LIMIT_FOREIGN ||
+ cookieBehavior == Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER ||
+ cookieBehavior ==
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN;
+
+ if (!activeWithCookieBehavior || !purgeEnabled) {
+ lazy.logger.log(
+ `returning early, activeWithCookieBehavior: ${activeWithCookieBehavior}, purgeEnabled: ${purgeEnabled}`
+ );
+ this.resetPurgeList();
+ return;
+ }
+ lazy.logger.log("Purging trackers enabled, beginning batch.");
+ // How many cookies to loop through in each batch before we quit
+ const MAX_PURGE_COUNT = Services.prefs.getIntPref(
+ "privacy.purge_trackers.max_purge_count",
+ 100
+ );
+
+ if (this._firstIteration) {
+ this._telemetryData = {
+ durationIntervals: [],
+ purged: new Set(),
+ notPurged: new Set(),
+ };
+
+ this._baseDomainsWithInteraction = new Map();
+ this._principalsWithInteraction = [];
+ for (let perm of Services.perms.getAllWithTypePrefix(
+ "storageAccessAPI"
+ )) {
+ this._baseDomainsWithInteraction.set(
+ perm.principal.baseDomain,
+ perm.expireTime
+ );
+ this._principalsWithInteraction.push(perm.principal);
+ }
+ }
+
+ // Record how long this iteration took for telemetry.
+ // This is a tuple of start and end time, the second
+ // part will be added at the end of this function.
+ let duration = [Cu.now()];
+
+ /**
+ * We record the creationTime of the last cookie we looked at and
+ * start from there next time. This way even if new cookies are added or old ones are deleted we
+ * have a reliable way of finding our spot.
+ **/
+ let saved_date = Services.prefs.getStringPref(
+ "privacy.purge_trackers.date_in_cookie_database",
+ "0"
+ );
+
+ let maybeClearPrincipals = new Map();
+
+ // TODO We only need the host name and creationTime, this gives too much info. See bug 1610373.
+ let cookies = Services.cookies.getCookiesSince(saved_date);
+ cookies = cookies.slice(0, MAX_PURGE_COUNT);
+
+ for (let cookie of cookies) {
+ let httpPrincipal;
+ let httpsPrincipal;
+
+ let origin =
+ "http://" +
+ cookie.rawHost +
+ ChromeUtils.originAttributesToSuffix(cookie.originAttributes);
+ try {
+ httpPrincipal =
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(
+ origin
+ );
+ } catch (e) {
+ lazy.logger.error(
+ `Creating principal from origin ${origin} led to error ${e}.`
+ );
+ }
+ if (httpPrincipal) {
+ maybeClearPrincipals.set(httpPrincipal.origin, httpPrincipal);
+ }
+
+ origin =
+ "https://" +
+ cookie.rawHost +
+ ChromeUtils.originAttributesToSuffix(cookie.originAttributes);
+ try {
+ httpsPrincipal =
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(
+ origin
+ );
+ } catch (e) {
+ lazy.logger.error(
+ `Creating principal from origin ${origin} led to error ${e}.`
+ );
+ }
+ if (httpsPrincipal) {
+ maybeClearPrincipals.set(httpsPrincipal.origin, httpsPrincipal);
+ }
+
+ saved_date = cookie.creationTime;
+ }
+
+ // We only consider recently active storage and don't batch it,
+ // so only do this in the first iteration.
+ if (this._firstIteration) {
+ let startDate = Date.now() - THREE_DAYS_MS;
+ let storagePrincipals = lazy.gStorageActivityService.getActiveOrigins(
+ startDate * 1000,
+ Date.now() * 1000
+ );
+
+ for (let principal of storagePrincipals.enumerate()) {
+ maybeClearPrincipals.set(principal.origin, principal);
+ }
+ }
+
+ for (let principal of maybeClearPrincipals.values()) {
+ await this.maybePurgePrincipal(principal);
+ }
+
+ Services.prefs.setStringPref(
+ "privacy.purge_trackers.date_in_cookie_database",
+ saved_date
+ );
+
+ duration.push(Cu.now());
+ this._telemetryData.durationIntervals.push(duration);
+
+ // We've reached the end, no need to repeat again until next idle-daily.
+ if (!cookies.length || cookies.length < 100) {
+ lazy.logger.log(
+ "All cookie purging finished, resetting list until tomorrow."
+ );
+ this.resetPurgeList();
+ this.submitTelemetry();
+ this._firstIteration = true;
+ return;
+ }
+
+ lazy.logger.log("Batch finished, queueing next batch.");
+ this._firstIteration = false;
+ Services.tm.idleDispatchToMainThread(() => {
+ this.purgeTrackingCookieJars();
+ });
+ },
+};
diff --git a/toolkit/components/antitracking/RejectForeignAllowList.cpp b/toolkit/components/antitracking/RejectForeignAllowList.cpp
new file mode 100644
index 0000000000..3af0494558
--- /dev/null
+++ b/toolkit/components/antitracking/RejectForeignAllowList.cpp
@@ -0,0 +1,127 @@
+/* -*- 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 "RejectForeignAllowList.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/BasePrincipal.h"
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/StaticPtr.h"
+#include "nsIHttpChannel.h"
+#include "nsIURI.h"
+#include "nsNetUtil.h"
+#include "nsScriptSecurityManager.h"
+
+#define REJECTFOREIGNALLOWLIST_PREF "privacy.rejectForeign.allowList"_ns
+#define REJECTFOREIGNALLOWLIST_NAME "RejectForeignAllowList"_ns
+
+namespace mozilla {
+
+namespace {
+
+StaticRefPtr<RejectForeignAllowList> gRejectForeignAllowList;
+
+} // namespace
+
+// static
+bool RejectForeignAllowList::Check(dom::Document* aDocument) {
+ MOZ_ASSERT(aDocument);
+
+ nsIURI* documentURI = aDocument->GetDocumentURI();
+ if (!documentURI) {
+ return false;
+ }
+
+ return GetOrCreate()->CheckInternal(documentURI);
+}
+
+// static
+bool RejectForeignAllowList::Check(nsIHttpChannel* aChannel) {
+ MOZ_ASSERT(aChannel);
+
+ nsCOMPtr<nsIURI> channelURI;
+ nsresult rv = NS_GetFinalChannelURI(aChannel, getter_AddRefs(channelURI));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return false;
+ }
+
+ return GetOrCreate()->CheckInternal(channelURI);
+}
+
+// static
+bool RejectForeignAllowList::Check(nsIPrincipal* aPrincipal) {
+ return GetOrCreate()->CheckInternal(aPrincipal);
+}
+
+// static
+RejectForeignAllowList* RejectForeignAllowList::GetOrCreate() {
+ if (!gRejectForeignAllowList) {
+ gRejectForeignAllowList = new RejectForeignAllowList();
+
+ nsCOMPtr<nsIUrlClassifierExceptionListService> exceptionListService =
+ do_GetService("@mozilla.org/url-classifier/exception-list-service;1");
+ if (exceptionListService) {
+ exceptionListService->RegisterAndRunExceptionListObserver(
+ REJECTFOREIGNALLOWLIST_NAME, REJECTFOREIGNALLOWLIST_PREF,
+ gRejectForeignAllowList);
+ }
+
+ RunOnShutdown([exceptionListService] {
+ if (gRejectForeignAllowList) {
+ if (exceptionListService) {
+ exceptionListService->UnregisterExceptionListObserver(
+ REJECTFOREIGNALLOWLIST_NAME, gRejectForeignAllowList);
+ }
+ gRejectForeignAllowList = nullptr;
+ }
+ });
+ }
+
+ return gRejectForeignAllowList;
+}
+
+// static
+bool RejectForeignAllowList::Check(nsIURI* aURI) {
+ return GetOrCreate()->CheckInternal(aURI);
+}
+
+bool RejectForeignAllowList::CheckInternal(nsIURI* aURI) {
+ MOZ_ASSERT(aURI);
+ return nsContentUtils::IsURIInList(aURI, mList);
+}
+
+bool RejectForeignAllowList::CheckInternal(nsIPrincipal* aPrincipal) {
+ MOZ_ASSERT(aPrincipal);
+
+ auto* basePrin = BasePrincipal::Cast(aPrincipal);
+ if (!basePrin) {
+ return false;
+ }
+
+ bool result = false;
+ basePrin->IsURIInList(mList, &result);
+
+ return result;
+}
+
+NS_IMETHODIMP
+RejectForeignAllowList::OnExceptionListUpdate(const nsACString& aList) {
+ mList = aList;
+ return NS_OK;
+}
+
+RejectForeignAllowList::RejectForeignAllowList() = default;
+RejectForeignAllowList::~RejectForeignAllowList() = default;
+
+NS_INTERFACE_MAP_BEGIN(RejectForeignAllowList)
+ NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports,
+ nsIUrlClassifierExceptionListObserver)
+ NS_INTERFACE_MAP_ENTRY(nsIUrlClassifierExceptionListObserver)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_ADDREF(RejectForeignAllowList)
+NS_IMPL_RELEASE(RejectForeignAllowList)
+
+} // namespace mozilla
diff --git a/toolkit/components/antitracking/RejectForeignAllowList.h b/toolkit/components/antitracking/RejectForeignAllowList.h
new file mode 100644
index 0000000000..f767bf88fc
--- /dev/null
+++ b/toolkit/components/antitracking/RejectForeignAllowList.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_RejectForeignAllowList_h
+#define mozilla_RejectForeignAllowList_h
+
+#include "nsIUrlClassifierExceptionListService.h"
+#include "nsIPrincipal.h"
+
+class nsIHttpChannel;
+class nsIURI;
+
+namespace mozilla {
+
+namespace dom {
+class Document;
+}
+
+class RejectForeignAllowList final
+ : public nsIUrlClassifierExceptionListObserver {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIURLCLASSIFIEREXCEPTIONLISTOBSERVER
+
+ static bool Check(dom::Document* aDocument);
+ static bool Check(nsIHttpChannel* aChannel);
+ static bool Check(nsIPrincipal* aPrincipal);
+ static bool Check(nsIURI* aURI);
+
+ private:
+ static RejectForeignAllowList* GetOrCreate();
+
+ RejectForeignAllowList();
+ ~RejectForeignAllowList();
+
+ bool CheckInternal(nsIURI* aURI);
+ bool CheckInternal(nsIPrincipal* aPrincipal);
+
+ nsCString mList;
+};
+
+} // namespace mozilla
+
+#endif // mozilla_RejectForeignAllowList_h
diff --git a/toolkit/components/antitracking/SettingsChangeObserver.cpp b/toolkit/components/antitracking/SettingsChangeObserver.cpp
new file mode 100644
index 0000000000..eb2ba5bbd3
--- /dev/null
+++ b/toolkit/components/antitracking/SettingsChangeObserver.cpp
@@ -0,0 +1,116 @@
+/* -*- 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 "SettingsChangeObserver.h"
+#include "ContentBlockingUserInteraction.h"
+
+#include "mozilla/Services.h"
+#include "mozilla/Preferences.h"
+#include "nsIObserverService.h"
+#include "nsIPermission.h"
+#include "nsTArray.h"
+
+using namespace mozilla;
+
+namespace {
+
+UniquePtr<nsTArray<SettingsChangeObserver::AntiTrackingSettingsChangedCallback>>
+ gSettingsChangedCallbacks;
+
+}
+
+NS_IMPL_ISUPPORTS(SettingsChangeObserver, nsIObserver)
+
+NS_IMETHODIMP SettingsChangeObserver::Observe(nsISupports* aSubject,
+ const char* aTopic,
+ const char16_t* aData) {
+ if (!strcmp(aTopic, "xpcom-shutdown")) {
+ nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
+ if (obs) {
+ obs->RemoveObserver(this, "perm-added");
+ obs->RemoveObserver(this, "perm-changed");
+ obs->RemoveObserver(this, "perm-cleared");
+ obs->RemoveObserver(this, "perm-deleted");
+ obs->RemoveObserver(this, "xpcom-shutdown");
+
+ Preferences::UnregisterPrefixCallback(
+ SettingsChangeObserver::PrivacyPrefChanged,
+ "browser.contentblocking.");
+ Preferences::UnregisterPrefixCallback(
+ SettingsChangeObserver::PrivacyPrefChanged, "network.cookie.");
+ Preferences::UnregisterPrefixCallback(
+ SettingsChangeObserver::PrivacyPrefChanged, "privacy.");
+
+ gSettingsChangedCallbacks = nullptr;
+ }
+ } else {
+ nsCOMPtr<nsIPermission> perm = do_QueryInterface(aSubject);
+ if (perm) {
+ nsAutoCString type;
+ nsresult rv = perm->GetType(type);
+ if (NS_WARN_IF(NS_FAILED(rv)) || type.Equals(USER_INTERACTION_PERM)) {
+ // Ignore failures or notifications that have been sent because of
+ // user interactions.
+ return NS_OK;
+ }
+ }
+
+ RunAntiTrackingSettingsChangedCallbacks();
+ }
+
+ return NS_OK;
+}
+
+// static
+void SettingsChangeObserver::PrivacyPrefChanged(const char* aPref,
+ void* aClosure) {
+ RunAntiTrackingSettingsChangedCallbacks();
+}
+
+// static
+void SettingsChangeObserver::RunAntiTrackingSettingsChangedCallbacks() {
+ if (gSettingsChangedCallbacks) {
+ for (auto& callback : *gSettingsChangedCallbacks) {
+ callback();
+ }
+ }
+}
+
+// static
+void SettingsChangeObserver::OnAntiTrackingSettingsChanged(
+ const SettingsChangeObserver::AntiTrackingSettingsChangedCallback&
+ aCallback) {
+ static bool initialized = false;
+ if (!initialized) {
+ // It is possible that while we have some data in our cache, something
+ // changes in our environment that causes the anti-tracking checks below to
+ // change their response. Therefore, we need to clear our cache when we
+ // detect a related change.
+ Preferences::RegisterPrefixCallback(
+ SettingsChangeObserver::PrivacyPrefChanged, "browser.contentblocking.");
+ Preferences::RegisterPrefixCallback(
+ SettingsChangeObserver::PrivacyPrefChanged, "network.cookie.");
+ Preferences::RegisterPrefixCallback(
+ SettingsChangeObserver::PrivacyPrefChanged, "privacy.");
+
+ nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
+ if (obs) {
+ RefPtr<SettingsChangeObserver> observer = new SettingsChangeObserver();
+ obs->AddObserver(observer, "perm-added", false);
+ obs->AddObserver(observer, "perm-changed", false);
+ obs->AddObserver(observer, "perm-cleared", false);
+ obs->AddObserver(observer, "perm-deleted", false);
+ obs->AddObserver(observer, "xpcom-shutdown", false);
+ }
+
+ gSettingsChangedCallbacks =
+ MakeUnique<nsTArray<AntiTrackingSettingsChangedCallback>>();
+
+ initialized = true;
+ }
+
+ gSettingsChangedCallbacks->AppendElement(aCallback);
+}
diff --git a/toolkit/components/antitracking/SettingsChangeObserver.h b/toolkit/components/antitracking/SettingsChangeObserver.h
new file mode 100644
index 0000000000..961942deac
--- /dev/null
+++ b/toolkit/components/antitracking/SettingsChangeObserver.h
@@ -0,0 +1,39 @@
+/* -*- 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_settingschangeobserver_h
+#define mozilla_settingschangeobserver_h
+
+#include "nsIObserver.h"
+
+#include <functional>
+
+namespace mozilla {
+
+class SettingsChangeObserver final : public nsIObserver {
+ ~SettingsChangeObserver() = default;
+
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIOBSERVER
+
+ // This API allows consumers to get notified when the anti-tracking component
+ // settings change. After this callback is called, an anti-tracking check
+ // that has been previously performed with the same parameters may now return
+ // a different result.
+ using AntiTrackingSettingsChangedCallback = std::function<void()>;
+ static void OnAntiTrackingSettingsChanged(
+ const AntiTrackingSettingsChangedCallback& aCallback);
+
+ static void PrivacyPrefChanged(const char* aPref = nullptr, void* = nullptr);
+
+ private:
+ static void RunAntiTrackingSettingsChangedCallbacks();
+};
+
+} // namespace mozilla
+
+#endif // mozilla_settingschangeobserver_h
diff --git a/toolkit/components/antitracking/StorageAccess.cpp b/toolkit/components/antitracking/StorageAccess.cpp
new file mode 100644
index 0000000000..4bdd7eccc1
--- /dev/null
+++ b/toolkit/components/antitracking/StorageAccess.cpp
@@ -0,0 +1,916 @@
+/* -*- 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 "StorageAccess.h"
+
+#include "mozilla/BasePrincipal.h"
+#include "mozilla/Components.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/net/CookieJarSettings.h"
+#include "mozilla/PermissionManager.h"
+#include "mozilla/StaticPrefs_browser.h"
+#include "mozilla/StaticPrefs_network.h"
+#include "mozilla/StaticPrefs_privacy.h"
+#include "mozilla/StorageAccess.h"
+#include "nsAboutProtocolUtils.h"
+#include "nsContentUtils.h"
+#include "nsGlobalWindowInner.h"
+#include "nsICookiePermission.h"
+#include "nsICookieService.h"
+#include "nsICookieJarSettings.h"
+#include "nsIHttpChannel.h"
+#include "nsIPermission.h"
+#include "nsIWebProgressListener.h"
+#include "nsIClassifiedChannel.h"
+#include "nsNetUtil.h"
+#include "nsScriptSecurityManager.h"
+#include "nsSandboxFlags.h"
+#include "AntiTrackingUtils.h"
+#include "AntiTrackingLog.h"
+#include "ContentBlockingAllowList.h"
+#include "mozIThirdPartyUtil.h"
+#include "RejectForeignAllowList.h"
+
+using namespace mozilla;
+using namespace mozilla::dom;
+using mozilla::net::CookieJarSettings;
+
+// This internal method returns ACCESS_DENY if the access is denied,
+// ACCESS_DEFAULT if unknown, some other access code if granted.
+uint32_t mozilla::detail::CheckCookiePermissionForPrincipal(
+ nsICookieJarSettings* aCookieJarSettings, nsIPrincipal* aPrincipal) {
+ MOZ_ASSERT(aCookieJarSettings);
+ MOZ_ASSERT(aPrincipal);
+
+ uint32_t cookiePermission = nsICookiePermission::ACCESS_DEFAULT;
+ if (!aPrincipal->GetIsContentPrincipal()) {
+ return cookiePermission;
+ }
+
+ nsresult rv =
+ aCookieJarSettings->CookiePermission(aPrincipal, &cookiePermission);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return nsICookiePermission::ACCESS_DEFAULT;
+ }
+
+ // If we have a custom cookie permission, let's use it.
+ return cookiePermission;
+}
+
+/*
+ * Checks if storage for a given principal is permitted by the user's
+ * preferences. If aWindow is non-null, its principal must be passed as
+ * aPrincipal, and the third-party iframe and sandboxing status of the window
+ * are also checked. If aURI is non-null, then it is used as the comparison
+ * against aWindow to determine if this is a third-party load. We also
+ * allow a channel instead of the window reference when determining 3rd party
+ * status.
+ *
+ * Used in the implementation of StorageAllowedForWindow,
+ * StorageAllowedForDocument, StorageAllowedForChannel and
+ * StorageAllowedForServiceWorker.
+ */
+static StorageAccess InternalStorageAllowedCheck(
+ nsIPrincipal* aPrincipal, nsPIDOMWindowInner* aWindow, nsIURI* aURI,
+ nsIChannel* aChannel, nsICookieJarSettings* aCookieJarSettings,
+ uint32_t& aRejectedReason) {
+ MOZ_ASSERT(aPrincipal);
+
+ aRejectedReason = 0;
+
+ StorageAccess access = StorageAccess::eAllow;
+
+ // We don't allow storage on the null principal, in general. Even if the
+ // calling context is chrome.
+ if (aPrincipal->GetIsNullPrincipal()) {
+ return StorageAccess::eDeny;
+ }
+
+ nsCOMPtr<nsIURI> documentURI;
+ if (aWindow) {
+ // If the document is sandboxed, then it is not permitted to use storage
+ Document* document = aWindow->GetExtantDoc();
+ if (document && document->GetSandboxFlags() & SANDBOXED_ORIGIN) {
+ return StorageAccess::eDeny;
+ }
+
+ // Check if we are in private browsing, and record that fact
+ if (nsContentUtils::IsInPrivateBrowsing(document)) {
+ access = StorageAccess::ePrivateBrowsing;
+ }
+
+ // Get the document URI for the below about: URI check.
+ documentURI = document ? document->GetDocumentURI() : nullptr;
+ }
+
+ // About URIs are allowed to access storage, even if they don't have chrome
+ // privileges. If this is not desired, than the consumer will have to
+ // implement their own restriction functionality.
+ //
+ // This is due to backwards-compatibility and the state of storage access
+ // before the introducton of InternalStorageAllowedCheck:
+ //
+ // BEFORE:
+ // localStorage, caches: allowed in 3rd-party iframes always
+ // IndexedDB: allowed in 3rd-party iframes only if 3rd party URI is an about:
+ // URI within a specific allowlist
+ //
+ // AFTER:
+ // localStorage, caches: allowed in 3rd-party iframes by default. Preference
+ // can be set to disable in 3rd-party, which will not disallow in about:
+ // URIs.
+ // IndexedDB: allowed in 3rd-party iframes by default. Preference can be set
+ // to disable in 3rd-party, which will disallow in about: URIs, unless they
+ // are within a specific allowlist.
+ //
+ // This means that behavior for storage with internal about: URIs should not
+ // be affected, which is desireable due to the lack of automated testing for
+ // about: URIs with these preferences set, and the importance of the correct
+ // functioning of these URIs even with custom preferences.
+ //
+ // We need to check the aURI or the document URI here instead of only checking
+ // the URI from the principal. Because the principal might not have a URI if
+ // it is a system principal.
+ if ((aURI && aURI->SchemeIs("about") &&
+ !NS_IsContentAccessibleAboutURI(aURI)) ||
+ (documentURI && documentURI->SchemeIs("about") &&
+ !NS_IsContentAccessibleAboutURI(documentURI)) ||
+ aPrincipal->SchemeIs("about")) {
+ return access;
+ }
+
+ if (!StorageDisabledByAntiTracking(aWindow, aChannel, aPrincipal, aURI,
+ aRejectedReason)) {
+ return access;
+ }
+
+ // We want to have a partitioned storage only for trackers.
+ if (aRejectedReason ==
+ static_cast<uint32_t>(
+ nsIWebProgressListener::STATE_COOKIES_BLOCKED_TRACKER) ||
+ aRejectedReason ==
+ static_cast<uint32_t>(
+ nsIWebProgressListener::STATE_COOKIES_BLOCKED_SOCIALTRACKER)) {
+ return StorageAccess::ePartitionTrackersOrDeny;
+ }
+
+ // We want to have a partitioned storage for all third parties.
+ if (aRejectedReason ==
+ static_cast<uint32_t>(
+ nsIWebProgressListener::STATE_COOKIES_PARTITIONED_FOREIGN)) {
+ return StorageAccess::ePartitionForeignOrDeny;
+ }
+
+ return StorageAccess::eDeny;
+}
+
+/**
+ * Wrapper around InternalStorageAllowedCheck which caches the check result on
+ * the inner window to improve performance. nsGlobalWindowInner is responsible
+ * for invalidating the cache state if storage access changes during window
+ * lifetime.
+ */
+static StorageAccess InternalStorageAllowedCheckCached(
+ nsIPrincipal* aPrincipal, nsPIDOMWindowInner* aWindow, nsIURI* aURI,
+ nsIChannel* aChannel, nsICookieJarSettings* aCookieJarSettings,
+ uint32_t& aRejectedReason) {
+ // If enabled, check if we have already computed the storage access field
+ // for this window. This avoids repeated calls to
+ // InternalStorageAllowedCheck.
+ nsGlobalWindowInner* win = nullptr;
+ if (aWindow) {
+ win = nsGlobalWindowInner::Cast(aWindow);
+
+ Maybe<StorageAccess> storageAccess =
+ win->GetStorageAllowedCache(aRejectedReason);
+ if (storageAccess.isSome()) {
+ return storageAccess.value();
+ }
+ }
+
+ StorageAccess result = InternalStorageAllowedCheck(
+ aPrincipal, aWindow, aURI, aChannel, aCookieJarSettings, aRejectedReason);
+ if (win) {
+ // Remember check result for the lifetime of the window. It's the windows
+ // responsibility to invalidate this field if storage access changes
+ // because a storage access permission is granted.
+ win->SetStorageAllowedCache(result, aRejectedReason);
+ }
+
+ return result;
+}
+
+static bool StorageDisabledByAntiTrackingInternal(
+ nsPIDOMWindowInner* aWindow, nsIChannel* aChannel, nsIPrincipal* aPrincipal,
+ nsIURI* aURI, nsICookieJarSettings* aCookieJarSettings,
+ uint32_t& aRejectedReason) {
+ MOZ_ASSERT(aWindow || aChannel || aPrincipal);
+
+ if (aWindow) {
+ nsIURI* documentURI = aURI ? aURI : aWindow->GetDocumentURI();
+ return !documentURI ||
+ !ShouldAllowAccessFor(aWindow, documentURI, &aRejectedReason);
+ }
+
+ if (aChannel) {
+ nsCOMPtr<nsIURI> uri;
+ nsresult rv = aChannel->GetURI(getter_AddRefs(uri));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return false;
+ }
+
+ return !ShouldAllowAccessFor(aChannel, uri, &aRejectedReason);
+ }
+
+ MOZ_ASSERT(aPrincipal);
+ return !ShouldAllowAccessFor(aPrincipal, aCookieJarSettings);
+}
+
+namespace mozilla {
+
+StorageAccess StorageAllowedForWindow(nsPIDOMWindowInner* aWindow,
+ uint32_t* aRejectedReason) {
+ uint32_t rejectedReason;
+ if (!aRejectedReason) {
+ aRejectedReason = &rejectedReason;
+ }
+
+ *aRejectedReason = 0;
+
+ if (Document* document = aWindow->GetExtantDoc()) {
+ nsCOMPtr<nsIPrincipal> principal = document->NodePrincipal();
+ // Note that GetChannel() below may return null, but that's OK, since the
+ // callee is able to deal with a null channel argument, and if passed null,
+ // will only fail to notify the UI in case storage gets blocked.
+ nsIChannel* channel = document->GetChannel();
+ return InternalStorageAllowedCheckCached(
+ principal, aWindow, nullptr, channel, document->CookieJarSettings(),
+ *aRejectedReason);
+ }
+
+ // No document? Try checking Private Browsing Mode without document
+ if (const nsCOMPtr<nsIGlobalObject> global = aWindow->AsGlobal()) {
+ if (const nsCOMPtr<nsIPrincipal> principal = global->PrincipalOrNull()) {
+ if (principal->GetPrivateBrowsingId() > 0) {
+ return StorageAccess::ePrivateBrowsing;
+ }
+ }
+ }
+
+ // Everything failed? Let's return a generic rejected reason.
+ return StorageAccess::eDeny;
+}
+
+StorageAccess StorageAllowedForDocument(const Document* aDoc) {
+ StorageAccess cookieAllowed = CookieAllowedForDocument(aDoc);
+ if (StaticPrefs::
+ privacy_partition_always_partition_third_party_non_cookie_storage() &&
+ cookieAllowed > StorageAccess::eDeny) {
+ return StorageAccess::ePartitionForeignOrDeny;
+ }
+ return cookieAllowed;
+}
+
+StorageAccess CookieAllowedForDocument(const Document* aDoc) {
+ MOZ_ASSERT(aDoc);
+
+ if (nsPIDOMWindowInner* inner = aDoc->GetInnerWindow()) {
+ nsCOMPtr<nsIPrincipal> principal = aDoc->NodePrincipal();
+ // Note that GetChannel() below may return null, but that's OK, since the
+ // callee is able to deal with a null channel argument, and if passed null,
+ // will only fail to notify the UI in case storage gets blocked.
+ nsIChannel* channel = aDoc->GetChannel();
+
+ uint32_t rejectedReason = 0;
+ return InternalStorageAllowedCheckCached(
+ principal, inner, nullptr, channel,
+ const_cast<Document*>(aDoc)->CookieJarSettings(), rejectedReason);
+ }
+
+ return StorageAccess::eDeny;
+}
+
+StorageAccess StorageAllowedForNewWindow(nsIPrincipal* aPrincipal, nsIURI* aURI,
+ nsPIDOMWindowInner* aParent) {
+ MOZ_ASSERT(aPrincipal);
+ MOZ_ASSERT(aURI);
+ // parent may be nullptr
+
+ uint32_t rejectedReason = 0;
+ nsCOMPtr<nsICookieJarSettings> cjs;
+ if (aParent && aParent->GetExtantDoc()) {
+ cjs = aParent->GetExtantDoc()->CookieJarSettings();
+ } else {
+ cjs = net::CookieJarSettings::Create(aPrincipal);
+ }
+ return InternalStorageAllowedCheck(aPrincipal, aParent, aURI, nullptr, cjs,
+ rejectedReason);
+}
+
+StorageAccess StorageAllowedForChannel(nsIChannel* aChannel) {
+ MOZ_DIAGNOSTIC_ASSERT(nsContentUtils::GetSecurityManager());
+ MOZ_DIAGNOSTIC_ASSERT(aChannel);
+
+ nsCOMPtr<nsIPrincipal> principal;
+ Unused << nsContentUtils::GetSecurityManager()->GetChannelResultPrincipal(
+ aChannel, getter_AddRefs(principal));
+ NS_ENSURE_TRUE(principal, StorageAccess::eDeny);
+
+ nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo();
+ nsCOMPtr<nsICookieJarSettings> cookieJarSettings;
+ nsresult rv =
+ loadInfo->GetCookieJarSettings(getter_AddRefs(cookieJarSettings));
+ NS_ENSURE_SUCCESS(rv, StorageAccess::eDeny);
+
+ uint32_t rejectedReason = 0;
+ StorageAccess result = InternalStorageAllowedCheck(
+ principal, nullptr, nullptr, aChannel, cookieJarSettings, rejectedReason);
+
+ return result;
+}
+
+StorageAccess StorageAllowedForServiceWorker(
+ nsIPrincipal* aPrincipal, nsICookieJarSettings* aCookieJarSettings) {
+ uint32_t rejectedReason = 0;
+ return InternalStorageAllowedCheck(aPrincipal, nullptr, nullptr, nullptr,
+ aCookieJarSettings, rejectedReason);
+}
+
+bool StorageDisabledByAntiTracking(nsPIDOMWindowInner* aWindow,
+ nsIChannel* aChannel,
+ nsIPrincipal* aPrincipal, nsIURI* aURI,
+ uint32_t& aRejectedReason) {
+ MOZ_ASSERT(aWindow || aChannel || aPrincipal);
+ nsCOMPtr<nsICookieJarSettings> cookieJarSettings;
+ if (aWindow) {
+ if (aWindow->GetExtantDoc()) {
+ cookieJarSettings = aWindow->GetExtantDoc()->CookieJarSettings();
+ }
+ } else if (aChannel) {
+ nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo();
+ Unused << loadInfo->GetCookieJarSettings(getter_AddRefs(cookieJarSettings));
+ }
+ if (!cookieJarSettings) {
+ cookieJarSettings = net::CookieJarSettings::Create(aPrincipal);
+ }
+ bool disabled = StorageDisabledByAntiTrackingInternal(
+ aWindow, aChannel, aPrincipal, aURI, cookieJarSettings, aRejectedReason);
+
+ if (aWindow) {
+ ContentBlockingNotifier::OnDecision(
+ aWindow,
+ disabled ? ContentBlockingNotifier::BlockingDecision::eBlock
+ : ContentBlockingNotifier::BlockingDecision::eAllow,
+ aRejectedReason);
+ } else if (aChannel) {
+ ContentBlockingNotifier::OnDecision(
+ aChannel,
+ disabled ? ContentBlockingNotifier::BlockingDecision::eBlock
+ : ContentBlockingNotifier::BlockingDecision::eAllow,
+ aRejectedReason);
+ }
+ return disabled;
+}
+
+bool StorageDisabledByAntiTracking(dom::Document* aDocument, nsIURI* aURI,
+ uint32_t& aRejectedReason) {
+ aRejectedReason = 0;
+ // Note that GetChannel() below may return null, but that's OK, since the
+ // callee is able to deal with a null channel argument, and if passed null,
+ // will only fail to notify the UI in case storage gets blocked.
+ return StorageDisabledByAntiTracking(
+ aDocument->GetInnerWindow(), aDocument->GetChannel(),
+ aDocument->NodePrincipal(), aURI, aRejectedReason);
+}
+
+bool ShouldPartitionStorage(StorageAccess aAccess) {
+ return aAccess == StorageAccess::ePartitionTrackersOrDeny ||
+ aAccess == StorageAccess::ePartitionForeignOrDeny;
+}
+
+bool ShouldPartitionStorage(uint32_t aRejectedReason) {
+ return aRejectedReason ==
+ static_cast<uint32_t>(
+ nsIWebProgressListener::STATE_COOKIES_BLOCKED_TRACKER) ||
+ aRejectedReason ==
+ static_cast<uint32_t>(
+ nsIWebProgressListener::STATE_COOKIES_BLOCKED_SOCIALTRACKER) ||
+ aRejectedReason ==
+ static_cast<uint32_t>(
+ nsIWebProgressListener::STATE_COOKIES_PARTITIONED_FOREIGN);
+}
+
+bool StoragePartitioningEnabled(StorageAccess aAccess,
+ nsICookieJarSettings* aCookieJarSettings) {
+ return aAccess == StorageAccess::ePartitionForeignOrDeny &&
+ aCookieJarSettings->GetCookieBehavior() ==
+ nsICookieService::BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN;
+}
+
+bool StoragePartitioningEnabled(uint32_t aRejectedReason,
+ nsICookieJarSettings* aCookieJarSettings) {
+ return aRejectedReason ==
+ static_cast<uint32_t>(
+ nsIWebProgressListener::STATE_COOKIES_PARTITIONED_FOREIGN) &&
+ aCookieJarSettings->GetCookieBehavior() ==
+ nsICookieService::BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN;
+}
+
+int32_t CookiesBehavior(Document* a3rdPartyDocument) {
+ MOZ_ASSERT(a3rdPartyDocument);
+
+ // WebExtensions principals always get BEHAVIOR_ACCEPT as cookieBehavior
+ // (See Bug 1406675 and Bug 1525917 for rationale).
+ if (BasePrincipal::Cast(a3rdPartyDocument->NodePrincipal())->AddonPolicy()) {
+ return nsICookieService::BEHAVIOR_ACCEPT;
+ }
+
+ return a3rdPartyDocument->CookieJarSettings()->GetCookieBehavior();
+}
+
+int32_t CookiesBehavior(nsILoadInfo* aLoadInfo, nsIURI* a3rdPartyURI) {
+ MOZ_ASSERT(aLoadInfo);
+ MOZ_ASSERT(a3rdPartyURI);
+
+ // WebExtensions 3rd party URI always get BEHAVIOR_ACCEPT as cookieBehavior,
+ // this is semantically equivalent to the principal having a AddonPolicy().
+ if (a3rdPartyURI->SchemeIs("moz-extension")) {
+ return nsICookieService::BEHAVIOR_ACCEPT;
+ }
+
+ nsCOMPtr<nsICookieJarSettings> cookieJarSettings;
+ nsresult rv =
+ aLoadInfo->GetCookieJarSettings(getter_AddRefs(cookieJarSettings));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return nsICookieService::BEHAVIOR_REJECT;
+ }
+
+ return cookieJarSettings->GetCookieBehavior();
+}
+
+int32_t CookiesBehavior(nsIPrincipal* aPrincipal,
+ nsICookieJarSettings* aCookieJarSettings) {
+ MOZ_ASSERT(aPrincipal);
+ MOZ_ASSERT(aCookieJarSettings);
+
+ // WebExtensions principals always get BEHAVIOR_ACCEPT as cookieBehavior
+ // (See Bug 1406675 for rationale).
+ if (BasePrincipal::Cast(aPrincipal)->AddonPolicy()) {
+ return nsICookieService::BEHAVIOR_ACCEPT;
+ }
+
+ return aCookieJarSettings->GetCookieBehavior();
+}
+
+bool ShouldAllowAccessFor(nsPIDOMWindowInner* aWindow, nsIURI* aURI,
+ uint32_t* aRejectedReason) {
+ MOZ_ASSERT(aWindow);
+ MOZ_ASSERT(aURI);
+
+ // Let's avoid a null check on aRejectedReason everywhere else.
+ uint32_t rejectedReason = 0;
+ if (!aRejectedReason) {
+ aRejectedReason = &rejectedReason;
+ }
+
+ LOG_SPEC(("Computing whether window %p has access to URI %s", aWindow, _spec),
+ aURI);
+
+ nsGlobalWindowInner* innerWindow = nsGlobalWindowInner::Cast(aWindow);
+ Document* document = innerWindow->GetExtantDoc();
+ if (!document) {
+ LOG(("Our window has no document"));
+ return false;
+ }
+
+ uint32_t cookiePermission = detail::CheckCookiePermissionForPrincipal(
+ document->CookieJarSettings(), document->NodePrincipal());
+ if (cookiePermission != nsICookiePermission::ACCESS_DEFAULT) {
+ LOG(
+ ("CheckCookiePermissionForPrincipal() returned a non-default access "
+ "code (%d) for window's principal, returning %s",
+ int(cookiePermission),
+ cookiePermission != nsICookiePermission::ACCESS_DENY ? "success"
+ : "failure"));
+ if (cookiePermission != nsICookiePermission::ACCESS_DENY) {
+ return true;
+ }
+
+ *aRejectedReason =
+ nsIWebProgressListener::STATE_COOKIES_BLOCKED_BY_PERMISSION;
+ return false;
+ }
+
+ int32_t behavior = CookiesBehavior(document);
+ if (behavior == nsICookieService::BEHAVIOR_ACCEPT) {
+ LOG(("The cookie behavior pref mandates accepting all cookies!"));
+ return true;
+ }
+
+ if (ContentBlockingAllowList::Check(aWindow)) {
+ return true;
+ }
+
+ if (behavior == nsICookieService::BEHAVIOR_REJECT) {
+ LOG(("The cookie behavior pref mandates rejecting all cookies!"));
+ *aRejectedReason = nsIWebProgressListener::STATE_COOKIES_BLOCKED_ALL;
+ return false;
+ }
+
+ // As a performance optimization, we only perform this check for
+ // BEHAVIOR_REJECT_FOREIGN and BEHAVIOR_LIMIT_FOREIGN. For
+ // BEHAVIOR_REJECT_TRACKER and BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN,
+ // third-partiness is implicily checked later below.
+ if (behavior != nsICookieService::BEHAVIOR_REJECT_TRACKER &&
+ behavior !=
+ nsICookieService::BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN) {
+ // Let's check if this is a 3rd party context.
+ if (!AntiTrackingUtils::IsThirdPartyWindow(aWindow, aURI)) {
+ LOG(("Our window isn't a third-party window"));
+ return true;
+ }
+ }
+
+ if ((behavior == nsICookieService::BEHAVIOR_REJECT_FOREIGN &&
+ !CookieJarSettings::IsRejectThirdPartyWithExceptions(behavior)) ||
+ behavior == nsICookieService::BEHAVIOR_LIMIT_FOREIGN) {
+ // XXX For non-cookie forms of storage, we handle BEHAVIOR_LIMIT_FOREIGN by
+ // simply rejecting the request to use the storage. In the future, if we
+ // change the meaning of BEHAVIOR_LIMIT_FOREIGN to be one which makes sense
+ // for non-cookie storage types, this may change.
+ LOG(("Nothing more to do due to the behavior code %d", int(behavior)));
+ *aRejectedReason = nsIWebProgressListener::STATE_COOKIES_BLOCKED_FOREIGN;
+ return false;
+ }
+
+ // The document has been allowlisted. We can return from here directly.
+ if (document->HasStorageAccessPermissionGrantedByAllowList()) {
+ return true;
+ }
+
+ MOZ_ASSERT(
+ CookieJarSettings::IsRejectThirdPartyWithExceptions(behavior) ||
+ behavior == nsICookieService::BEHAVIOR_REJECT_TRACKER ||
+ behavior ==
+ nsICookieService::BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN);
+
+ uint32_t blockedReason =
+ nsIWebProgressListener::STATE_COOKIES_BLOCKED_TRACKER;
+
+ if (behavior == nsICookieService::BEHAVIOR_REJECT_TRACKER) {
+ if (!nsContentUtils::IsThirdPartyTrackingResourceWindow(aWindow)) {
+ LOG(("Our window isn't a third-party tracking window"));
+ return true;
+ }
+
+ nsCOMPtr<nsIClassifiedChannel> classifiedChannel =
+ do_QueryInterface(document->GetChannel());
+ if (classifiedChannel) {
+ uint32_t classificationFlags =
+ classifiedChannel->GetThirdPartyClassificationFlags();
+ if (classificationFlags & nsIClassifiedChannel::ClassificationFlags::
+ CLASSIFIED_SOCIALTRACKING) {
+ blockedReason =
+ nsIWebProgressListener::STATE_COOKIES_BLOCKED_SOCIALTRACKER;
+ }
+ }
+ } else if (behavior ==
+ nsICookieService::BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN) {
+ if (nsContentUtils::IsThirdPartyTrackingResourceWindow(aWindow)) {
+ // fall through
+ } else if (AntiTrackingUtils::IsThirdPartyWindow(aWindow, aURI)) {
+ LOG(("We're in the third-party context, storage should be partitioned"));
+ // fall through, but remember that we're partitioning.
+ blockedReason = nsIWebProgressListener::STATE_COOKIES_PARTITIONED_FOREIGN;
+ } else {
+ LOG(("Our window isn't a third-party window, storage is allowed"));
+ return true;
+ }
+ } else {
+ MOZ_ASSERT(CookieJarSettings::IsRejectThirdPartyWithExceptions(behavior));
+ if (RejectForeignAllowList::Check(document)) {
+ LOG(("This window is exceptionlisted for reject foreign"));
+ return true;
+ }
+
+ blockedReason = nsIWebProgressListener::STATE_COOKIES_PARTITIONED_FOREIGN;
+ }
+
+ Document* doc = aWindow->GetExtantDoc();
+ // Make sure storage access isn't disabled
+ if (doc && (doc->StorageAccessSandboxed())) {
+ LOG(("Our document is sandboxed"));
+ *aRejectedReason = blockedReason;
+ return false;
+ }
+
+ // Document::HasStoragePermission first checks if storage access granted is
+ // cached in the inner window, if no, it then checks the storage permission
+ // flag in the channel's loadinfo
+ bool allowed = document->HasStorageAccessPermissionGranted();
+
+ if (!allowed) {
+ *aRejectedReason = blockedReason;
+ } else {
+ if (MOZ_LOG_TEST(gAntiTrackingLog, mozilla::LogLevel::Debug) &&
+ aWindow->HasStorageAccessPermissionGranted()) {
+ LOG(("Permission stored in the window. All good."));
+ }
+ }
+
+ return allowed;
+}
+
+bool ShouldAllowAccessFor(nsIChannel* aChannel, nsIURI* aURI,
+ uint32_t* aRejectedReason) {
+ MOZ_ASSERT(aURI);
+ MOZ_ASSERT(aChannel);
+
+ // Let's avoid a null check on aRejectedReason everywhere else.
+ uint32_t rejectedReason = 0;
+ if (!aRejectedReason) {
+ aRejectedReason = &rejectedReason;
+ }
+
+ nsIScriptSecurityManager* ssm =
+ nsScriptSecurityManager::GetScriptSecurityManager();
+ MOZ_ASSERT(ssm);
+
+ nsCOMPtr<nsIURI> channelURI;
+ nsresult rv = NS_GetFinalChannelURI(aChannel, getter_AddRefs(channelURI));
+ if (NS_FAILED(rv)) {
+ LOG(("Failed to get the channel final URI, bail out early"));
+ return true;
+ }
+ LOG_SPEC(
+ ("Computing whether channel %p has access to URI %s", aChannel, _spec),
+ channelURI);
+
+ nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo();
+ nsCOMPtr<nsICookieJarSettings> cookieJarSettings;
+ rv = loadInfo->GetCookieJarSettings(getter_AddRefs(cookieJarSettings));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ LOG(
+ ("Failed to get the cookie jar settings from the loadinfo, bail out "
+ "early"));
+ return true;
+ }
+
+ nsCOMPtr<nsIPrincipal> channelPrincipal;
+ rv = ssm->GetChannelURIPrincipal(aChannel, getter_AddRefs(channelPrincipal));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ LOG(("No channel principal, bail out early"));
+ return false;
+ }
+
+ uint32_t cookiePermission = detail::CheckCookiePermissionForPrincipal(
+ cookieJarSettings, channelPrincipal);
+ if (cookiePermission != nsICookiePermission::ACCESS_DEFAULT) {
+ LOG(
+ ("CheckCookiePermissionForPrincipal() returned a non-default access "
+ "code (%d) for channel's principal, returning %s",
+ int(cookiePermission),
+ cookiePermission != nsICookiePermission::ACCESS_DENY ? "success"
+ : "failure"));
+ if (cookiePermission != nsICookiePermission::ACCESS_DENY) {
+ return true;
+ }
+
+ *aRejectedReason =
+ nsIWebProgressListener::STATE_COOKIES_BLOCKED_BY_PERMISSION;
+ return false;
+ }
+
+ if (!channelURI) {
+ LOG(("No channel uri, bail out early"));
+ return false;
+ }
+
+ int32_t behavior = CookiesBehavior(loadInfo, channelURI);
+ if (behavior == nsICookieService::BEHAVIOR_ACCEPT) {
+ LOG(("The cookie behavior pref mandates accepting all cookies!"));
+ return true;
+ }
+
+ nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(aChannel);
+
+ if (httpChannel && ContentBlockingAllowList::Check(httpChannel)) {
+ return true;
+ }
+
+ if (behavior == nsICookieService::BEHAVIOR_REJECT) {
+ LOG(("The cookie behavior pref mandates rejecting all cookies!"));
+ *aRejectedReason = nsIWebProgressListener::STATE_COOKIES_BLOCKED_ALL;
+ return false;
+ }
+
+ nsCOMPtr<mozIThirdPartyUtil> thirdPartyUtil =
+ components::ThirdPartyUtil::Service();
+ if (!thirdPartyUtil) {
+ LOG(("No thirdPartyUtil, bail out early"));
+ return true;
+ }
+
+ bool thirdParty = false;
+ rv = thirdPartyUtil->IsThirdPartyChannel(aChannel, aURI, &thirdParty);
+ // Grant if it's not a 3rd party.
+ // Be careful to check the return value of IsThirdPartyChannel, since
+ // IsThirdPartyChannel() will fail if the channel's loading principal is the
+ // system principal...
+ if (NS_SUCCEEDED(rv) && !thirdParty) {
+ LOG(("Our channel isn't a third-party channel"));
+ return true;
+ }
+
+ if ((behavior == nsICookieService::BEHAVIOR_REJECT_FOREIGN &&
+ !CookieJarSettings::IsRejectThirdPartyWithExceptions(behavior)) ||
+ behavior == nsICookieService::BEHAVIOR_LIMIT_FOREIGN) {
+ // XXX For non-cookie forms of storage, we handle BEHAVIOR_LIMIT_FOREIGN by
+ // simply rejecting the request to use the storage. In the future, if we
+ // change the meaning of BEHAVIOR_LIMIT_FOREIGN to be one which makes sense
+ // for non-cookie storage types, this may change.
+ LOG(("Nothing more to do due to the behavior code %d", int(behavior)));
+ *aRejectedReason = nsIWebProgressListener::STATE_COOKIES_BLOCKED_FOREIGN;
+ return false;
+ }
+
+ // The channel has been allowlisted. We can return from here.
+ if (loadInfo->GetStoragePermission() ==
+ nsILoadInfo::StoragePermissionAllowListed) {
+ return true;
+ }
+
+ MOZ_ASSERT(
+ CookieJarSettings::IsRejectThirdPartyWithExceptions(behavior) ||
+ behavior == nsICookieService::BEHAVIOR_REJECT_TRACKER ||
+ behavior ==
+ nsICookieService::BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN);
+
+ uint32_t blockedReason =
+ nsIWebProgressListener::STATE_COOKIES_BLOCKED_TRACKER;
+
+ // Not a tracker.
+ nsCOMPtr<nsIClassifiedChannel> classifiedChannel =
+ do_QueryInterface(aChannel);
+ if (behavior == nsICookieService::BEHAVIOR_REJECT_TRACKER) {
+ if (classifiedChannel) {
+ if (!classifiedChannel->IsThirdPartyTrackingResource()) {
+ LOG(("Our channel isn't a third-party tracking channel"));
+ return true;
+ }
+
+ uint32_t classificationFlags =
+ classifiedChannel->GetThirdPartyClassificationFlags();
+ if (classificationFlags & nsIClassifiedChannel::ClassificationFlags::
+ CLASSIFIED_SOCIALTRACKING) {
+ blockedReason =
+ nsIWebProgressListener::STATE_COOKIES_BLOCKED_SOCIALTRACKER;
+ }
+ }
+ } else if (behavior ==
+ nsICookieService::BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN) {
+ if (classifiedChannel &&
+ classifiedChannel->IsThirdPartyTrackingResource()) {
+ // fall through
+ } else if (AntiTrackingUtils::IsThirdPartyChannel(aChannel)) {
+ LOG(("We're in the third-party context, storage should be partitioned"));
+ // fall through but remember that we're partitioning.
+ blockedReason = nsIWebProgressListener::STATE_COOKIES_PARTITIONED_FOREIGN;
+ } else {
+ LOG(("Our channel isn't a third-party channel, storage is allowed"));
+ return true;
+ }
+ } else {
+ MOZ_ASSERT(CookieJarSettings::IsRejectThirdPartyWithExceptions(behavior));
+ if (httpChannel && RejectForeignAllowList::Check(httpChannel)) {
+ LOG(("This channel is exceptionlisted"));
+ return true;
+ }
+ blockedReason = nsIWebProgressListener::STATE_COOKIES_BLOCKED_FOREIGN;
+ }
+
+ RefPtr<BrowsingContext> targetBC;
+ rv = loadInfo->GetTargetBrowsingContext(getter_AddRefs(targetBC));
+ if (!targetBC || NS_WARN_IF(NS_FAILED(rv))) {
+ LOG(("Failed to get the channel's target browsing context"));
+ return false;
+ }
+
+ if (Document::StorageAccessSandboxed(targetBC->GetSandboxFlags())) {
+ LOG(("Our document is sandboxed"));
+ *aRejectedReason = blockedReason;
+ return false;
+ }
+
+ // Let's see if we have to grant the access for this particular channel.
+
+ // HasStorageAccessPermissionGranted only applies to channels that load
+ // documents, for sub-resources loads, just returns the result from loadInfo.
+ bool isDocument = false;
+ aChannel->GetIsDocument(&isDocument);
+
+ if (isDocument) {
+ nsCOMPtr<nsPIDOMWindowInner> inner =
+ AntiTrackingUtils::GetInnerWindow(targetBC);
+ if (inner && inner->HasStorageAccessPermissionGranted()) {
+ LOG(("Permission stored in the window. All good."));
+ return true;
+ }
+ }
+
+ bool allowed =
+ loadInfo->GetStoragePermission() != nsILoadInfo::NoStoragePermission;
+ if (!allowed) {
+ *aRejectedReason = blockedReason;
+ }
+
+ return allowed;
+}
+
+bool ShouldAllowAccessFor(nsIPrincipal* aPrincipal,
+ nsICookieJarSettings* aCookieJarSettings) {
+ MOZ_ASSERT(aPrincipal);
+ MOZ_ASSERT(aCookieJarSettings);
+
+ uint32_t access = nsICookiePermission::ACCESS_DEFAULT;
+ if (aPrincipal->GetIsContentPrincipal()) {
+ PermissionManager* permManager = PermissionManager::GetInstance();
+ if (permManager) {
+ Unused << NS_WARN_IF(NS_FAILED(permManager->TestPermissionFromPrincipal(
+ aPrincipal, "cookie"_ns, &access)));
+ }
+ }
+
+ if (access != nsICookiePermission::ACCESS_DEFAULT) {
+ return access != nsICookiePermission::ACCESS_DENY;
+ }
+
+ int32_t behavior = CookiesBehavior(aPrincipal, aCookieJarSettings);
+ return behavior != nsICookieService::BEHAVIOR_REJECT;
+}
+
+/* static */
+bool ApproximateAllowAccessForWithoutChannel(
+ nsPIDOMWindowInner* aFirstPartyWindow, nsIURI* aURI) {
+ MOZ_ASSERT(aFirstPartyWindow);
+ MOZ_ASSERT(aURI);
+
+ LOG_SPEC(
+ ("Computing a best guess as to whether window %p has access to URI %s",
+ aFirstPartyWindow, _spec),
+ aURI);
+
+ Document* parentDocument =
+ nsGlobalWindowInner::Cast(aFirstPartyWindow)->GetExtantDoc();
+ if (NS_WARN_IF(!parentDocument)) {
+ LOG(("Failed to get the first party window's document"));
+ return false;
+ }
+
+ if (!parentDocument->CookieJarSettings()->GetRejectThirdPartyContexts()) {
+ LOG(("Disabled by the pref (%d), bail out early",
+ parentDocument->CookieJarSettings()->GetCookieBehavior()));
+ return true;
+ }
+
+ if (ContentBlockingAllowList::Check(aFirstPartyWindow)) {
+ return true;
+ }
+
+ if (!AntiTrackingUtils::IsThirdPartyWindow(aFirstPartyWindow, aURI)) {
+ LOG(("Our window isn't a third-party window"));
+ return true;
+ }
+
+ uint32_t cookiePermission = detail::CheckCookiePermissionForPrincipal(
+ parentDocument->CookieJarSettings(), parentDocument->NodePrincipal());
+ if (cookiePermission != nsICookiePermission::ACCESS_DEFAULT) {
+ LOG(
+ ("CheckCookiePermissionForPrincipal() returned a non-default access "
+ "code (%d), returning %s",
+ int(cookiePermission),
+ cookiePermission != nsICookiePermission::ACCESS_DENY ? "success"
+ : "failure"));
+ return cookiePermission != nsICookiePermission::ACCESS_DENY;
+ }
+
+ nsAutoCString origin;
+ nsresult rv = nsContentUtils::GetASCIIOrigin(aURI, origin);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ LOG_SPEC(("Failed to compute the origin from %s", _spec), aURI);
+ return false;
+ }
+
+ nsIPrincipal* parentPrincipal = parentDocument->NodePrincipal();
+
+ nsAutoCString type;
+ AntiTrackingUtils::CreateStoragePermissionKey(origin, type);
+
+ return AntiTrackingUtils::CheckStoragePermission(
+ parentPrincipal, type,
+ nsContentUtils::IsInPrivateBrowsing(parentDocument), nullptr, 0);
+}
+} // namespace mozilla
diff --git a/toolkit/components/antitracking/StorageAccess.h b/toolkit/components/antitracking/StorageAccess.h
new file mode 100644
index 0000000000..24d8e05357
--- /dev/null
+++ b/toolkit/components/antitracking/StorageAccess.h
@@ -0,0 +1,168 @@
+/* -*- 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_StorageAccess_h
+#define mozilla_StorageAccess_h
+
+#include <cstdint>
+
+#include "mozilla/MozPromise.h"
+#include "mozilla/RefPtr.h"
+
+#include "mozilla/dom/BrowsingContext.h"
+
+class nsIChannel;
+class nsICookieJarSettings;
+class nsIPrincipal;
+class nsIURI;
+class nsPIDOMWindowInner;
+
+namespace mozilla {
+namespace dom {
+class Document;
+}
+
+// The order of these entries matters, as we use std::min for total ordering
+// of permissions. Private Browsing is considered to be more limiting
+// then session scoping
+enum class StorageAccess {
+ // The storage should be partitioned for third-party resources. if the
+ // caller is unable to do it, deny the storage access.
+ ePartitionForeignOrDeny = -2,
+ // The storage should be partitioned for third-party trackers. if the caller
+ // is unable to do it, deny the storage access.
+ ePartitionTrackersOrDeny = -1,
+ // Don't allow access to the storage
+ eDeny = 0,
+ // Allow access to the storage, but only if it is secure to do so in a
+ // private browsing context.
+ ePrivateBrowsing = 1,
+ // Allow access to the storage, but only persist it for the current session
+ eSessionScoped = 2,
+ // Allow access to the storage
+ eAllow = 3,
+ // Keep this at the end. Used for serialization, but not a valid value.
+ eNumValues = 4,
+};
+
+/*
+ * Checks if storage for the given window is permitted by a combination of
+ * the user's preferences, and whether the window is a third-party iframe.
+ *
+ * This logic is intended to be shared between the different forms of
+ * persistent storage which are available to web pages. Cookies don't use
+ * this logic, and security logic related to them must be updated separately.
+ */
+StorageAccess StorageAllowedForWindow(nsPIDOMWindowInner* aWindow,
+ uint32_t* aRejectedReason = nullptr);
+
+/*
+ * Checks if storage for the given document is permitted by a combination of
+ * the user's preferences, and whether the document's window is a third-party
+ * iframe.
+ *
+ * Note, this may be used on documents during the loading process where
+ * the window's extant document has not been set yet. The code in
+ * StorageAllowedForWindow(), however, will not work in these cases.
+ */
+StorageAccess StorageAllowedForDocument(const dom::Document* aDoc);
+
+StorageAccess CookieAllowedForDocument(const dom::Document* aDoc);
+
+/*
+ * Checks if storage should be allowed for a new window with the given
+ * principal, load URI, and parent.
+ */
+StorageAccess StorageAllowedForNewWindow(nsIPrincipal* aPrincipal, nsIURI* aURI,
+ nsPIDOMWindowInner* aParent);
+
+/*
+ * Checks if storage should be allowed for the given channel. The check will
+ * be based on the channel result principal and, depending on preferences and
+ * permissions, mozIThirdPartyUtil.isThirdPartyChannel().
+ */
+StorageAccess StorageAllowedForChannel(nsIChannel* aChannel);
+
+/*
+ * Checks if storage for the given principal is permitted by the user's
+ * preferences. This method should be used only by ServiceWorker loading.
+ */
+StorageAccess StorageAllowedForServiceWorker(
+ nsIPrincipal* aPrincipal, nsICookieJarSettings* aCookieJarSettings);
+
+/*
+ * Returns true if this window/channel/aPrincipal should disable storages
+ * because of the anti-tracking feature.
+ * Note that either aWindow or aChannel may be null when calling this
+ * function. If the caller wants the UI to be notified when the storage gets
+ * disabled, it must pass a non-null channel object.
+ */
+bool StorageDisabledByAntiTracking(nsPIDOMWindowInner* aWindow,
+ nsIChannel* aChannel,
+ nsIPrincipal* aPrincipal, nsIURI* aURI,
+ uint32_t& aRejectedReason);
+
+/*
+ * Returns true if this document should disable storages because of the
+ * anti-tracking feature.
+ */
+bool StorageDisabledByAntiTracking(dom::Document* aDocument, nsIURI* aURI,
+ uint32_t& aRejectedReason);
+
+bool ShouldPartitionStorage(StorageAccess aAccess);
+
+bool ShouldPartitionStorage(uint32_t aRejectedReason);
+
+bool StoragePartitioningEnabled(StorageAccess aAccess,
+ nsICookieJarSettings* aCookieJarSettings);
+
+bool StoragePartitioningEnabled(uint32_t aRejectedReason,
+ nsICookieJarSettings* aCookieJarSettings);
+
+// This method returns true if the URI has first party storage access when
+// loaded inside the passed 3rd party context tracking resource window.
+// If the window is first party context, please use
+// ApproximateAllowAccessForWithoutChannel();
+//
+// aRejectedReason could be set to one of these values if passed and if the
+// storage permission is not granted:
+// * nsIWebProgressListener::STATE_COOKIES_BLOCKED_BY_PERMISSION
+// * nsIWebProgressListener::STATE_COOKIES_BLOCKED_TRACKER
+// * nsIWebProgressListener::STATE_COOKIES_BLOCKED_SOCIALTRACKER
+// * nsIWebProgressListener::STATE_COOKIES_BLOCKED_ALL
+// * nsIWebProgressListener::STATE_COOKIES_BLOCKED_FOREIGN
+bool ShouldAllowAccessFor(nsPIDOMWindowInner* a3rdPartyTrackingWindow,
+ nsIURI* aURI, uint32_t* aRejectedReason);
+
+// Note: you should use ShouldAllowAccessFor() passing the nsIChannel! Use
+// this method _only_ if the channel is not available. For first party
+// window, it's impossible to know if the aURI is a tracking resource
+// synchronously, so here we return the best guest: if we are sure that the
+// permission is granted for the origin of aURI, this method returns true,
+// otherwise false.
+bool ApproximateAllowAccessForWithoutChannel(
+ nsPIDOMWindowInner* aFirstPartyWindow, nsIURI* aURI);
+
+// It returns true if the URI has access to the first party storage.
+// aChannel can be a 3rd party channel, or not.
+// See ShouldAllowAccessFor(window) to see the possible values of
+// aRejectedReason.
+bool ShouldAllowAccessFor(nsIChannel* aChannel, nsIURI* aURI,
+ uint32_t* aRejectedReason);
+
+// This method checks if the principal has the permission to access to the
+// first party storage.
+bool ShouldAllowAccessFor(nsIPrincipal* aPrincipal,
+ nsICookieJarSettings* aCookieJarSettings);
+
+namespace detail {
+uint32_t CheckCookiePermissionForPrincipal(
+ nsICookieJarSettings* aCookieJarSettings, nsIPrincipal* aPrincipal);
+}
+
+} // namespace mozilla
+
+#endif // mozilla_StorageAccess_h
diff --git a/toolkit/components/antitracking/StorageAccessAPIHelper.cpp b/toolkit/components/antitracking/StorageAccessAPIHelper.cpp
new file mode 100644
index 0000000000..1a49f7708c
--- /dev/null
+++ b/toolkit/components/antitracking/StorageAccessAPIHelper.cpp
@@ -0,0 +1,1105 @@
+/* -*- 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 "AntiTrackingLog.h"
+#include "StorageAccessAPIHelper.h"
+#include "AntiTrackingUtils.h"
+#include "TemporaryAccessGrantObserver.h"
+
+#include "mozilla/Components.h"
+#include "mozilla/ContentBlockingAllowList.h"
+#include "mozilla/ContentBlockingUserInteraction.h"
+#include "mozilla/dom/BrowsingContext.h"
+#include "mozilla/dom/BrowsingContextGroup.h"
+#include "mozilla/dom/ContentChild.h"
+#include "mozilla/dom/ContentParent.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/WindowContext.h"
+#include "mozilla/dom/WindowGlobalParent.h"
+#include "mozilla/net/CookieJarSettings.h"
+#include "mozilla/PermissionManager.h"
+#include "mozilla/StaticPrefs_network.h"
+#include "mozilla/StaticPrefs_privacy.h"
+#include "mozilla/Telemetry.h"
+#include "mozIThirdPartyUtil.h"
+#include "nsContentUtils.h"
+#include "nsGlobalWindowInner.h"
+#include "nsIClassifiedChannel.h"
+#include "nsICookiePermission.h"
+#include "nsICookieService.h"
+#include "nsIPermission.h"
+#include "nsIPrincipal.h"
+#include "nsIURI.h"
+#include "nsIURIClassifier.h"
+#include "nsIUrlClassifierFeature.h"
+#include "nsIOService.h"
+#include "nsIWebProgressListener.h"
+#include "nsScriptSecurityManager.h"
+#include "RejectForeignAllowList.h"
+#include "StorageAccess.h"
+
+namespace mozilla {
+
+LazyLogModule gAntiTrackingLog("AntiTracking");
+
+}
+
+using namespace mozilla;
+using mozilla::dom::BrowsingContext;
+using mozilla::dom::ContentChild;
+using mozilla::dom::Document;
+using mozilla::dom::WindowGlobalParent;
+using mozilla::net::CookieJarSettings;
+
+namespace {
+
+bool GetTopLevelWindowId(BrowsingContext* aParentContext, uint32_t aBehavior,
+ uint64_t& aTopLevelInnerWindowId) {
+ MOZ_ASSERT(aParentContext);
+
+ aTopLevelInnerWindowId =
+ (aBehavior == nsICookieService::BEHAVIOR_REJECT_TRACKER)
+ ? AntiTrackingUtils::GetTopLevelStorageAreaWindowId(aParentContext)
+ : AntiTrackingUtils::GetTopLevelAntiTrackingWindowId(aParentContext);
+ return aTopLevelInnerWindowId != 0;
+}
+
+} // namespace
+
+/* static */ RefPtr<StorageAccessAPIHelper::StorageAccessPermissionGrantPromise>
+StorageAccessAPIHelper::AllowAccessFor(
+ nsIPrincipal* aPrincipal, dom::BrowsingContext* aParentContext,
+ ContentBlockingNotifier::StorageAccessPermissionGrantedReason aReason,
+ const StorageAccessAPIHelper::PerformPermissionGrant& aPerformFinalChecks) {
+ MOZ_ASSERT(aParentContext);
+
+ switch (aReason) {
+ case ContentBlockingNotifier::eOpener:
+ if (!StaticPrefs::privacy_antitracking_enableWebcompat() ||
+ !StaticPrefs::
+ privacy_restrict3rdpartystorage_heuristic_window_open()) {
+ LOG(
+ ("Bailing out early because the window open heuristic is disabled "
+ "by pref"));
+ return StorageAccessPermissionGrantPromise::CreateAndReject(false,
+ __func__);
+ }
+ break;
+ case ContentBlockingNotifier::eOpenerAfterUserInteraction:
+ if (!StaticPrefs::privacy_antitracking_enableWebcompat() ||
+ !StaticPrefs::
+ privacy_restrict3rdpartystorage_heuristic_opened_window_after_interaction()) {
+ LOG(
+ ("Bailing out early because the window open after interaction "
+ "heuristic is disabled by pref"));
+ return StorageAccessPermissionGrantPromise::CreateAndReject(false,
+ __func__);
+ }
+ break;
+ default:
+ break;
+ }
+
+ if (MOZ_LOG_TEST(gAntiTrackingLog, mozilla::LogLevel::Debug)) {
+ nsAutoCString origin;
+ aPrincipal->GetAsciiOrigin(origin);
+ LOG(("Adding a first-party storage exception for %s, triggered by %s",
+ PromiseFlatCString(origin).get(),
+ AntiTrackingUtils::GrantedReasonToString(aReason).get()));
+ }
+
+ RefPtr<dom::WindowContext> parentWindowContext =
+ aParentContext->GetCurrentWindowContext();
+ if (!parentWindowContext) {
+ LOG(
+ ("No window context found for our parent browsing context, bailing out "
+ "early"));
+ return StorageAccessPermissionGrantPromise::CreateAndReject(false,
+ __func__);
+ }
+
+ if (parentWindowContext->GetCookieBehavior().isNothing()) {
+ LOG(
+ ("No cookie behaviour found for our parent window context, bailing "
+ "out early"));
+ return StorageAccessPermissionGrantPromise::CreateAndReject(false,
+ __func__);
+ }
+
+ // Only add storage permission when there is a reason to do so.
+ uint32_t behavior = *parentWindowContext->GetCookieBehavior();
+ if (!CookieJarSettings::IsRejectThirdPartyContexts(behavior)) {
+ LOG(
+ ("Disabled by network.cookie.cookieBehavior pref (%d), bailing out "
+ "early",
+ behavior));
+ return StorageAccessPermissionGrantPromise::CreateAndResolve(true,
+ __func__);
+ }
+
+ MOZ_ASSERT(
+ CookieJarSettings::IsRejectThirdPartyWithExceptions(behavior) ||
+ behavior == nsICookieService::BEHAVIOR_REJECT_TRACKER ||
+ behavior ==
+ nsICookieService::BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN);
+
+ // No need to continue when we are already in the allow list.
+ if (parentWindowContext->GetIsOnContentBlockingAllowList()) {
+ return StorageAccessPermissionGrantPromise::CreateAndResolve(true,
+ __func__);
+ }
+
+ // Make sure storage access isn't disabled
+ if (!aParentContext->IsTopContent() &&
+ Document::StorageAccessSandboxed(aParentContext->GetSandboxFlags())) {
+ LOG(("Our document is sandboxed"));
+ return StorageAccessPermissionGrantPromise::CreateAndReject(false,
+ __func__);
+ }
+
+ bool isParentThirdParty = parentWindowContext->GetIsThirdPartyWindow();
+ uint64_t topLevelWindowId;
+ nsAutoCString trackingOrigin;
+ nsCOMPtr<nsIPrincipal> trackingPrincipal;
+
+ LOG(("The current resource is %s-party",
+ isParentThirdParty ? "third" : "first"));
+
+ // We are a first party resource.
+ if (!isParentThirdParty) {
+ nsAutoCString origin;
+ nsresult rv = aPrincipal->GetAsciiOrigin(origin);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ LOG(("Can't get the origin from the URI"));
+ return StorageAccessPermissionGrantPromise::CreateAndReject(false,
+ __func__);
+ }
+
+ trackingOrigin = origin;
+ trackingPrincipal = aPrincipal;
+ topLevelWindowId = aParentContext->GetCurrentInnerWindowId();
+ if (NS_WARN_IF(!topLevelWindowId)) {
+ LOG(("Top-level storage area window id not found, bailing out early"));
+ return StorageAccessPermissionGrantPromise::CreateAndReject(false,
+ __func__);
+ }
+
+ } else {
+ // We should be a 3rd party source.
+ if (behavior == nsICookieService::BEHAVIOR_REJECT_TRACKER &&
+ !parentWindowContext->GetIsThirdPartyTrackingResourceWindow()) {
+ LOG(("Our window isn't a third-party tracking window"));
+ return StorageAccessPermissionGrantPromise::CreateAndReject(false,
+ __func__);
+ }
+ if ((CookieJarSettings::IsRejectThirdPartyWithExceptions(behavior) ||
+ behavior ==
+ nsICookieService::BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN) &&
+ !isParentThirdParty) {
+ LOG(("Our window isn't a third-party window"));
+ return StorageAccessPermissionGrantPromise::CreateAndReject(false,
+ __func__);
+ }
+
+ if (!GetTopLevelWindowId(aParentContext,
+ // Don't request the ETP specific behaviour of
+ // allowing only singly-nested iframes here,
+ // because we are recording an allow permission.
+ nsICookieService::BEHAVIOR_ACCEPT,
+ topLevelWindowId)) {
+ LOG(("Error while retrieving the parent window id, bailing out early"));
+ return StorageAccessPermissionGrantPromise::CreateAndReject(false,
+ __func__);
+ }
+
+ // If we can't get the principal and tracking origin at this point, the
+ // tracking principal will be gotten while running ::CompleteAllowAccessFor
+ // in the parent.
+ if (aParentContext->IsInProcess()) {
+ if (!AntiTrackingUtils::GetPrincipalAndTrackingOrigin(
+ aParentContext, getter_AddRefs(trackingPrincipal),
+ trackingOrigin)) {
+ LOG(
+ ("Error while computing the parent principal and tracking origin, "
+ "bailing out early"));
+ return StorageAccessPermissionGrantPromise::CreateAndReject(false,
+ __func__);
+ }
+ }
+ }
+
+ // We MAY need information that is only accessible in the parent,
+ // so we need to determine whether we can run it in the current process (in
+ // most of cases it should be a child process).
+ //
+ // We will follow below algorithm to decide if we can continue to run in
+ // the current process, otherwise, we need to ask the parent to continue
+ // the work.
+ // 1. Check if aParentContext is an in-process browsing context. If it isn't,
+ // we cannot proceed in the content process because we need the
+ // principal of the parent window. Otherwise, we go to step 2.
+ // 2. Check if the grant reason is ePrivilegeStorageAccessForOriginAPI. In
+ // this case, we don't need to check the user interaction of the tracking
+ // origin. So, we can proceed in the content process. Otherwise, go to
+ // step 3.
+ // 2. tracking origin is not third-party with respect to the parent window
+ // (aParentContext). This is because we need to test whether the user
+ // has interacted with the tracking origin before, and this info is
+ // not supposed to be seen from cross-origin processes.
+
+ // The only case that aParentContext is not in-process is when the heuristic
+ // is triggered because of user interactions.
+ MOZ_ASSERT_IF(
+ !aParentContext->IsInProcess(),
+ aReason == ContentBlockingNotifier::eOpenerAfterUserInteraction);
+
+ bool runInSameProcess;
+ if (XRE_IsParentProcess()) {
+ // If we are already in the parent, then continue to run in the parent.
+ runInSameProcess = true;
+ } else {
+ // We should run in the parent process when the tracking origin is
+ // third-party with respect to it's parent window. This is because we can't
+ // test if the user has interacted with the third-party origin in the child
+ // process.
+ if (aParentContext->IsInProcess()) {
+ bool isThirdParty;
+ nsCOMPtr<nsIPrincipal> principal =
+ AntiTrackingUtils::GetPrincipal(aParentContext);
+ if (!principal) {
+ LOG(("Can't get the principal from the browsing context"));
+ return StorageAccessPermissionGrantPromise::CreateAndReject(false,
+ __func__);
+ }
+ Unused << trackingPrincipal->IsThirdPartyPrincipal(principal,
+ &isThirdParty);
+ runInSameProcess =
+ aReason ==
+ ContentBlockingNotifier::ePrivilegeStorageAccessForOriginAPI ||
+ !isThirdParty;
+ } else {
+ runInSameProcess = false;
+ }
+ }
+
+ if (runInSameProcess) {
+ return StorageAccessAPIHelper::CompleteAllowAccessFor(
+ aParentContext, topLevelWindowId, trackingPrincipal, trackingOrigin,
+ behavior, aReason, aPerformFinalChecks);
+ }
+
+ MOZ_ASSERT(XRE_IsContentProcess());
+ // Only support PerformPermissionGrant when we run ::CompleteAllowAccessFor in
+ // the same process. This callback is only used by eStorageAccessAPI,
+ // which is always runned in the same process.
+ MOZ_ASSERT(!aPerformFinalChecks);
+
+ ContentChild* cc = ContentChild::GetSingleton();
+ MOZ_ASSERT(cc);
+
+ RefPtr<BrowsingContext> bc = aParentContext;
+ return cc
+ ->SendCompleteAllowAccessFor(aParentContext, topLevelWindowId,
+ trackingPrincipal, trackingOrigin, behavior,
+ aReason)
+ ->Then(GetCurrentSerialEventTarget(), __func__,
+ [bc, trackingOrigin, behavior,
+ aReason](const ContentChild::CompleteAllowAccessForPromise::
+ ResolveOrRejectValue& aValue) {
+ if (aValue.IsResolve() && aValue.ResolveValue().isSome()) {
+ // we don't call OnAllowAccessFor in the parent when this is
+ // triggered by the opener heuristic, so we have to do it here.
+ // See storePermission below for the reason.
+ if (aReason == ContentBlockingNotifier::eOpener &&
+ !bc->IsDiscarded()) {
+ MOZ_ASSERT(bc->IsInProcess());
+ StorageAccessAPIHelper::OnAllowAccessFor(bc, trackingOrigin,
+ behavior, aReason);
+ }
+ return StorageAccessPermissionGrantPromise::CreateAndResolve(
+ aValue.ResolveValue().value(), __func__);
+ }
+ return StorageAccessPermissionGrantPromise::CreateAndReject(
+ false, __func__);
+ });
+}
+
+// CompleteAllowAccessFor is used to process the remaining work in
+// AllowAccessFor that may need to access information not accessible
+// in the current process.
+// This API supports running running in the child process and the
+// parent process. When running in the child, aParentContext must be in-process.
+//
+// Here lists the possible cases based on our heuristics:
+// 1. eStorageAccessAPI
+// aParentContext is the browsing context of the document that calls this
+// API, so it is always in-process. Since the tracking origin is the
+// document's origin, it's same-origin to the parent window.
+// CompleteAllowAccessFor runs in the same process as AllowAccessFor.
+//
+// 2. eOpener
+// aParentContext is the browsing context of the opener that calls this
+// API, so it is always in-process. However, when the opener is a first
+// party and it opens a third-party window, the tracking origin is
+// origin of the third-party window. In this case, we should
+// run this API in the parent, as for the other cases, we can run in the
+// same process.
+//
+// 3. eOpenerAfterUserInteraction
+// aParentContext is the browsing context of the opener window, but
+// AllowAccessFor is called by the opened window. So as long as
+// aParentContext is not in-process, we should run in the parent.
+//
+// 4. ePrivilegeStorageAccessForOriginAPI
+// aParentContext is the browsing context of the top window which calls the
+// privilege API. So, it is always in-process. And we don't need to check the
+// user interaction permission for the tracking origin in this case. We can
+// run in the same process.
+/* static */ RefPtr<StorageAccessAPIHelper::StorageAccessPermissionGrantPromise>
+StorageAccessAPIHelper::CompleteAllowAccessFor(
+ dom::BrowsingContext* aParentContext, uint64_t aTopLevelWindowId,
+ nsIPrincipal* aTrackingPrincipal, const nsACString& aTrackingOrigin,
+ uint32_t aCookieBehavior,
+ ContentBlockingNotifier::StorageAccessPermissionGrantedReason aReason,
+ const PerformPermissionGrant& aPerformFinalChecks) {
+ MOZ_ASSERT(aParentContext);
+ MOZ_ASSERT_IF(XRE_IsContentProcess(), aParentContext->IsInProcess());
+
+ nsCOMPtr<nsIPrincipal> trackingPrincipal;
+ nsAutoCString trackingOrigin;
+ if (!aTrackingPrincipal) {
+ // User interaction is the only case that tracking principal is not
+ // available.
+ MOZ_ASSERT(XRE_IsParentProcess() &&
+ aReason == ContentBlockingNotifier::eOpenerAfterUserInteraction);
+
+ if (!AntiTrackingUtils::GetPrincipalAndTrackingOrigin(
+ aParentContext, getter_AddRefs(trackingPrincipal),
+ trackingOrigin)) {
+ LOG(
+ ("Error while computing the parent principal and tracking origin, "
+ "bailing out early"));
+ return StorageAccessPermissionGrantPromise::CreateAndReject(false,
+ __func__);
+ }
+ } else {
+ trackingPrincipal = aTrackingPrincipal;
+ trackingOrigin = aTrackingOrigin;
+ }
+
+ LOG(("Tracking origin is %s", PromiseFlatCString(trackingOrigin).get()));
+
+ // We hardcode this block reason since the first-party storage access
+ // permission is granted for the purpose of blocking trackers.
+ // Note that if aReason is eOpenerAfterUserInteraction and the
+ // trackingPrincipal is not in a blocklist, we don't check the
+ // user-interaction state, because it could be that the current process has
+ // just sent the request to store the user-interaction permission into the
+ // parent, without having received the permission itself yet.
+ //
+ // For ePrivilegeStorageAccessForOriginAPI, we explicitly don't check the user
+ // interaction for the tracking origin.
+
+ bool isInPrefList = false;
+ trackingPrincipal->IsURIInPrefList(
+ "privacy.restrict3rdpartystorage."
+ "userInteractionRequiredForHosts",
+ &isInPrefList);
+ if (aReason != ContentBlockingNotifier::ePrivilegeStorageAccessForOriginAPI &&
+ isInPrefList &&
+ !ContentBlockingUserInteraction::Exists(trackingPrincipal)) {
+ LOG_PRIN(("Tracking principal (%s) hasn't been interacted with before, "
+ "refusing to add a first-party storage permission to access it",
+ _spec),
+ trackingPrincipal);
+ ContentBlockingNotifier::OnDecision(
+ aParentContext, ContentBlockingNotifier::BlockingDecision::eBlock,
+ CookieJarSettings::IsRejectThirdPartyWithExceptions(aCookieBehavior)
+ ? nsIWebProgressListener::STATE_COOKIES_BLOCKED_FOREIGN
+ : nsIWebProgressListener::STATE_COOKIES_BLOCKED_TRACKER);
+ return StorageAccessPermissionGrantPromise::CreateAndReject(false,
+ __func__);
+ }
+
+ // Ensure we can find the window before continuing, so we can safely
+ // execute storePermission.
+ if (aParentContext->IsInProcess() &&
+ (!aParentContext->GetDOMWindow() ||
+ !aParentContext->GetDOMWindow()->GetCurrentInnerWindow())) {
+ LOG(
+ ("No window found for our parent browsing context, bailing out "
+ "early"));
+ return StorageAccessPermissionGrantPromise::CreateAndReject(false,
+ __func__);
+ }
+
+ auto storePermission =
+ [aParentContext, aTopLevelWindowId, trackingOrigin, trackingPrincipal,
+ aCookieBehavior,
+ aReason](int aAllowMode) -> RefPtr<StorageAccessPermissionGrantPromise> {
+ // Inform the window we granted permission for. This has to be done in the
+ // window's process.
+ if (aParentContext->IsInProcess()) {
+ StorageAccessAPIHelper::OnAllowAccessFor(aParentContext, trackingOrigin,
+ aCookieBehavior, aReason);
+ } else {
+ MOZ_ASSERT(XRE_IsParentProcess());
+
+ // We don't have the window, send an IPC to the content process that
+ // owns the parent window. But there is a special case, for window.open,
+ // we'll return to the content process we need to inform when this
+ // function is done. So we don't need to create an extra IPC for the case.
+ if (aReason != ContentBlockingNotifier::eOpener) {
+ dom::ContentParent* cp =
+ aParentContext->Canonical()->GetContentParent();
+ Unused << cp->SendOnAllowAccessFor(aParentContext, trackingOrigin,
+ aCookieBehavior, aReason);
+ }
+ }
+
+ Maybe<ContentBlockingNotifier::StorageAccessPermissionGrantedReason>
+ reportReason;
+ // We can directly report here if we can know the origin of the top.
+ if (XRE_IsParentProcess() || aParentContext->Top()->IsInProcess()) {
+ ContentBlockingNotifier::ReportUnblockingToConsole(
+ aParentContext, NS_ConvertUTF8toUTF16(trackingOrigin), aReason);
+
+ // Set the report reason to nothing if we've already reported.
+ reportReason = Nothing();
+ } else {
+ // Set the report reason, so that we can know the reason when reporting
+ // in the parent.
+ reportReason.emplace(aReason);
+ }
+
+ if (XRE_IsParentProcess()) {
+ LOG(("Saving the permission: trackingOrigin=%s", trackingOrigin.get()));
+ return SaveAccessForOriginOnParentProcess(aTopLevelWindowId,
+ aParentContext,
+ trackingPrincipal, aAllowMode)
+ ->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [aReason, trackingPrincipal](
+ ParentAccessGrantPromise::ResolveOrRejectValue&& aValue) {
+ if (!aValue.IsResolve()) {
+ return StorageAccessPermissionGrantPromise::CreateAndReject(
+ false, __func__);
+ }
+ // We only wish to observe user interaction in the case of a
+ // "normal" requestStorageAccess grant. We do not observe user
+ // interaction where the priveledged API is used. Acquiring
+ // the storageAccessAPI permission for the first time will only
+ // occur through the clicking accept on the doorhanger.
+ if (aReason == ContentBlockingNotifier::eStorageAccessAPI) {
+ ContentBlockingUserInteraction::Observe(trackingPrincipal);
+ }
+ return StorageAccessPermissionGrantPromise::CreateAndResolve(
+ StorageAccessAPIHelper::eAllow, __func__);
+ });
+ }
+
+ ContentChild* cc = ContentChild::GetSingleton();
+ MOZ_ASSERT(cc);
+
+ LOG(
+ ("Asking the parent process to save the permission for us: "
+ "trackingOrigin=%s",
+ trackingOrigin.get()));
+
+ // This is not really secure, because here we have the content process
+ // sending the request of storing a permission.
+ return cc
+ ->SendStorageAccessPermissionGrantedForOrigin(
+ aTopLevelWindowId, aParentContext, trackingPrincipal,
+ trackingOrigin, aAllowMode, reportReason)
+ ->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [aReason, trackingPrincipal](
+ const ContentChild::
+ StorageAccessPermissionGrantedForOriginPromise::
+ ResolveOrRejectValue& aValue) {
+ if (aValue.IsResolve()) {
+ if (aValue.ResolveValue() &&
+ (aReason == ContentBlockingNotifier::eStorageAccessAPI)) {
+ ContentBlockingUserInteraction::Observe(trackingPrincipal);
+ }
+ return StorageAccessPermissionGrantPromise::CreateAndResolve(
+ aValue.ResolveValue(), __func__);
+ }
+ return StorageAccessPermissionGrantPromise::CreateAndReject(
+ false, __func__);
+ });
+ };
+
+ if (aPerformFinalChecks) {
+ return aPerformFinalChecks()->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [storePermission](
+ StorageAccessPermissionGrantPromise::ResolveOrRejectValue&&
+ aValue) {
+ if (aValue.IsResolve()) {
+ return storePermission(aValue.ResolveValue());
+ }
+ return StorageAccessPermissionGrantPromise::CreateAndReject(false,
+ __func__);
+ });
+ }
+ return storePermission(false);
+}
+
+/* static */ void StorageAccessAPIHelper::OnAllowAccessFor(
+ dom::BrowsingContext* aParentContext, const nsACString& aTrackingOrigin,
+ uint32_t aCookieBehavior,
+ ContentBlockingNotifier::StorageAccessPermissionGrantedReason aReason) {
+ MOZ_ASSERT(aParentContext->IsInProcess());
+
+ // Let's inform the parent window and the other windows having the
+ // same tracking origin about the storage permission is granted.
+ StorageAccessAPIHelper::UpdateAllowAccessOnCurrentProcess(aParentContext,
+ aTrackingOrigin);
+
+ // Let's inform the parent window.
+ nsCOMPtr<nsPIDOMWindowInner> parentInner =
+ AntiTrackingUtils::GetInnerWindow(aParentContext);
+ if (NS_WARN_IF(!parentInner)) {
+ return;
+ }
+
+ Document* doc = parentInner->GetExtantDoc();
+ if (NS_WARN_IF(!doc)) {
+ return;
+ }
+
+ if (!doc->GetChannel()) {
+ return;
+ }
+
+ Telemetry::AccumulateCategorical(
+ Telemetry::LABELS_STORAGE_ACCESS_GRANTED_COUNT::StorageGranted);
+
+ switch (aReason) {
+ case ContentBlockingNotifier::StorageAccessPermissionGrantedReason::
+ eStorageAccessAPI:
+ Telemetry::AccumulateCategorical(
+ Telemetry::LABELS_STORAGE_ACCESS_GRANTED_COUNT::StorageAccessAPI);
+ break;
+ case ContentBlockingNotifier::StorageAccessPermissionGrantedReason::
+ eOpenerAfterUserInteraction:
+ Telemetry::AccumulateCategorical(
+ Telemetry::LABELS_STORAGE_ACCESS_GRANTED_COUNT::OpenerAfterUI);
+ break;
+ case ContentBlockingNotifier::StorageAccessPermissionGrantedReason::eOpener:
+ Telemetry::AccumulateCategorical(
+ Telemetry::LABELS_STORAGE_ACCESS_GRANTED_COUNT::Opener);
+ break;
+ default:
+ break;
+ }
+
+ // Theoratically this can be done in the parent process. But right now,
+ // we need the channel while notifying content blocking events, and
+ // we don't have a trivial way to obtain the channel in the parent
+ // via BrowsingContext. So we just ask the child to do the work.
+ ContentBlockingNotifier::OnEvent(
+ doc->GetChannel(), false,
+ CookieJarSettings::IsRejectThirdPartyWithExceptions(aCookieBehavior)
+ ? nsIWebProgressListener::STATE_COOKIES_BLOCKED_FOREIGN
+ : nsIWebProgressListener::STATE_COOKIES_BLOCKED_TRACKER,
+ aTrackingOrigin, Some(aReason));
+}
+
+/* static */
+RefPtr<mozilla::StorageAccessAPIHelper::ParentAccessGrantPromise>
+StorageAccessAPIHelper::SaveAccessForOriginOnParentProcess(
+ uint64_t aTopLevelWindowId, BrowsingContext* aParentContext,
+ nsIPrincipal* aTrackingPrincipal, int aAllowMode,
+ uint64_t aExpirationTime) {
+ MOZ_ASSERT(aTopLevelWindowId != 0);
+ MOZ_ASSERT(aTrackingPrincipal);
+
+ if (!aTrackingPrincipal || aTrackingPrincipal->IsSystemPrincipal() ||
+ aTrackingPrincipal->GetIsNullPrincipal() ||
+ aTrackingPrincipal->GetIsExpandedPrincipal()) {
+ LOG(("aTrackingPrincipal is of invalid principal type"));
+ return ParentAccessGrantPromise::CreateAndReject(false, __func__);
+ }
+
+ nsAutoCString trackingOrigin;
+ nsresult rv = aTrackingPrincipal->GetOriginNoSuffix(trackingOrigin);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return ParentAccessGrantPromise::CreateAndReject(false, __func__);
+ }
+
+ RefPtr<WindowGlobalParent> wgp =
+ WindowGlobalParent::GetByInnerWindowId(aTopLevelWindowId);
+ if (!wgp) {
+ LOG(("Can't get window global parent"));
+ return ParentAccessGrantPromise::CreateAndReject(false, __func__);
+ }
+
+ // If the permission is granted on a first-party window, also have to update
+ // the permission to all the other windows with the same tracking origin (in
+ // the same tab), if any.
+ StorageAccessAPIHelper::UpdateAllowAccessOnParentProcess(aParentContext,
+ trackingOrigin);
+
+ return StorageAccessAPIHelper::SaveAccessForOriginOnParentProcess(
+ wgp->DocumentPrincipal(), aTrackingPrincipal, aAllowMode,
+ aExpirationTime);
+}
+
+/* static */
+RefPtr<mozilla::StorageAccessAPIHelper::ParentAccessGrantPromise>
+StorageAccessAPIHelper::SaveAccessForOriginOnParentProcess(
+ nsIPrincipal* aParentPrincipal, nsIPrincipal* aTrackingPrincipal,
+ int aAllowMode, uint64_t aExpirationTime) {
+ MOZ_ASSERT(XRE_IsParentProcess());
+ MOZ_ASSERT(aAllowMode == eAllow || aAllowMode == eAllowAutoGrant);
+
+ if (!aParentPrincipal || !aTrackingPrincipal) {
+ LOG(("Invalid input arguments passed"));
+ return ParentAccessGrantPromise::CreateAndReject(false, __func__);
+ };
+
+ if (aTrackingPrincipal->IsSystemPrincipal() ||
+ aTrackingPrincipal->GetIsNullPrincipal() ||
+ aTrackingPrincipal->GetIsExpandedPrincipal()) {
+ LOG(("aTrackingPrincipal is of invalid principal type"));
+ return ParentAccessGrantPromise::CreateAndReject(false, __func__);
+ }
+
+ nsAutoCString trackingOrigin;
+ nsresult rv = aTrackingPrincipal->GetOriginNoSuffix(trackingOrigin);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return ParentAccessGrantPromise::CreateAndReject(false, __func__);
+ }
+
+ LOG_PRIN(("Saving a first-party storage permission on %s for "
+ "trackingOrigin=%s",
+ _spec, trackingOrigin.get()),
+ aParentPrincipal);
+
+ if (NS_WARN_IF(!aParentPrincipal)) {
+ // The child process is sending something wrong. Let's ignore it.
+ LOG(("aParentPrincipal is null, bailing out early"));
+ return ParentAccessGrantPromise::CreateAndReject(false, __func__);
+ }
+
+ PermissionManager* permManager = PermissionManager::GetInstance();
+ if (NS_WARN_IF(!permManager)) {
+ LOG(("Permission manager is null, bailing out early"));
+ return ParentAccessGrantPromise::CreateAndReject(false, __func__);
+ }
+
+ // Remember that this pref is stored in seconds!
+ uint32_t expirationType = nsIPermissionManager::EXPIRE_TIME;
+ uint32_t expirationTime = aExpirationTime * 1000;
+ int64_t when = (PR_Now() / PR_USEC_PER_MSEC) + expirationTime;
+
+ uint32_t privateBrowsingId = 0;
+ rv = aParentPrincipal->GetPrivateBrowsingId(&privateBrowsingId);
+ if ((!NS_WARN_IF(NS_FAILED(rv)) && privateBrowsingId > 0) ||
+ (aAllowMode == eAllowAutoGrant)) {
+ // If we are coming from a private window or are automatically granting a
+ // permission, make sure to store a session-only permission which won't
+ // get persisted to disk.
+ expirationType = nsIPermissionManager::EXPIRE_SESSION;
+ when = 0;
+ }
+
+ nsAutoCString type;
+ AntiTrackingUtils::CreateStoragePermissionKey(trackingOrigin, type);
+
+ LOG(
+ ("Computed permission key: %s, expiry: %u, proceeding to save in the "
+ "permission manager",
+ type.get(), expirationTime));
+
+ rv = permManager->AddFromPrincipal(aParentPrincipal, type,
+ nsIPermissionManager::ALLOW_ACTION,
+ expirationType, when);
+ Unused << NS_WARN_IF(NS_FAILED(rv));
+
+ if (StaticPrefs::privacy_antitracking_testing()) {
+ nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
+ obs->NotifyObservers(nullptr, "antitracking-test-storage-access-perm-added",
+ nullptr);
+ }
+
+ if (NS_SUCCEEDED(rv) && (aAllowMode == eAllowAutoGrant)) {
+ // Make sure temporary access grants do not survive more than 24 hours.
+ TemporaryAccessGrantObserver::Create(permManager, aParentPrincipal, type);
+ }
+
+ LOG(("Result: %s", NS_SUCCEEDED(rv) ? "success" : "failure"));
+ return ParentAccessGrantPromise::CreateAndResolve(rv, __func__);
+}
+
+// static
+Maybe<bool>
+StorageAccessAPIHelper::CheckCookiesPermittedDecidesStorageAccessAPI(
+ nsICookieJarSettings* aCookieJarSettings,
+ nsIPrincipal* aRequestingPrincipal) {
+ MOZ_ASSERT(aCookieJarSettings);
+ MOZ_ASSERT(aRequestingPrincipal);
+ uint32_t cookiePermission = detail::CheckCookiePermissionForPrincipal(
+ aCookieJarSettings, aRequestingPrincipal);
+ if (cookiePermission == nsICookiePermission::ACCESS_ALLOW ||
+ cookiePermission == nsICookiePermission::ACCESS_SESSION) {
+ return Some(true);
+ } else if (cookiePermission == nsICookiePermission::ACCESS_DENY) {
+ return Some(false);
+ }
+
+ if (ContentBlockingAllowList::Check(aCookieJarSettings)) {
+ return Some(true);
+ }
+ return Nothing();
+}
+
+// static
+RefPtr<MozPromise<Maybe<bool>, nsresult, true>>
+StorageAccessAPIHelper::AsyncCheckCookiesPermittedDecidesStorageAccessAPI(
+ dom::BrowsingContext* aBrowsingContext,
+ nsIPrincipal* aRequestingPrincipal) {
+ MOZ_ASSERT(XRE_IsContentProcess());
+
+ ContentChild* cc = ContentChild::GetSingleton();
+ MOZ_ASSERT(cc);
+
+ return cc
+ ->SendTestCookiePermissionDecided(aBrowsingContext, aRequestingPrincipal)
+ ->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [](const ContentChild::TestCookiePermissionDecidedPromise::
+ ResolveOrRejectValue& aPromise) {
+ if (aPromise.IsResolve()) {
+ return MozPromise<Maybe<bool>, nsresult, true>::CreateAndResolve(
+ aPromise.ResolveValue(), __func__);
+ }
+ return MozPromise<Maybe<bool>, nsresult, true>::CreateAndReject(
+ NS_ERROR_UNEXPECTED, __func__);
+ });
+}
+
+// static
+Maybe<bool> StorageAccessAPIHelper::CheckBrowserSettingsDecidesStorageAccessAPI(
+ nsICookieJarSettings* aCookieJarSettings, bool aThirdParty,
+ bool aOnRejectForeignAllowlist, bool aIsOnThirdPartySkipList,
+ bool aIsThirdPartyTracker) {
+ MOZ_ASSERT(aCookieJarSettings);
+ uint32_t behavior = aCookieJarSettings->GetCookieBehavior();
+ switch (behavior) {
+ case nsICookieService::BEHAVIOR_ACCEPT:
+ return Some(true);
+ case nsICookieService::BEHAVIOR_REJECT_FOREIGN:
+ if (!aThirdParty) {
+ return Some(true);
+ }
+ if (!StaticPrefs::network_cookie_rejectForeignWithExceptions_enabled()) {
+ return Some(false);
+ }
+ return Some(aOnRejectForeignAllowlist);
+ case nsICookieService::BEHAVIOR_REJECT:
+ return Some(false);
+ case nsICookieService::BEHAVIOR_LIMIT_FOREIGN:
+ if (!aThirdParty) {
+ return Some(true);
+ }
+ return Some(false);
+ case nsICookieService::BEHAVIOR_REJECT_TRACKER:
+ if (!aIsThirdPartyTracker) {
+ return Some(true);
+ }
+ if (aIsOnThirdPartySkipList) {
+ return Some(true);
+ }
+ return Nothing();
+ case nsICookieService::BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN:
+ if (!aThirdParty) {
+ return Some(true);
+ }
+ if (aIsOnThirdPartySkipList) {
+ return Some(true);
+ }
+ return Nothing();
+ default:
+ MOZ_ASSERT_UNREACHABLE("Must not have undefined cookie behavior");
+ }
+ MOZ_ASSERT_UNREACHABLE("Must not have undefined cookie behavior");
+ return Nothing();
+}
+
+// static
+Maybe<bool> StorageAccessAPIHelper::CheckCallingContextDecidesStorageAccessAPI(
+ Document* aDocument, bool aRequestingStorageAccess) {
+ MOZ_ASSERT(aDocument);
+ // Window doesn't have user activation and we are asking for access -> reject.
+ if (aRequestingStorageAccess) {
+ if (!aDocument->HasValidTransientUserGestureActivation()) {
+ // Report an error to the console for this case
+ nsContentUtils::ReportToConsole(
+ nsIScriptError::errorFlag, nsLiteralCString("requestStorageAccess"),
+ aDocument, nsContentUtils::eDOM_PROPERTIES,
+ "RequestStorageAccessUserGesture");
+ return Some(false);
+ }
+ }
+
+ if (aDocument->IsTopLevelContentDocument()) {
+ return Some(true);
+ }
+
+ RefPtr<BrowsingContext> bc = aDocument->GetBrowsingContext();
+ if (!bc) {
+ return Some(false);
+ }
+
+ // We check if the document is a first-party document here by testing if the
+ // top-level window is same-origin. In non-Fission mode, we can directly get
+ // the top-level window through the top browsing context since it should be
+ // in-process. And test their principals.
+ //
+ // In fission, if the sub frame's origin differs from the main frame's
+ // origin, they will be in different processes. We use IsInProcess()
+ // check here to deterimine whether they have the same origin. In
+ // non-fission mode, it is always in-process so we need to compare their
+ // principals.
+ if (bc->Top()->IsInProcess()) {
+ nsCOMPtr<nsPIDOMWindowOuter> topOuter = bc->Top()->GetDOMWindow();
+ if (!topOuter) {
+ return Some(false);
+ }
+ nsCOMPtr<Document> topLevelDoc = topOuter->GetExtantDoc();
+ if (!topLevelDoc) {
+ return Some(false);
+ }
+
+ if (topLevelDoc->NodePrincipal()->Equals(aDocument->NodePrincipal())) {
+ return Some(true);
+ }
+ }
+
+ // If the document has a null origin, reject.
+ if (aDocument->NodePrincipal()->GetIsNullPrincipal()) {
+ // Report an error to the console for this case if we are requesting access
+ if (aRequestingStorageAccess) {
+ nsContentUtils::ReportToConsole(
+ nsIScriptError::errorFlag, nsLiteralCString("requestStorageAccess"),
+ aDocument, nsContentUtils::eDOM_PROPERTIES,
+ "RequestStorageAccessNullPrincipal");
+ }
+ return Some(false);
+ }
+
+ if (aRequestingStorageAccess) {
+ if (aDocument->StorageAccessSandboxed()) {
+ nsContentUtils::ReportToConsole(
+ nsIScriptError::errorFlag, nsLiteralCString("requestStorageAccess"),
+ aDocument, nsContentUtils::eDOM_PROPERTIES,
+ "RequestStorageAccessSandboxed");
+ return Some(false);
+ }
+ }
+ return Nothing();
+}
+
+// static
+Maybe<bool>
+StorageAccessAPIHelper::CheckSameSiteCallingContextDecidesStorageAccessAPI(
+ dom::Document* aDocument, bool aRequireUserActivation) {
+ MOZ_ASSERT(aDocument);
+ if (aRequireUserActivation) {
+ if (!aDocument->HasValidTransientUserGestureActivation()) {
+ // Report an error to the console for this case
+ nsContentUtils::ReportToConsole(
+ nsIScriptError::errorFlag, nsLiteralCString("requestStorageAccess"),
+ aDocument, nsContentUtils::eDOM_PROPERTIES,
+ "RequestStorageAccessUserGesture");
+ return Some(false);
+ }
+ }
+
+ nsIChannel* chan = aDocument->GetChannel();
+ if (!chan) {
+ return Some(false);
+ }
+ nsCOMPtr<nsILoadInfo> loadInfo = chan->LoadInfo();
+ if (loadInfo->GetIsThirdPartyContextToTopWindow()) {
+ return Some(false);
+ }
+
+ // If the document has a null origin, reject.
+ if (aDocument->NodePrincipal()->GetIsNullPrincipal()) {
+ // Report an error to the console for this case
+ nsContentUtils::ReportToConsole(nsIScriptError::errorFlag,
+ nsLiteralCString("requestStorageAccess"),
+ aDocument, nsContentUtils::eDOM_PROPERTIES,
+ "RequestStorageAccessNullPrincipal");
+ return Some(false);
+ }
+ return Maybe<bool>();
+}
+
+// static
+Maybe<bool>
+StorageAccessAPIHelper::CheckExistingPermissionDecidesStorageAccessAPI(
+ dom::Document* aDocument, bool aRequestingStorageAccess) {
+ MOZ_ASSERT(aDocument);
+ if (aDocument->StorageAccessSandboxed()) {
+ if (aRequestingStorageAccess) {
+ nsContentUtils::ReportToConsole(
+ nsIScriptError::errorFlag, nsLiteralCString("requestStorageAccess"),
+ aDocument, nsContentUtils::eDOM_PROPERTIES,
+ "RequestStorageAccessSandboxed");
+ }
+ return Some(false);
+ }
+ if (aDocument->HasStorageAccessPermissionGranted()) {
+ return Some(true);
+ }
+ return Nothing();
+}
+
+// static
+RefPtr<StorageAccessAPIHelper::StorageAccessPermissionGrantPromise>
+StorageAccessAPIHelper::RequestStorageAccessAsyncHelper(
+ dom::Document* aDocument, nsPIDOMWindowInner* aInnerWindow,
+ dom::BrowsingContext* aBrowsingContext, nsIPrincipal* aPrincipal,
+ bool aHasUserInteraction,
+ ContentBlockingNotifier::StorageAccessPermissionGrantedReason aNotifier,
+ bool aRequireGrant) {
+ MOZ_ASSERT(aDocument);
+
+ if (!aRequireGrant) {
+ // Try to allow access for the given principal.
+ return StorageAccessAPIHelper::AllowAccessFor(aPrincipal, aBrowsingContext,
+ aNotifier);
+ }
+
+ RefPtr<nsIPrincipal> principal(aPrincipal);
+
+ // This is a lambda function that has some variables bound to it. It will be
+ // called later in CompleteAllowAccessFor inside of AllowAccessFor.
+ auto performPermissionGrant = aDocument->CreatePermissionGrantPromise(
+ aInnerWindow, principal, aHasUserInteraction, Nothing());
+
+ // Try to allow access for the given principal.
+ return StorageAccessAPIHelper::AllowAccessFor(
+ principal, aBrowsingContext, aNotifier, performPermissionGrant);
+}
+
+// There are two methods to handle permission update:
+// 1. UpdateAllowAccessOnCurrentProcess
+// 2. UpdateAllowAccessOnParentProcess
+//
+// In general, UpdateAllowAccessOnCurrentProcess is used to propagate storage
+// permission to same-origin frames in the same tab.
+// UpdateAllowAccessOnParentProcess is used to propagate storage permission to
+// same-origin frames in the same agent cluster.
+//
+// However, there is an exception in fission mode. When the heuristic is
+// triggered by a first-party window, for instance, a first-party script calls
+// window.open(tracker), we can't update 3rd-party frames's storage permission
+// in the child process that triggers the permission update because the
+// first-party and the 3rd-party are not in the same process. In this case, we
+// should update the storage permission in UpdateAllowAccessOnParentProcess.
+
+// This function is used to update permission to all in-process windows, so it
+// can be called either from the parent or the child.
+/* static */
+void StorageAccessAPIHelper::UpdateAllowAccessOnCurrentProcess(
+ BrowsingContext* aParentContext, const nsACString& aTrackingOrigin) {
+ MOZ_ASSERT(aParentContext && aParentContext->IsInProcess());
+
+ bool useRemoteSubframes;
+ aParentContext->GetUseRemoteSubframes(&useRemoteSubframes);
+
+ if (useRemoteSubframes && aParentContext->IsTopContent()) {
+ // If we are a first-party and we are in fission mode, bail out early
+ // because we can't do anything here.
+ return;
+ }
+
+ BrowsingContext* top = aParentContext->Top();
+
+ // Propagate the storage permission to same-origin frames in the same tab.
+ top->PreOrderWalk([&](BrowsingContext* aContext) {
+ // Only check browsing contexts that are in-process.
+ if (aContext->IsInProcess()) {
+ nsAutoCString origin;
+ Unused << AntiTrackingUtils::GetPrincipalAndTrackingOrigin(
+ aContext, nullptr, origin);
+
+ if (aTrackingOrigin == origin) {
+ nsCOMPtr<nsPIDOMWindowInner> inner =
+ AntiTrackingUtils::GetInnerWindow(aContext);
+ if (inner) {
+ inner->SaveStorageAccessPermissionGranted();
+ }
+ }
+ }
+ });
+}
+
+/* static */
+void StorageAccessAPIHelper::UpdateAllowAccessOnParentProcess(
+ BrowsingContext* aParentContext, const nsACString& aTrackingOrigin) {
+ MOZ_ASSERT(XRE_IsParentProcess());
+
+ nsAutoCString topKey;
+ nsCOMPtr<nsIPrincipal> topPrincipal =
+ AntiTrackingUtils::GetPrincipal(aParentContext->Top());
+ PermissionManager::GetKeyForPrincipal(topPrincipal, false, true, topKey);
+
+ // Propagate the storage permission to same-origin frames in the same
+ // agent-cluster.
+ for (const auto& topContext : aParentContext->Group()->Toplevels()) {
+ if (topContext == aParentContext->Top()) {
+ // In non-fission mode, storage permission is stored in the top-level,
+ // don't need to propagate it to tracker frames.
+ bool useRemoteSubframes;
+ aParentContext->GetUseRemoteSubframes(&useRemoteSubframes);
+ if (!useRemoteSubframes) {
+ continue;
+ }
+ // If parent context is third-party, we already propagate permission
+ // in the child process, skip propagating here.
+ RefPtr<dom::WindowContext> ctx =
+ aParentContext->GetCurrentWindowContext();
+ if (ctx && ctx->GetIsThirdPartyWindow()) {
+ continue;
+ }
+ } else {
+ nsCOMPtr<nsIPrincipal> principal =
+ AntiTrackingUtils::GetPrincipal(topContext);
+ if (!principal) {
+ continue;
+ }
+
+ nsAutoCString key;
+ PermissionManager::GetKeyForPrincipal(principal, false, true, key);
+ // Make sure we only apply to frames that have the same top-level.
+ if (topKey != key) {
+ continue;
+ }
+ }
+
+ topContext->PreOrderWalk([&](BrowsingContext* aContext) {
+ WindowGlobalParent* wgp = aContext->Canonical()->GetCurrentWindowGlobal();
+ if (!wgp) {
+ return;
+ }
+
+ nsAutoCString origin;
+ AntiTrackingUtils::GetPrincipalAndTrackingOrigin(aContext, nullptr,
+ origin);
+ if (aTrackingOrigin == origin) {
+ Unused << wgp->SendSaveStorageAccessPermissionGranted();
+ }
+ });
+ }
+}
diff --git a/toolkit/components/antitracking/StorageAccessAPIHelper.h b/toolkit/components/antitracking/StorageAccessAPIHelper.h
new file mode 100644
index 0000000000..9be39df1c5
--- /dev/null
+++ b/toolkit/components/antitracking/StorageAccessAPIHelper.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_antitrackingservice_h
+#define mozilla_antitrackingservice_h
+
+#include "nsString.h"
+#include "mozilla/ContentBlockingNotifier.h"
+#include "mozilla/MozPromise.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/StaticPrefs_privacy.h"
+
+#include "nsIUrlClassifierFeature.h"
+
+class nsIChannel;
+class nsICookieJarSettings;
+class nsIPermission;
+class nsIPrincipal;
+class nsIURI;
+class nsPIDOMWindowInner;
+class nsPIDOMWindowOuter;
+
+namespace mozilla {
+
+class OriginAttributes;
+
+namespace dom {
+class BrowsingContext;
+class ContentParent;
+class Document;
+} // namespace dom
+
+class StorageAccessAPIHelper final {
+ public:
+ enum StorageAccessPromptChoices { eAllow, eAllowAutoGrant };
+
+ // Grant the permission for aOrigin to have access to the first party storage.
+ // This method can handle 2 different scenarios:
+ // - aParentContext is a 3rd party context, it opens an aOrigin window and the
+ // user interacts with it. We want to grant the permission at the
+ // combination: top-level + aParentWindow + aOrigin.
+ // Ex: example.net loads an iframe tracker.com, which opens a popup
+ // tracker.prg and the user interacts with it. tracker.org is allowed if
+ // loaded by tracker.com when loaded by example.net.
+ // - aParentContext is a first party context and a 3rd party resource
+ // (probably
+ // becuase of a script) opens a popup and the user interacts with it. We
+ // want to grant the permission for the 3rd party context to have access to
+ // the first party stoage when loaded in aParentWindow.
+ // Ex: example.net import tracker.com/script.js which does opens a popup and
+ // the user interacts with it. tracker.com is allowed when loaded by
+ // example.net.
+ typedef MozPromise<int, bool, true> StorageAccessPermissionGrantPromise;
+ typedef std::function<RefPtr<StorageAccessPermissionGrantPromise>()>
+ PerformPermissionGrant;
+ [[nodiscard]] static RefPtr<StorageAccessPermissionGrantPromise>
+ AllowAccessFor(
+ nsIPrincipal* aPrincipal, dom::BrowsingContext* aParentContext,
+ ContentBlockingNotifier::StorageAccessPermissionGrantedReason aReason,
+ const PerformPermissionGrant& aPerformFinalChecks = nullptr);
+
+ // This function handles tasks that have to be done in the process
+ // of the window that we just grant permission for.
+ static void OnAllowAccessFor(
+ dom::BrowsingContext* aParentContext, const nsACString& aTrackingOrigin,
+ uint32_t aCookieBehavior,
+ ContentBlockingNotifier::StorageAccessPermissionGrantedReason aReason);
+
+ // For IPC only.
+ typedef MozPromise<nsresult, bool, true> ParentAccessGrantPromise;
+ static RefPtr<ParentAccessGrantPromise> SaveAccessForOriginOnParentProcess(
+ nsIPrincipal* aParentPrincipal, nsIPrincipal* aTrackingPrincipal,
+ int aAllowMode,
+ uint64_t aExpirationTime =
+ StaticPrefs::privacy_restrict3rdpartystorage_expiration());
+
+ static RefPtr<ParentAccessGrantPromise> SaveAccessForOriginOnParentProcess(
+ uint64_t aTopLevelWindowId, dom::BrowsingContext* aParentContext,
+ nsIPrincipal* aTrackingPrincipal, int aAllowMode,
+ uint64_t aExpirationTime =
+ StaticPrefs::privacy_restrict3rdpartystorage_expiration());
+
+ // This function checks if the document has explicit permission either to
+ // allow or deny access to cookies. This may be because of the "cookie"
+ // permission or because the domain is on the ContentBlockingAllowList
+ // e.g. because the user flipped the sheild.
+ // This returns:
+ // Some(true) if unpartitioned cookies will be permitted
+ // Some(false) if unpartitioned cookies will be blocked
+ // None if it is not clear from permission alone what to do
+ static Maybe<bool> CheckCookiesPermittedDecidesStorageAccessAPI(
+ nsICookieJarSettings* aCookieJarSettings,
+ nsIPrincipal* aRequestingPrincipal);
+
+ // Calls CheckCookiesPermittedDecidesStorageAccessAPI in the Content Parent
+ // using aBrowsingContext's Top's Window Global's CookieJarSettings.
+ static RefPtr<MozPromise<Maybe<bool>, nsresult, true>>
+ AsyncCheckCookiesPermittedDecidesStorageAccessAPI(
+ dom::BrowsingContext* aBrowsingContext,
+ nsIPrincipal* aRequestingPrincipal);
+
+ // This function checks if the browser settings give explicit permission
+ // either to allow or deny access to cookies. This only checks the
+ // cookieBehavior setting. This requires an additional bool to indicate
+ // whether or not the context considered is third-party. This returns:
+ // Some(true) if unpartitioned cookies will be permitted
+ // Some(false) if unpartitioned cookies will be blocked
+ // None if it is not clear from settings alone what to do
+ static Maybe<bool> CheckBrowserSettingsDecidesStorageAccessAPI(
+ nsICookieJarSettings* aCookieJarSettings, bool aThirdParty,
+ bool aOnRejectForeignAllowlist, bool aIsOnThirdPartySkipList,
+ bool aIsThirdPartyTracker);
+
+ // This function checks if the document's context (like if it is third-party
+ // or an iframe) gives an answer of how a the StorageAccessAPI call, that is
+ // meant to be called by an embedded third party, should return.
+ // This requires an argument that allows some checks to be run only if the
+ // caller of this function is performing a request for storage access.
+ // This returns:
+ // Some(true) if the calling context has access to cookies if it is not
+ // disallowed by the browser settings and cookie permissions
+ // Some(false) if the calling context should not have access to cookies if
+ // it is not expressly allowed by the browser settings and
+ // cookie permissions
+ // None if the calling context does not determine the document's access to
+ // unpartitioned cookies
+ static Maybe<bool> CheckCallingContextDecidesStorageAccessAPI(
+ dom::Document* aDocument, bool aRequestingStorageAccess);
+
+ // This function checks if the document's context (like if it is third-party
+ // or an iframe) gives an answer of how a the StorageAccessAPI call that is
+ // meant to be called in a top-level context, should return.
+ // This returns:
+ // Some(true) if the calling context indicates calls to the top-level
+ // API must resolve if it is not
+ // disallowed by the browser settings and cookie permissions
+ // Some(false) if the calling context must reject when calling top level
+ // portions of the API if it is not expressly allowed by the
+ // browser settings and cookie permissions
+ // None if the calling context does not determine the outcome of the
+ // document's use of the top-level portions of the Storage Access API.
+ static Maybe<bool> CheckSameSiteCallingContextDecidesStorageAccessAPI(
+ dom::Document* aDocument, bool aRequireUserActivation);
+
+ // This function checks if the document has already been granted or denied
+ // access to its unpartitioned cookies by the StorageAccessAPI
+ // This returns:
+ // Some(true) if the document has been granted access by the Storage Access
+ // API before
+ // Some(false) if the document has been denied access by the Storage Access
+ // API before
+ // None if the document has not been granted or denied access by the Storage
+ // Access API before
+ static Maybe<bool> CheckExistingPermissionDecidesStorageAccessAPI(
+ dom::Document* aDocument, bool aRequestingStorageAccess);
+
+ // This function performs the asynchronous portion of checking if requests
+ // for storage access will be successful or not. This includes calling
+ // Document member functions that creating a permission prompt request and
+ // trying to perform an "autogrant" if aRequireGrant is true.
+ // This will return a promise whose values correspond to those of a
+ // ContentBlocking::AllowAccessFor call that ends the function.
+ static RefPtr<StorageAccessPermissionGrantPromise>
+ RequestStorageAccessAsyncHelper(
+ dom::Document* aDocument, nsPIDOMWindowInner* aInnerWindow,
+ dom::BrowsingContext* aBrowsingContext, nsIPrincipal* aPrincipal,
+ bool aHasUserInteraction,
+ ContentBlockingNotifier::StorageAccessPermissionGrantedReason aNotifier,
+ bool aRequireGrant);
+
+ private:
+ friend class dom::ContentParent;
+ // This should be running either in the parent process or in the child
+ // processes with an in-process browsing context.
+ [[nodiscard]] static RefPtr<StorageAccessPermissionGrantPromise>
+ CompleteAllowAccessFor(
+ dom::BrowsingContext* aParentContext, uint64_t aTopLevelWindowId,
+ nsIPrincipal* aTrackingPrincipal, const nsACString& aTrackingOrigin,
+ uint32_t aCookieBehavior,
+ ContentBlockingNotifier::StorageAccessPermissionGrantedReason aReason,
+ const PerformPermissionGrant& aPerformFinalChecks = nullptr);
+
+ static void UpdateAllowAccessOnCurrentProcess(
+ dom::BrowsingContext* aParentContext, const nsACString& aTrackingOrigin);
+
+ static void UpdateAllowAccessOnParentProcess(
+ dom::BrowsingContext* aParentContext, const nsACString& aTrackingOrigin);
+};
+
+} // namespace mozilla
+
+#endif // mozilla_antitrackingservice_h
diff --git a/toolkit/components/antitracking/StoragePrincipalHelper.cpp b/toolkit/components/antitracking/StoragePrincipalHelper.cpp
new file mode 100644
index 0000000000..10be1112ca
--- /dev/null
+++ b/toolkit/components/antitracking/StoragePrincipalHelper.cpp
@@ -0,0 +1,677 @@
+/* -*- 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 "StoragePrincipalHelper.h"
+
+#include "mozilla/ipc/PBackgroundSharedTypes.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/WorkerPrivate.h"
+#include "mozilla/net/CookieJarSettings.h"
+#include "mozilla/ScopeExit.h"
+#include "mozilla/StaticPrefs_privacy.h"
+#include "mozilla/StorageAccess.h"
+#include "nsContentUtils.h"
+#include "nsICookieJarSettings.h"
+#include "nsICookieService.h"
+#include "nsIDocShell.h"
+#include "nsIEffectiveTLDService.h"
+#include "nsIPrivateBrowsingChannel.h"
+#include "AntiTrackingUtils.h"
+
+namespace mozilla {
+
+namespace {
+
+bool ShouldPartitionChannel(nsIChannel* aChannel,
+ nsICookieJarSettings* aCookieJarSettings) {
+ MOZ_ASSERT(aChannel);
+
+ nsCOMPtr<nsIURI> uri;
+ nsresult rv = aChannel->GetURI(getter_AddRefs(uri));
+ if (NS_FAILED(rv)) {
+ return false;
+ }
+
+ uint32_t rejectedReason = 0;
+ if (ShouldAllowAccessFor(aChannel, uri, &rejectedReason)) {
+ return false;
+ }
+
+ // Let's use the storage principal only if we need to partition the cookie
+ // jar. We use the lower-level ContentBlocking API here to ensure this
+ // check doesn't send notifications.
+ if (!ShouldPartitionStorage(rejectedReason) ||
+ !StoragePartitioningEnabled(rejectedReason, aCookieJarSettings)) {
+ return false;
+ }
+
+ return true;
+}
+
+bool ChooseOriginAttributes(nsIChannel* aChannel, OriginAttributes& aAttrs,
+ bool aForcePartitionedPrincipal) {
+ MOZ_ASSERT(aChannel);
+
+ nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo();
+ nsCOMPtr<nsICookieJarSettings> cjs;
+ Unused << loadInfo->GetCookieJarSettings(getter_AddRefs(cjs));
+
+ if (!aForcePartitionedPrincipal && !ShouldPartitionChannel(aChannel, cjs)) {
+ return false;
+ }
+
+ nsAutoString partitionKey;
+ Unused << cjs->GetPartitionKey(partitionKey);
+
+ if (!partitionKey.IsEmpty()) {
+ aAttrs.SetPartitionKey(partitionKey);
+ return true;
+ }
+
+ // Fallback to get first-party domain from top-level principal when we can't
+ // get it from CookieJarSetting. This might happen when a channel is not
+ // opened via http, for example, about page.
+ nsCOMPtr<nsIPrincipal> toplevelPrincipal = loadInfo->GetTopLevelPrincipal();
+ if (!toplevelPrincipal) {
+ return false;
+ }
+ // Cast to BasePrincipal to continue to get acess to GetUri()
+ auto* basePrin = BasePrincipal::Cast(toplevelPrincipal);
+ nsCOMPtr<nsIURI> principalURI;
+
+ nsresult rv = basePrin->GetURI(getter_AddRefs(principalURI));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return false;
+ }
+
+ aAttrs.SetPartitionKey(principalURI);
+ return true;
+}
+
+bool VerifyValidPartitionedPrincipalInfoForPrincipalInfoInternal(
+ const ipc::PrincipalInfo& aPartitionedPrincipalInfo,
+ const ipc::PrincipalInfo& aPrincipalInfo,
+ bool aIgnoreSpecForContentPrincipal,
+ bool aIgnoreDomainForContentPrincipal) {
+ if (aPartitionedPrincipalInfo.type() != aPrincipalInfo.type()) {
+ return false;
+ }
+
+ if (aPartitionedPrincipalInfo.type() ==
+ mozilla::ipc::PrincipalInfo::TContentPrincipalInfo) {
+ const mozilla::ipc::ContentPrincipalInfo& spInfo =
+ aPartitionedPrincipalInfo.get_ContentPrincipalInfo();
+ const mozilla::ipc::ContentPrincipalInfo& pInfo =
+ aPrincipalInfo.get_ContentPrincipalInfo();
+
+ return spInfo.attrs().EqualsIgnoringPartitionKey(pInfo.attrs()) &&
+ spInfo.originNoSuffix() == pInfo.originNoSuffix() &&
+ (aIgnoreSpecForContentPrincipal || spInfo.spec() == pInfo.spec()) &&
+ (aIgnoreDomainForContentPrincipal ||
+ spInfo.domain() == pInfo.domain()) &&
+ spInfo.baseDomain() == pInfo.baseDomain();
+ }
+
+ if (aPartitionedPrincipalInfo.type() ==
+ mozilla::ipc::PrincipalInfo::TSystemPrincipalInfo) {
+ // Nothing to check here.
+ return true;
+ }
+
+ if (aPartitionedPrincipalInfo.type() ==
+ mozilla::ipc::PrincipalInfo::TNullPrincipalInfo) {
+ const mozilla::ipc::NullPrincipalInfo& spInfo =
+ aPartitionedPrincipalInfo.get_NullPrincipalInfo();
+ const mozilla::ipc::NullPrincipalInfo& pInfo =
+ aPrincipalInfo.get_NullPrincipalInfo();
+
+ return spInfo.spec() == pInfo.spec() &&
+ spInfo.attrs().EqualsIgnoringPartitionKey(pInfo.attrs());
+ }
+
+ if (aPartitionedPrincipalInfo.type() ==
+ mozilla::ipc::PrincipalInfo::TExpandedPrincipalInfo) {
+ const mozilla::ipc::ExpandedPrincipalInfo& spInfo =
+ aPartitionedPrincipalInfo.get_ExpandedPrincipalInfo();
+ const mozilla::ipc::ExpandedPrincipalInfo& pInfo =
+ aPrincipalInfo.get_ExpandedPrincipalInfo();
+
+ if (!spInfo.attrs().EqualsIgnoringPartitionKey(pInfo.attrs())) {
+ return false;
+ }
+
+ if (spInfo.allowlist().Length() != pInfo.allowlist().Length()) {
+ return false;
+ }
+
+ for (uint32_t i = 0; i < spInfo.allowlist().Length(); ++i) {
+ if (!VerifyValidPartitionedPrincipalInfoForPrincipalInfoInternal(
+ spInfo.allowlist()[i], pInfo.allowlist()[i],
+ aIgnoreSpecForContentPrincipal,
+ aIgnoreDomainForContentPrincipal)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ MOZ_CRASH("Invalid principalInfo type");
+ return false;
+}
+
+} // namespace
+
+// static
+nsresult StoragePrincipalHelper::Create(nsIChannel* aChannel,
+ nsIPrincipal* aPrincipal,
+ bool aForceIsolation,
+ nsIPrincipal** aStoragePrincipal) {
+ MOZ_ASSERT(aChannel);
+ MOZ_ASSERT(aPrincipal);
+ MOZ_ASSERT(aStoragePrincipal);
+
+ auto scopeExit = MakeScopeExit([&] {
+ nsCOMPtr<nsIPrincipal> storagePrincipal = aPrincipal;
+ storagePrincipal.forget(aStoragePrincipal);
+ });
+
+ OriginAttributes attrs = aPrincipal->OriginAttributesRef();
+ if (!ChooseOriginAttributes(aChannel, attrs, aForceIsolation)) {
+ return NS_OK;
+ }
+
+ scopeExit.release();
+
+ nsCOMPtr<nsIPrincipal> storagePrincipal =
+ BasePrincipal::Cast(aPrincipal)->CloneForcingOriginAttributes(attrs);
+
+ // If aPrincipal is not a ContentPrincipal, e.g. a NullPrincipal, the clone
+ // call will return a nullptr.
+ NS_ENSURE_TRUE(storagePrincipal, NS_ERROR_FAILURE);
+
+ storagePrincipal.forget(aStoragePrincipal);
+ return NS_OK;
+}
+
+// static
+nsresult StoragePrincipalHelper::CreatePartitionedPrincipalForServiceWorker(
+ nsIPrincipal* aPrincipal, nsICookieJarSettings* aCookieJarSettings,
+ nsIPrincipal** aPartitionedPrincipal) {
+ MOZ_ASSERT(aPrincipal);
+ MOZ_ASSERT(aPartitionedPrincipal);
+
+ OriginAttributes attrs = aPrincipal->OriginAttributesRef();
+
+ nsAutoString partitionKey;
+ Unused << aCookieJarSettings->GetPartitionKey(partitionKey);
+
+ if (!partitionKey.IsEmpty()) {
+ attrs.SetPartitionKey(partitionKey);
+ }
+
+ nsCOMPtr<nsIPrincipal> partitionedPrincipal =
+ BasePrincipal::Cast(aPrincipal)->CloneForcingOriginAttributes(attrs);
+
+ // If aPrincipal is not a ContentPrincipal, e.g. a NullPrincipal, the clone
+ // call will return a nullptr.
+ NS_ENSURE_TRUE(partitionedPrincipal, NS_ERROR_FAILURE);
+
+ partitionedPrincipal.forget(aPartitionedPrincipal);
+ return NS_OK;
+}
+
+// static
+nsresult
+StoragePrincipalHelper::PrepareEffectiveStoragePrincipalOriginAttributes(
+ nsIChannel* aChannel, OriginAttributes& aOriginAttributes) {
+ MOZ_ASSERT(aChannel);
+
+ ChooseOriginAttributes(aChannel, aOriginAttributes, false);
+ return NS_OK;
+}
+
+// static
+bool StoragePrincipalHelper::VerifyValidStoragePrincipalInfoForPrincipalInfo(
+ const mozilla::ipc::PrincipalInfo& aStoragePrincipalInfo,
+ const mozilla::ipc::PrincipalInfo& aPrincipalInfo) {
+ return VerifyValidPartitionedPrincipalInfoForPrincipalInfoInternal(
+ aStoragePrincipalInfo, aPrincipalInfo, false, false);
+}
+
+// static
+bool StoragePrincipalHelper::VerifyValidClientPrincipalInfoForPrincipalInfo(
+ const mozilla::ipc::PrincipalInfo& aClientPrincipalInfo,
+ const mozilla::ipc::PrincipalInfo& aPrincipalInfo) {
+ return VerifyValidPartitionedPrincipalInfoForPrincipalInfoInternal(
+ aClientPrincipalInfo, aPrincipalInfo, true, true);
+}
+
+// static
+nsresult StoragePrincipalHelper::GetPrincipal(nsIChannel* aChannel,
+ PrincipalType aPrincipalType,
+ nsIPrincipal** aPrincipal) {
+ MOZ_ASSERT(aChannel);
+ MOZ_ASSERT(aPrincipal);
+
+ nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo();
+ nsCOMPtr<nsICookieJarSettings> cjs;
+ Unused << loadInfo->GetCookieJarSettings(getter_AddRefs(cjs));
+
+ nsIScriptSecurityManager* ssm = nsContentUtils::GetSecurityManager();
+ MOZ_DIAGNOSTIC_ASSERT(ssm);
+
+ nsCOMPtr<nsIPrincipal> principal;
+ nsCOMPtr<nsIPrincipal> partitionedPrincipal;
+
+ nsresult rv =
+ ssm->GetChannelResultPrincipals(aChannel, getter_AddRefs(principal),
+ getter_AddRefs(partitionedPrincipal));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ // The aChannel might not be opened in some cases, e.g. getting principal
+ // for the new channel during a redirect. So, the value
+ // `IsThirdPartyToTopWindow` is incorrect in this case because this value is
+ // calculated during opening a channel. And we need to know the value in order
+ // to get the correct principal. To fix this, we compute the value here even
+ // the channel hasn't been opened yet.
+ //
+ // Note that we don't need to compute the value if there is no browsing
+ // context ID assigned. This could happen in a GTest or XPCShell.
+ //
+ // ToDo: The AntiTrackingUtils::ComputeIsThirdPartyToTopWindow() is only
+ // available in the parent process. So, this can only work in the parent
+ // process. It's fine for now, but we should change this to also work in
+ // content processes. Bug 1736452 will address this.
+ //
+ if (XRE_IsParentProcess() && loadInfo->GetBrowsingContextID() != 0) {
+ AntiTrackingUtils::ComputeIsThirdPartyToTopWindow(aChannel);
+ }
+
+ nsCOMPtr<nsIPrincipal> outPrincipal = principal;
+
+ switch (aPrincipalType) {
+ case eRegularPrincipal:
+ break;
+
+ case eStorageAccessPrincipal:
+ if (ShouldPartitionChannel(aChannel, cjs)) {
+ outPrincipal = partitionedPrincipal;
+ }
+ break;
+
+ case ePartitionedPrincipal:
+ outPrincipal = partitionedPrincipal;
+ break;
+
+ case eForeignPartitionedPrincipal:
+ // We only support foreign partitioned principal when dFPI is enabled.
+ if (cjs->GetCookieBehavior() ==
+ nsICookieService::BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN &&
+ loadInfo->GetIsThirdPartyContextToTopWindow()) {
+ outPrincipal = partitionedPrincipal;
+ }
+ break;
+ }
+
+ outPrincipal.forget(aPrincipal);
+ return NS_OK;
+}
+
+// static
+nsresult StoragePrincipalHelper::GetPrincipal(nsPIDOMWindowInner* aWindow,
+ PrincipalType aPrincipalType,
+ nsIPrincipal** aPrincipal) {
+ MOZ_ASSERT(aWindow);
+ MOZ_ASSERT(aPrincipal);
+
+ nsCOMPtr<dom::Document> doc = aWindow->GetExtantDoc();
+ NS_ENSURE_STATE(doc);
+
+ nsCOMPtr<nsIPrincipal> outPrincipal;
+
+ switch (aPrincipalType) {
+ case eRegularPrincipal:
+ outPrincipal = doc->NodePrincipal();
+ break;
+
+ case eStorageAccessPrincipal:
+ outPrincipal = doc->EffectiveStoragePrincipal();
+ break;
+
+ case ePartitionedPrincipal:
+ outPrincipal = doc->PartitionedPrincipal();
+ break;
+
+ case eForeignPartitionedPrincipal:
+ // We only support foreign partitioned principal when dFPI is enabled.
+ if (doc->CookieJarSettings()->GetCookieBehavior() ==
+ nsICookieService::BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN &&
+ AntiTrackingUtils::IsThirdPartyWindow(aWindow, nullptr)) {
+ outPrincipal = doc->PartitionedPrincipal();
+ } else {
+ outPrincipal = doc->NodePrincipal();
+ }
+ break;
+ }
+
+ outPrincipal.forget(aPrincipal);
+ return NS_OK;
+}
+
+// static
+bool StoragePrincipalHelper::ShouldUsePartitionPrincipalForServiceWorker(
+ nsIDocShell* aDocShell) {
+ MOZ_ASSERT(aDocShell);
+
+ // We don't use the partitioned principal for service workers if it's
+ // disabled.
+ if (!StaticPrefs::privacy_partition_serviceWorkers()) {
+ return false;
+ }
+
+ RefPtr<dom::Document> document = aDocShell->GetExtantDocument();
+
+ // If we cannot get the document from the docShell, we turn to get its
+ // parent's document.
+ if (!document) {
+ nsCOMPtr<nsIDocShellTreeItem> parentItem;
+ aDocShell->GetInProcessSameTypeParent(getter_AddRefs(parentItem));
+
+ if (parentItem) {
+ document = parentItem->GetDocument();
+ }
+ }
+
+ nsCOMPtr<nsICookieJarSettings> cookieJarSettings;
+
+ if (document) {
+ cookieJarSettings = document->CookieJarSettings();
+ } else {
+ // If there was no document, we create one cookieJarSettings here in order
+ // to get the cookieBehavior. We don't need a real value for RFP because
+ // we are only using this object to check default cookie behavior.
+ cookieJarSettings =
+ net::CookieJarSettings::Create(net::CookieJarSettings::eRegular,
+ /* shouldResistFingerpreinting */ false);
+ }
+
+ // We only support partitioned service workers when dFPI is enabled.
+ if (cookieJarSettings->GetCookieBehavior() !=
+ nsICookieService::BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN) {
+ return false;
+ }
+
+ // Only the third-party context will need to use the partitioned principal. A
+ // first-party context is still using the regular principal for the service
+ // worker.
+ return AntiTrackingUtils::IsThirdPartyContext(
+ document ? document->GetBrowsingContext()
+ : aDocShell->GetBrowsingContext());
+}
+
+// static
+bool StoragePrincipalHelper::ShouldUsePartitionPrincipalForServiceWorker(
+ dom::WorkerPrivate* aWorkerPrivate) {
+ MOZ_ASSERT(aWorkerPrivate);
+
+ // We don't use the partitioned principal for service workers if it's
+ // disabled.
+ if (!StaticPrefs::privacy_partition_serviceWorkers()) {
+ return false;
+ }
+
+ nsCOMPtr<nsICookieJarSettings> cookieJarSettings =
+ aWorkerPrivate->CookieJarSettings();
+
+ // We only support partitioned service workers when dFPI is enabled.
+ if (cookieJarSettings->GetCookieBehavior() !=
+ nsICookieService::BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN) {
+ return false;
+ }
+
+ return aWorkerPrivate->IsThirdPartyContextToTopWindow();
+}
+
+// static
+bool StoragePrincipalHelper::GetOriginAttributes(
+ nsIChannel* aChannel, mozilla::OriginAttributes& aAttributes,
+ StoragePrincipalHelper::PrincipalType aPrincipalType) {
+ nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo();
+ loadInfo->GetOriginAttributes(&aAttributes);
+
+ bool isPrivate = false;
+ nsCOMPtr<nsIPrivateBrowsingChannel> pbChannel = do_QueryInterface(aChannel);
+ if (pbChannel) {
+ nsresult rv = pbChannel->GetIsChannelPrivate(&isPrivate);
+ NS_ENSURE_SUCCESS(rv, false);
+ } else {
+ // Some channels may not implement nsIPrivateBrowsingChannel
+ nsCOMPtr<nsILoadContext> loadContext;
+ NS_QueryNotificationCallbacks(aChannel, loadContext);
+ isPrivate = loadContext && loadContext->UsePrivateBrowsing();
+ }
+ aAttributes.SyncAttributesWithPrivateBrowsing(isPrivate);
+
+ nsCOMPtr<nsICookieJarSettings> cjs;
+
+ switch (aPrincipalType) {
+ case eRegularPrincipal:
+ break;
+
+ case eStorageAccessPrincipal:
+ PrepareEffectiveStoragePrincipalOriginAttributes(aChannel, aAttributes);
+ break;
+
+ case ePartitionedPrincipal:
+ ChooseOriginAttributes(aChannel, aAttributes, true);
+ break;
+
+ case eForeignPartitionedPrincipal:
+ Unused << loadInfo->GetCookieJarSettings(getter_AddRefs(cjs));
+
+ // We only support foreign partitioned principal when dFPI is enabled.
+ // Otherwise, we will use the regular principal.
+ if (cjs->GetCookieBehavior() ==
+ nsICookieService::BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN &&
+ loadInfo->GetIsThirdPartyContextToTopWindow()) {
+ ChooseOriginAttributes(aChannel, aAttributes, true);
+ }
+ break;
+ }
+
+ return true;
+}
+
+// static
+bool StoragePrincipalHelper::GetRegularPrincipalOriginAttributes(
+ dom::Document* aDocument, OriginAttributes& aAttributes) {
+ aAttributes = mozilla::OriginAttributes();
+ if (!aDocument) {
+ return false;
+ }
+
+ nsCOMPtr<nsILoadGroup> loadGroup = aDocument->GetDocumentLoadGroup();
+ if (loadGroup) {
+ return GetRegularPrincipalOriginAttributes(loadGroup, aAttributes);
+ }
+
+ nsCOMPtr<nsIChannel> channel = aDocument->GetChannel();
+ if (!channel) {
+ return false;
+ }
+
+ return GetOriginAttributes(channel, aAttributes, eRegularPrincipal);
+}
+
+// static
+bool StoragePrincipalHelper::GetRegularPrincipalOriginAttributes(
+ nsILoadGroup* aLoadGroup, OriginAttributes& aAttributes) {
+ aAttributes = mozilla::OriginAttributes();
+ if (!aLoadGroup) {
+ return false;
+ }
+
+ nsCOMPtr<nsIInterfaceRequestor> callbacks;
+ aLoadGroup->GetNotificationCallbacks(getter_AddRefs(callbacks));
+ if (!callbacks) {
+ return false;
+ }
+
+ nsCOMPtr<nsILoadContext> loadContext = do_GetInterface(callbacks);
+ if (!loadContext) {
+ return false;
+ }
+
+ loadContext->GetOriginAttributes(aAttributes);
+ return true;
+}
+
+// static
+bool StoragePrincipalHelper::GetOriginAttributesForNetworkState(
+ nsIChannel* aChannel, OriginAttributes& aAttributes) {
+ return StoragePrincipalHelper::GetOriginAttributes(
+ aChannel, aAttributes,
+ StaticPrefs::privacy_partition_network_state() ? ePartitionedPrincipal
+ : eRegularPrincipal);
+}
+
+// static
+void StoragePrincipalHelper::GetOriginAttributesForNetworkState(
+ dom::Document* aDocument, OriginAttributes& aAttributes) {
+ aAttributes = aDocument->NodePrincipal()->OriginAttributesRef();
+
+ if (!StaticPrefs::privacy_partition_network_state()) {
+ return;
+ }
+
+ aAttributes = aDocument->PartitionedPrincipal()->OriginAttributesRef();
+}
+
+// static
+void StoragePrincipalHelper::UpdateOriginAttributesForNetworkState(
+ nsIURI* aFirstPartyURI, OriginAttributes& aAttributes) {
+ if (!StaticPrefs::privacy_partition_network_state()) {
+ return;
+ }
+
+ aAttributes.SetPartitionKey(aFirstPartyURI);
+}
+
+enum SupportedScheme { HTTP, HTTPS };
+
+static bool GetOriginAttributesWithScheme(nsIChannel* aChannel,
+ OriginAttributes& aAttributes,
+ SupportedScheme aScheme) {
+ const nsString targetScheme = aScheme == HTTP ? u"http"_ns : u"https"_ns;
+ if (!StoragePrincipalHelper::GetOriginAttributesForNetworkState(
+ aChannel, aAttributes)) {
+ return false;
+ }
+
+ if (aAttributes.mPartitionKey.IsEmpty() ||
+ aAttributes.mPartitionKey[0] != '(') {
+ return true;
+ }
+
+ nsAString::const_iterator start, end;
+ aAttributes.mPartitionKey.BeginReading(start);
+ aAttributes.mPartitionKey.EndReading(end);
+
+ MOZ_DIAGNOSTIC_ASSERT(*start == '(');
+ start++;
+
+ nsAString::const_iterator iter(start);
+ bool ok = FindCharInReadable(',', iter, end);
+ MOZ_DIAGNOSTIC_ASSERT(ok);
+
+ if (!ok) {
+ return false;
+ }
+
+ nsAutoString scheme;
+ scheme.Assign(Substring(start, iter));
+
+ if (scheme.Equals(targetScheme)) {
+ return true;
+ }
+
+ nsAutoString key;
+ key += u"("_ns;
+ key += targetScheme;
+ key.Append(Substring(iter, end));
+ aAttributes.SetPartitionKey(key);
+
+ return true;
+}
+
+// static
+bool StoragePrincipalHelper::GetOriginAttributesForHSTS(
+ nsIChannel* aChannel, OriginAttributes& aAttributes) {
+ return GetOriginAttributesWithScheme(aChannel, aAttributes, HTTP);
+}
+
+// static
+bool StoragePrincipalHelper::GetOriginAttributesForHTTPSRR(
+ nsIChannel* aChannel, OriginAttributes& aAttributes) {
+ return GetOriginAttributesWithScheme(aChannel, aAttributes, HTTPS);
+}
+
+// static
+bool StoragePrincipalHelper::GetOriginAttributes(
+ const mozilla::ipc::PrincipalInfo& aPrincipalInfo,
+ OriginAttributes& aAttributes) {
+ aAttributes = mozilla::OriginAttributes();
+
+ using Type = ipc::PrincipalInfo;
+ switch (aPrincipalInfo.type()) {
+ case Type::TContentPrincipalInfo:
+ aAttributes = aPrincipalInfo.get_ContentPrincipalInfo().attrs();
+ break;
+ case Type::TNullPrincipalInfo:
+ aAttributes = aPrincipalInfo.get_NullPrincipalInfo().attrs();
+ break;
+ case Type::TExpandedPrincipalInfo:
+ aAttributes = aPrincipalInfo.get_ExpandedPrincipalInfo().attrs();
+ break;
+ case Type::TSystemPrincipalInfo:
+ break;
+ default:
+ return false;
+ }
+
+ return true;
+}
+
+bool StoragePrincipalHelper::PartitionKeyHasBaseDomain(
+ const nsAString& aPartitionKey, const nsACString& aBaseDomain) {
+ return PartitionKeyHasBaseDomain(aPartitionKey,
+ NS_ConvertUTF8toUTF16(aBaseDomain));
+}
+
+// static
+bool StoragePrincipalHelper::PartitionKeyHasBaseDomain(
+ const nsAString& aPartitionKey, const nsAString& aBaseDomain) {
+ if (aPartitionKey.IsEmpty() || aBaseDomain.IsEmpty()) {
+ return false;
+ }
+
+ nsString scheme;
+ nsString pkBaseDomain;
+ int32_t port;
+ bool success = OriginAttributes::ParsePartitionKey(aPartitionKey, scheme,
+ pkBaseDomain, port);
+
+ if (!success) {
+ return false;
+ }
+
+ return aBaseDomain.Equals(pkBaseDomain);
+}
+
+} // namespace mozilla
diff --git a/toolkit/components/antitracking/StoragePrincipalHelper.h b/toolkit/components/antitracking/StoragePrincipalHelper.h
new file mode 100644
index 0000000000..f813417eb6
--- /dev/null
+++ b/toolkit/components/antitracking/StoragePrincipalHelper.h
@@ -0,0 +1,358 @@
+/* -*- 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_StoragePrincipalHelper_h
+#define mozilla_StoragePrincipalHelper_h
+
+#include <cstdint>
+#include "ErrorList.h"
+#include "nsStringFwd.h"
+
+/**
+ * StoragePrincipal
+ * ~~~~~~~~~~~~~~~~
+
+ * StoragePrincipal is the nsIPrincipal to be used to open the cookie jar of a
+ * resource's origin. Normally, the StoragePrincipal corresponds to the
+ * resource's origin, but, in some scenarios, it can be different: it has the
+ * `partitionKey` attribute set to the top-level “site” (i.e., scheme plus
+ * eTLD+1 of the origin of the top-level document).
+ *
+ * Each storage component should always use the StoragePrincipal instead of the
+ * 'real' one in order to implement the partitioning correctly. See the list of
+ * the components here: https://privacycg.github.io/storage-partitioning/
+ *
+ * On the web, each resource has its own origin (see
+ * https://html.spec.whatwg.org/multipage/origin.html#concept-origin) and each
+ * origin has its own cookie jar, containing cookies, storage data, cache and so
+ * on.
+ *
+ * In gecko-world, the origin and its attributes are stored and managed by the
+ * nsIPrincipal interface. Both a resource's Principal and a resource's
+ * StoragePrincipal are nsIPrincipal interfaces and, normally, they are the same
+ * object.
+ *
+ * Naming and usage
+ * ~~~~~~~~~~~~~~~~
+ *
+ * StoragePrincipal exposes four types of principals for a resource:
+ * - Regular Principal:
+ * A “first-party” principal derived from the origin of the resource. This
+ * does not have the `partitionKey` origin attribute set.
+ * - Partitioned Principal:
+ * The regular principal plus the partitionKey origin attribute set to
+ * the site of the top-level document (i.e., scheme plus eTLD+1).
+ * - Storage Access Principal:
+ * A dynamic principal that changes when a resource receives storage access.
+ * By default, when storage access is denied, this is equal to the
+ * Partitioned Principal. When storage access is granted, this is equal to
+ * the Regular Principal.
+ * - Foreign Partitioned Principal
+ * A principal that would be decided according to the fact that if the
+ * resource is a third party or not. If the resource is in a third-party
+ * context, this will be the partitioned principal. Otherwise, a regular
+ * principal will be used. Also, this doesn't like Storage Access Principal
+ * which changes according to storage access of a resource. Note that this
+ * is dFPI only; this prinipcal will always return regular principal when
+ * dFPI is disabled.
+ *
+ * Consumers of StoragePrincipal can request the principal type that meets their
+ * needs. For example, storage that should always be partitioned should choose
+ * the Partitioned Principal, while storage that should change with storage
+ * access grants should choose the Storage Access Principal. And the storage
+ * should be always partiitoned in the third-party context should use the
+ * Foreign Partitioned Principal.
+ *
+ * You can obtain these nsIPrincipal objects:
+ *
+ * From a Document:
+ * - Regular Principal: nsINode::NodePrincipal
+ * - Storage Access Principal: Document::EffectiveStoragePrincipal
+ * - Partitioned Principal: Document::PartitionedPrincipal
+ *
+ * From a Global object:
+ * - Regular Principal: nsIScriptObjectPrincipal::GetPrincipal
+ * - Storage Access Principal:
+ * nsIScriptObjectPrincipal::GetEffectiveStoragePrincipal
+ * - Partitioned Principal: nsIScriptObjectPrincipal::PartitionedPrincipal
+ *
+ * From a Worker:
+ * - Regular Principal: WorkerPrivate::GetPrincipal (main-thread)
+ * - Regular Principal: WorkerPrivate::GetPrincipalInfo (worker thread)
+ * - Storage Access Principal: WorkerPrivate::GetEffectiveStoragePrincipalInfo
+ * (worker-thread)
+ *
+ * For a nsIChannel, the final principals must be calculated and they can be
+ * obtained by calling:
+ * - Regular Principal: nsIScriptSecurityManager::getChannelResultPrincipal
+ * - Storage Access Principal:
+ * nsIScriptSecurityManager::getChannelResultStoragePrincipal
+ * - Partitioned and regular Principal:
+ * nsIScriptSecurityManager::getChannelResultPrincipals
+ *
+ * Each use of nsIPrincipal is unique and it should be reviewed by anti-tracking
+ * peers. But we can group the use of nsIPrincipal in these categories:
+ *
+ * - Network loading: use the Regular Principal
+ * - Cache, not directly visible by content (network cache, HSTS, image cache,
+ * etc): Use the Storage Access Principal (in the future we will use the
+ * Partitioned Principal, but this part is not done yet)
+ * - Storage APIs or anything that is written on disk (or kept in memory in
+ * private-browsing): use the Storage Access Principal
+ * - PostMessage: if in the agent-cluster, use the Regular Principal. Otherwise,
+ * use the Storage Access Principal
+ *
+ * Storage access permission
+ * ~~~~~~~~~~~~~~~~~~~~~~~~~
+ *
+ * When the storage access permission is granted, any of the Storage Access
+ * Principal getter methods will return the Regular Principal instead of the
+ * Partitioned Principal, and each storage component should consider the new
+ * principal only.
+ *
+ * The trackers and the 3rd parties (in dFPI) will have access to its
+ first-party
+ * cookie jar, escaping from its partitioning.
+ *
+ * Storage access permissions can be granted in several ways:
+ * - The Storage Access API
+ * (https://developer.mozilla.org/en-US/docs/Web/API/Storage_Access_API)
+ * - ETP’s heuristics
+ *
+ (https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Privacy/Storage_access_policy#Storage_access_grants)
+ * - A dFPI-specific login heuristic
+ * (https://bugzilla.mozilla.org/show_bug.cgi?id=1616585#c12)
+ *
+ * There are several ways to receive storage-permission notifications. You can
+ * use these notifications to re-initialize components, to nullify or enable
+ them
+ * to use the “new” effective StoragePrincipal. The list of the notifications
+ is:
+ *
+ * - Add some code in nsGlobalWindowInner::StorageAccessPermissionGranted().
+ * - WorkerScope::StorageAccessPermissionGranted for Workers.
+ * - observe the permission changes (not recommended)
+ *
+ * Scope of Storage Access
+ * ~~~~~~~~~~~~~~~~~~~~~~~
+ *
+ * Immediately after access is granted, the permission is propagated and
+ notified
+ * to any contexts (windows and workers) in the same agent-cluster
+ * (BrowserContextGroup).
+ *
+ * This means that if A.com has 2 iframes with B.com, and one of the 2 Bs
+ obtains
+ * the storage access, the other B will be notified too. Other B.com, 3rd
+ parties
+ * in other agent clusters will not obtain the storage permission.
+ *
+ * When the page is reloaded or is loaded for the first time, if it contains
+ * B.com, and B.com has received the storage permission for the same first-party
+ * in a previous loading, B.com will have the storage access permission granted
+ * immediately.
+ *
+ * Cookies, LocalStorage, indexedDB
+ * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ *
+ * When granting storage permission, several storage and channel API getters and
+ * constructors will start exposing first-party cookie jar objects
+ (localStorage,
+ * BroadcastChannel, etc).
+ *
+ * There is a side effect of this change: If a tracker has a reference to these
+ * objects pre-storage permission granting, it will be able to interact with the
+ * partitioned and the non-partitioned cookie jar at the same time. Note that
+ * similar synchronization can be done server-side too. Because of this, we
+ don’t
+ * think that privacy-wise, this is an issue.
+ *
+ * localStorage supports StoragePrincipal, and will be switched after storage
+ * access is granted. Trackers listed in the pref
+ * privacy.restrict3rdpartystorage.partitionedHosts will use another special
+ * partitioned session-only storage called PartitionedLocalStorage.
+ *
+ * sessionStorage is not covered by StoragePrincipal, but is double-keyed using
+ * the top-level site when dFPI is active
+ * (https://bugzilla.mozilla.org/show_bug.cgi?id=1629707).
+ *
+ * SharedWorkers and BroadcastChannels
+ * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ *
+ * SharedWorker and BroadcastChannel instances latch the effective storage
+ * principal at the moment of their creation. Existing bindings to the
+ * partitioned storage principal will continue to exist and operate even as it
+ * becomes possible to create bindings associated with the Regular Principal.
+ * This makes it possible for such globals to bi-directionally bridge
+ information
+ * between partitioned and non-partitioned principals.
+ *
+ * This is true until the page is reloaded. After the reload, the partitioned
+ * cookie jar will no longer be accessible.
+ *
+ * We are planning to clear the partitioned site-data as soon as the page is
+ * reloaded or dismissed (not done yet - bug 1628313).
+ *
+ * {Dedicated,Shared,Service}Workers
+ * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ *
+ * The storage access permission propagation happens with a ControlRunnable.
+ This
+ * could impact the use of sync event-loops. Take a reference of the principal
+ * you want to use because it can change!
+ *
+ * ServiceWorkers are currently disabled for partitioned contexts.
+ *
+ * Client API uses the regular nsIPrincipal always because there is not a direct
+ * connection between this API and the cookie jar. If we want to support
+ * ServiceWorkers in partitioned context, this part must be revisited.
+ */
+
+class nsIChannel;
+class nsICookieJarSettings;
+class nsIDocShell;
+class nsILoadGroup;
+class nsIPrincipal;
+class nsIURI;
+class nsPIDOMWindowInner;
+
+namespace mozilla {
+
+namespace dom {
+class Document;
+class WorkerPrivate;
+} // namespace dom
+
+namespace ipc {
+class PrincipalInfo;
+}
+
+class OriginAttributes;
+
+class StoragePrincipalHelper final {
+ public:
+ static nsresult Create(nsIChannel* aChannel, nsIPrincipal* aPrincipal,
+ bool aForceIsolation,
+ nsIPrincipal** aStoragePrincipal);
+
+ static nsresult CreatePartitionedPrincipalForServiceWorker(
+ nsIPrincipal* aPrincipal, nsICookieJarSettings* aCookieJarSettings,
+ nsIPrincipal** aPartitionedPrincipal);
+
+ static nsresult PrepareEffectiveStoragePrincipalOriginAttributes(
+ nsIChannel* aChannel, OriginAttributes& aOriginAttributes);
+
+ // A helper function to verify storage principal info with the principal info.
+ static bool VerifyValidStoragePrincipalInfoForPrincipalInfo(
+ const mozilla::ipc::PrincipalInfo& aStoragePrincipalInfo,
+ const mozilla::ipc::PrincipalInfo& aPrincipalInfo);
+
+ // A helper function to verify client principal info with the principal info.
+ //
+ // Note that the client principal refers the principal of the client, which is
+ // supposed to be the foreign partitioned principal.
+ static bool VerifyValidClientPrincipalInfoForPrincipalInfo(
+ const mozilla::ipc::PrincipalInfo& aClientPrincipalInfo,
+ const mozilla::ipc::PrincipalInfo& aPrincipalInfo);
+
+ enum PrincipalType {
+ // This is the first-party principal.
+ eRegularPrincipal,
+
+ // This is a dynamic principal based on the current state of the origin. If
+ // the origin has the storage permission granted, effective storagePrincipal
+ // will be the regular principal, otherwise, the partitioned Principal
+ // will be used.
+ eStorageAccessPrincipal,
+
+ // This is the first-party principal, plus, First-party isolation attribute
+ // set.
+ ePartitionedPrincipal,
+
+ // This principal returns different results based on whether its associated
+ // channel/window is in a third-party context. While in a third-party
+ // context, it returns the partitioned principal; otherwise, it returns the
+ // regular principal.
+ //
+ // Note that this principal is not a dynamic principal like
+ // `eStorageAccessPrincipal`, which changes depending on whether the storage
+ // access permission is granted. This principal doesn't take the storage
+ // access permission into consideration. Also, this principle is used in
+ // dFPI only, meaning that it always returns the regular principal when dFP
+ // Is disabled.
+ eForeignPartitionedPrincipal,
+ };
+
+ /**
+ * Extract the principal from the channel/document according to the given
+ * principal type.
+ */
+ static nsresult GetPrincipal(nsIChannel* aChannel,
+ PrincipalType aPrincipalType,
+ nsIPrincipal** aPrincipal);
+ static nsresult GetPrincipal(nsPIDOMWindowInner* aWindow,
+ PrincipalType aPrincipalType,
+ nsIPrincipal** aPrincipal);
+
+ // Check if we need to use the partitioned principal for the service worker of
+ // the given docShell. Please do not use this API unless you cannot get the
+ // foreign partitioned principal, e.g. creating the inital about:blank page.
+ static bool ShouldUsePartitionPrincipalForServiceWorker(
+ nsIDocShell* aDocShell);
+
+ static bool ShouldUsePartitionPrincipalForServiceWorker(
+ dom::WorkerPrivate* aWorkerPrivate);
+
+ /**
+ * Extract the right OriginAttributes from the channel's triggering
+ * principal.
+ */
+ static bool GetOriginAttributes(nsIChannel* aChannel,
+ OriginAttributes& aAttributes,
+ PrincipalType aPrincipalType);
+
+ static bool GetRegularPrincipalOriginAttributes(
+ dom::Document* aDocument, OriginAttributes& aAttributes);
+
+ static bool GetRegularPrincipalOriginAttributes(
+ nsILoadGroup* aLoadGroup, OriginAttributes& aAttributes);
+
+ // These methods return the correct originAttributes to be used for network
+ // state components (HSTS, network cache, image-cache, and so on).
+ static bool GetOriginAttributesForNetworkState(nsIChannel* aChannel,
+ OriginAttributes& aAttributes);
+ static void GetOriginAttributesForNetworkState(dom::Document* aDocument,
+ OriginAttributes& aAttributes);
+ static void UpdateOriginAttributesForNetworkState(
+ nsIURI* aFirstPartyURI, OriginAttributes& aAttributes);
+
+ // For HSTS we want to force 'HTTP' in the partition key.
+ static bool GetOriginAttributesForHSTS(nsIChannel* aChannel,
+ OriginAttributes& aAttributes);
+
+ // Like the function above, this function forces `HTTPS` in the partition key.
+ // The OA created by this function is mainly used in DNS cache. The spec
+ // specifies that the presence of HTTPS RR for an origin also indicates that
+ // all HTTP resources are available over HTTPS, so we use this function to
+ // ensure that all HTTPS RRs in DNS cache are accessed by HTTPS requests only.
+ static bool GetOriginAttributesForHTTPSRR(nsIChannel* aChannel,
+ OriginAttributes& aAttributes);
+
+ // Get the origin attributes from a PrincipalInfo
+ static bool GetOriginAttributes(
+ const mozilla::ipc::PrincipalInfo& aPrincipalInfo,
+ OriginAttributes& aAttributes);
+
+ static bool PartitionKeyHasBaseDomain(const nsAString& aPartitionKey,
+ const nsACString& aBaseDomain);
+
+ static bool PartitionKeyHasBaseDomain(const nsAString& aPartitionKey,
+ const nsAString& aBaseDomain);
+};
+
+} // namespace mozilla
+
+#endif // mozilla_StoragePrincipalHelper_h
diff --git a/toolkit/components/antitracking/TemporaryAccessGrantObserver.cpp b/toolkit/components/antitracking/TemporaryAccessGrantObserver.cpp
new file mode 100644
index 0000000000..a22b0caee8
--- /dev/null
+++ b/toolkit/components/antitracking/TemporaryAccessGrantObserver.cpp
@@ -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/. */
+
+#include "TemporaryAccessGrantObserver.h"
+
+#include "mozilla/PermissionManager.h"
+#include "mozilla/Services.h"
+#include "nsIObserverService.h"
+#include "nsTHashtable.h"
+#include "nsXULAppAPI.h"
+
+using namespace mozilla;
+
+UniquePtr<TemporaryAccessGrantObserver::ObserversTable>
+ TemporaryAccessGrantObserver::sObservers;
+
+TemporaryAccessGrantObserver::TemporaryAccessGrantObserver(
+ PermissionManager* aPM, nsIPrincipal* aPrincipal, const nsACString& aType)
+ : mPM(aPM), mPrincipal(aPrincipal), mType(aType) {
+ MOZ_ASSERT(XRE_IsParentProcess(),
+ "Enforcing temporary access grant lifetimes can only be done in "
+ "the parent process");
+}
+
+NS_IMPL_ISUPPORTS(TemporaryAccessGrantObserver, nsIObserver, nsINamed)
+
+// static
+void TemporaryAccessGrantObserver::Create(PermissionManager* aPM,
+ nsIPrincipal* aPrincipal,
+ const nsACString& aType) {
+ MOZ_ASSERT(XRE_IsParentProcess());
+
+ if (!sObservers) {
+ sObservers = MakeUnique<ObserversTable>();
+ }
+ sObservers->LookupOrInsertWith(
+ std::make_pair(nsCOMPtr<nsIPrincipal>(aPrincipal), nsCString(aType)),
+ [&]() -> nsCOMPtr<nsITimer> {
+ // Only create a new observer if we don't have a matching
+ // entry in our hashtable.
+ nsCOMPtr<nsITimer> timer;
+ RefPtr<TemporaryAccessGrantObserver> observer =
+ new TemporaryAccessGrantObserver(aPM, aPrincipal, aType);
+ nsresult rv = NS_NewTimerWithObserver(getter_AddRefs(timer), observer,
+ 24 * 60 * 60 * 1000, // 24 hours
+ nsITimer::TYPE_ONE_SHOT);
+
+ if (NS_SUCCEEDED(rv)) {
+ observer->SetTimer(timer);
+ return timer;
+ }
+ timer->Cancel();
+ return nullptr;
+ });
+}
+
+void TemporaryAccessGrantObserver::SetTimer(nsITimer* aTimer) {
+ mTimer = aTimer;
+ nsCOMPtr<nsIObserverService> observerService =
+ mozilla::services::GetObserverService();
+ if (observerService) {
+ observerService->AddObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID, false);
+ }
+}
+
+NS_IMETHODIMP
+TemporaryAccessGrantObserver::Observe(nsISupports* aSubject, const char* aTopic,
+ const char16_t* aData) {
+ if (strcmp(aTopic, NS_TIMER_CALLBACK_TOPIC) == 0) {
+ Unused << mPM->RemoveFromPrincipal(mPrincipal, mType);
+
+ MOZ_ASSERT(sObservers);
+ sObservers->Remove(std::make_pair(mPrincipal, mType));
+ } else if (strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID) == 0) {
+ nsCOMPtr<nsIObserverService> observerService =
+ mozilla::services::GetObserverService();
+ if (observerService) {
+ observerService->RemoveObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID);
+ }
+ if (mTimer) {
+ mTimer->Cancel();
+ mTimer = nullptr;
+ }
+ sObservers.reset();
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+TemporaryAccessGrantObserver::GetName(nsACString& aName) {
+ aName.AssignLiteral("TemporaryAccessGrantObserver");
+ return NS_OK;
+}
diff --git a/toolkit/components/antitracking/TemporaryAccessGrantObserver.h b/toolkit/components/antitracking/TemporaryAccessGrantObserver.h
new file mode 100644
index 0000000000..09ff93311a
--- /dev/null
+++ b/toolkit/components/antitracking/TemporaryAccessGrantObserver.h
@@ -0,0 +1,87 @@
+/* -*- 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_temporaryaccessgrantobserver_h
+#define mozilla_temporaryaccessgrantobserver_h
+
+#include "mozilla/PrincipalHashKey.h"
+#include "nsCOMPtr.h"
+#include "nsHashKeys.h"
+#include "nsHashtablesFwd.h"
+#include "nsTHashMap.h"
+#include "nsINamed.h"
+#include "nsIObserver.h"
+#include "nsString.h"
+#include "PLDHashTable.h"
+
+class nsITimer;
+class TemporaryAccessGrantCacheKey;
+
+namespace mozilla {
+
+class PermissionManager;
+
+class TemporaryAccessGrantCacheKey : public PrincipalHashKey {
+ public:
+ typedef std::pair<nsCOMPtr<nsIPrincipal>, nsCString> KeyType;
+ typedef const KeyType* KeyTypePointer;
+
+ explicit TemporaryAccessGrantCacheKey(KeyTypePointer aKey)
+ : PrincipalHashKey(aKey->first), mType(aKey->second) {}
+ TemporaryAccessGrantCacheKey(TemporaryAccessGrantCacheKey&& aOther) = default;
+
+ ~TemporaryAccessGrantCacheKey() = default;
+
+ KeyType GetKey() const { return std::make_pair(mPrincipal, mType); }
+ bool KeyEquals(KeyTypePointer aKey) const {
+ return PrincipalHashKey::KeyEquals(aKey->first) && mType == aKey->second;
+ }
+
+ static KeyTypePointer KeyToPointer(KeyType& aKey) { return &aKey; }
+ static PLDHashNumber HashKey(KeyTypePointer aKey) {
+ if (!aKey) {
+ return 0;
+ }
+
+ return HashGeneric(PrincipalHashKey::HashKey(aKey->first),
+ HashString(aKey->second));
+ }
+
+ enum { ALLOW_MEMMOVE = true };
+
+ private:
+ nsCString mType;
+};
+
+class TemporaryAccessGrantObserver final : public nsIObserver, public nsINamed {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIOBSERVER
+ NS_DECL_NSINAMED
+
+ static void Create(PermissionManager* aPM, nsIPrincipal* aPrincipal,
+ const nsACString& aType);
+
+ void SetTimer(nsITimer* aTimer);
+
+ private:
+ TemporaryAccessGrantObserver(PermissionManager* aPM, nsIPrincipal* aPrincipal,
+ const nsACString& aType);
+ ~TemporaryAccessGrantObserver() = default;
+
+ private:
+ using ObserversTable =
+ nsTHashMap<TemporaryAccessGrantCacheKey, nsCOMPtr<nsITimer>>;
+ static UniquePtr<ObserversTable> sObservers;
+ nsCOMPtr<nsITimer> mTimer;
+ RefPtr<PermissionManager> mPM;
+ nsCOMPtr<nsIPrincipal> mPrincipal;
+ nsCString mType;
+};
+
+} // namespace mozilla
+
+#endif // mozilla_temporaryaccessgrantobserver_h
diff --git a/toolkit/components/antitracking/TrackingDBService.sys.mjs b/toolkit/components/antitracking/TrackingDBService.sys.mjs
new file mode 100644
index 0000000000..9f3b952a65
--- /dev/null
+++ b/toolkit/components/antitracking/TrackingDBService.sys.mjs
@@ -0,0 +1,375 @@
+/* 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/. */
+
+import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
+import { Sqlite } from "resource://gre/modules/Sqlite.sys.mjs";
+
+const SCHEMA_VERSION = 1;
+const TRACKERS_BLOCKED_COUNT = "contentblocking.trackers_blocked_count";
+
+const lazy = {};
+
+XPCOMUtils.defineLazyGetter(lazy, "DB_PATH", function () {
+ return PathUtils.join(PathUtils.profileDir, "protections.sqlite");
+});
+
+XPCOMUtils.defineLazyPreferenceGetter(
+ lazy,
+ "social_enabled",
+ "privacy.socialtracking.block_cookies.enabled",
+ false
+);
+
+XPCOMUtils.defineLazyPreferenceGetter(
+ lazy,
+ "milestoneMessagingEnabled",
+ "browser.contentblocking.cfr-milestone.enabled",
+ false
+);
+
+XPCOMUtils.defineLazyPreferenceGetter(
+ lazy,
+ "milestones",
+ "browser.contentblocking.cfr-milestone.milestones",
+ "[]",
+ null,
+ JSON.parse
+);
+
+XPCOMUtils.defineLazyPreferenceGetter(
+ lazy,
+ "oldMilestone",
+ "browser.contentblocking.cfr-milestone.milestone-achieved",
+ 0
+);
+
+// How often we check if the user is eligible for seeing a "milestone"
+// doorhanger. 24 hours by default.
+XPCOMUtils.defineLazyPreferenceGetter(
+ lazy,
+ "MILESTONE_UPDATE_INTERVAL",
+ "browser.contentblocking.cfr-milestone.update-interval",
+ 24 * 60 * 60 * 1000
+);
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ AsyncShutdown: "resource://gre/modules/AsyncShutdown.sys.mjs",
+ DeferredTask: "resource://gre/modules/DeferredTask.sys.mjs",
+});
+
+/**
+ * All SQL statements should be defined here.
+ */
+const SQL = {
+ createEvents:
+ "CREATE TABLE events (" +
+ "id INTEGER PRIMARY KEY, " +
+ "type INTEGER NOT NULL, " +
+ "count INTEGER NOT NULL, " +
+ "timestamp DATE " +
+ ");",
+
+ addEvent:
+ "INSERT INTO events (type, count, timestamp) " +
+ "VALUES (:type, 1, date(:date));",
+
+ incrementEvent: "UPDATE events SET count = count + 1 WHERE id = :id;",
+
+ selectByTypeAndDate:
+ "SELECT * FROM events " +
+ "WHERE type = :type " +
+ "AND timestamp = date(:date);",
+
+ deleteEventsRecords: "DELETE FROM events;",
+
+ removeRecordsSince: "DELETE FROM events WHERE timestamp >= date(:date);",
+
+ selectByDateRange:
+ "SELECT * FROM events " +
+ "WHERE timestamp BETWEEN date(:dateFrom) AND date(:dateTo);",
+
+ sumAllEvents: "SELECT sum(count) FROM events;",
+
+ getEarliestDate:
+ "SELECT timestamp FROM events ORDER BY timestamp ASC LIMIT 1;",
+};
+
+/**
+ * Creates the database schema.
+ */
+async function createDatabase(db) {
+ await db.execute(SQL.createEvents);
+}
+
+async function removeAllRecords(db) {
+ await db.execute(SQL.deleteEventsRecords);
+}
+
+async function removeRecordsSince(db, date) {
+ await db.execute(SQL.removeRecordsSince, { date });
+}
+
+export function TrackingDBService() {
+ this._initPromise = this._initialize();
+}
+
+TrackingDBService.prototype = {
+ classID: Components.ID("{3c9c43b6-09eb-4ed2-9b87-e29f4221eef0}"),
+ QueryInterface: ChromeUtils.generateQI(["nsITrackingDBService"]),
+ // This is the connection to the database, opened in _initialize and closed on _shutdown.
+ _db: null,
+ waitingTasks: new Set(),
+ finishedShutdown: true,
+
+ async ensureDB() {
+ await this._initPromise;
+ return this._db;
+ },
+
+ async _initialize() {
+ let db = await Sqlite.openConnection({ path: lazy.DB_PATH });
+
+ try {
+ // Check to see if we need to perform any migrations.
+ let dbVersion = parseInt(await db.getSchemaVersion());
+
+ // getSchemaVersion() returns a 0 int if the schema
+ // version is undefined.
+ if (dbVersion === 0) {
+ await createDatabase(db);
+ } else if (dbVersion < SCHEMA_VERSION) {
+ // TODO
+ // await upgradeDatabase(db, dbVersion, SCHEMA_VERSION);
+ }
+
+ await db.setSchemaVersion(SCHEMA_VERSION);
+ } catch (e) {
+ // Close the DB connection before passing the exception to the consumer.
+ await db.close();
+ throw e;
+ }
+
+ lazy.AsyncShutdown.profileBeforeChange.addBlocker(
+ "TrackingDBService: Shutting down the content blocking database.",
+ () => this._shutdown()
+ );
+ this.finishedShutdown = false;
+ this._db = db;
+ },
+
+ async _shutdown() {
+ let db = await this.ensureDB();
+ this.finishedShutdown = true;
+ await Promise.all(Array.from(this.waitingTasks, task => task.finalize()));
+ await db.close();
+ },
+
+ async recordContentBlockingLog(data) {
+ if (this.finishedShutdown) {
+ // The database has already been closed.
+ return;
+ }
+ let task = new lazy.DeferredTask(async () => {
+ try {
+ await this.saveEvents(data);
+ } finally {
+ this.waitingTasks.delete(task);
+ }
+ }, 0);
+ task.arm();
+ this.waitingTasks.add(task);
+ },
+
+ identifyType(events) {
+ let result = null;
+ let isTracker = false;
+ for (let [state, blocked] of events) {
+ if (
+ state &
+ Ci.nsIWebProgressListener.STATE_LOADED_LEVEL_1_TRACKING_CONTENT ||
+ state & Ci.nsIWebProgressListener.STATE_LOADED_LEVEL_2_TRACKING_CONTENT
+ ) {
+ isTracker = true;
+ }
+ if (blocked) {
+ if (
+ state &
+ Ci.nsIWebProgressListener.STATE_BLOCKED_FINGERPRINTING_CONTENT ||
+ state &
+ Ci.nsIWebProgressListener.STATE_REPLACED_FINGERPRINTING_CONTENT
+ ) {
+ result = Ci.nsITrackingDBService.FINGERPRINTERS_ID;
+ } else if (
+ // If STP is enabled and either a social tracker or cookie is blocked.
+ lazy.social_enabled &&
+ (state &
+ Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_SOCIALTRACKER ||
+ state &
+ Ci.nsIWebProgressListener.STATE_BLOCKED_SOCIALTRACKING_CONTENT)
+ ) {
+ result = Ci.nsITrackingDBService.SOCIAL_ID;
+ } else if (
+ // If there is a tracker blocked. If there is a social tracker blocked, but STP is not enabled.
+ state & Ci.nsIWebProgressListener.STATE_BLOCKED_TRACKING_CONTENT ||
+ state & Ci.nsIWebProgressListener.STATE_BLOCKED_SOCIALTRACKING_CONTENT
+ ) {
+ result = Ci.nsITrackingDBService.TRACKERS_ID;
+ } else if (
+ // If a tracking cookie was blocked attribute it to tracking cookies.
+ // This includes social tracking cookies since STP is not enabled.
+ state & Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER ||
+ state & Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_SOCIALTRACKER
+ ) {
+ result = Ci.nsITrackingDBService.TRACKING_COOKIES_ID;
+ } else if (
+ state &
+ Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_BY_PERMISSION ||
+ state & Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_ALL ||
+ state & Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_FOREIGN
+ ) {
+ result = Ci.nsITrackingDBService.OTHER_COOKIES_BLOCKED_ID;
+ } else if (
+ state & Ci.nsIWebProgressListener.STATE_BLOCKED_CRYPTOMINING_CONTENT
+ ) {
+ result = Ci.nsITrackingDBService.CRYPTOMINERS_ID;
+ }
+ }
+ }
+ // if a cookie is blocked for any reason, and it is identified as a tracker,
+ // then add to the tracking cookies count.
+ if (
+ result == Ci.nsITrackingDBService.OTHER_COOKIES_BLOCKED_ID &&
+ isTracker
+ ) {
+ result = Ci.nsITrackingDBService.TRACKING_COOKIES_ID;
+ }
+
+ return result;
+ },
+
+ /**
+ * Saves data rows to the DB.
+ * @param data
+ * An array of JS objects representing row items to save.
+ */
+ async saveEvents(data) {
+ let db = await this.ensureDB();
+ let log = JSON.parse(data);
+ try {
+ await db.executeTransaction(async () => {
+ for (let thirdParty in log) {
+ // "type" will be undefined if there is no blocking event, or 0 if it is a
+ // cookie which is not a tracking cookie. These should not be added to the database.
+ let type = this.identifyType(log[thirdParty]);
+ if (type) {
+ // Send the blocked event to Telemetry
+ Services.telemetry.scalarAdd(TRACKERS_BLOCKED_COUNT, 1);
+
+ // today is a date "YYY-MM-DD" which can compare with what is
+ // already saved in the database.
+ let today = new Date().toISOString().split("T")[0];
+ let row = await db.executeCached(SQL.selectByTypeAndDate, {
+ type,
+ date: today,
+ });
+ let todayEntry = row[0];
+
+ // If previous events happened today (local time), aggregate them.
+ if (todayEntry) {
+ let id = todayEntry.getResultByName("id");
+ await db.executeCached(SQL.incrementEvent, { id });
+ } else {
+ // Event is created on a new day, add a new entry.
+ await db.executeCached(SQL.addEvent, { type, date: today });
+ }
+ }
+ }
+ });
+ } catch (e) {
+ console.error(e);
+ }
+
+ // If milestone CFR messaging is not enabled we don't need to update the milestone pref or send the event.
+ // We don't do this check too frequently, for performance reasons.
+ if (
+ !lazy.milestoneMessagingEnabled ||
+ (this.lastChecked &&
+ Date.now() - this.lastChecked < lazy.MILESTONE_UPDATE_INTERVAL)
+ ) {
+ return;
+ }
+ this.lastChecked = Date.now();
+ let totalSaved = await this.sumAllEvents();
+
+ let reachedMilestone = null;
+ let nextMilestone = null;
+ for (let [index, milestone] of lazy.milestones.entries()) {
+ if (totalSaved >= milestone) {
+ reachedMilestone = milestone;
+ nextMilestone = lazy.milestones[index + 1];
+ }
+ }
+
+ // Show the milestone message if the user is not too close to the next milestone.
+ // Or if there is no next milestone.
+ if (
+ reachedMilestone &&
+ (!nextMilestone || nextMilestone - totalSaved > 3000) &&
+ (!lazy.oldMilestone || lazy.oldMilestone < reachedMilestone)
+ ) {
+ Services.obs.notifyObservers(
+ {
+ wrappedJSObject: {
+ event: "ContentBlockingMilestone",
+ },
+ },
+ "SiteProtection:ContentBlockingMilestone"
+ );
+ }
+ },
+
+ async clearAll() {
+ let db = await this.ensureDB();
+ await removeAllRecords(db);
+ },
+
+ async clearSince(date) {
+ let db = await this.ensureDB();
+ date = new Date(date).toISOString();
+ await removeRecordsSince(db, date);
+ },
+
+ async getEventsByDateRange(dateFrom, dateTo) {
+ let db = await this.ensureDB();
+ dateFrom = new Date(dateFrom).toISOString();
+ dateTo = new Date(dateTo).toISOString();
+ return db.execute(SQL.selectByDateRange, { dateFrom, dateTo });
+ },
+
+ async sumAllEvents() {
+ let db = await this.ensureDB();
+ let results = await db.execute(SQL.sumAllEvents);
+ if (!results[0]) {
+ return 0;
+ }
+ let total = results[0].getResultByName("sum(count)");
+ return total || 0;
+ },
+
+ async getEarliestRecordedDate() {
+ let db = await this.ensureDB();
+ let date = await db.execute(SQL.getEarliestDate);
+ if (!date[0]) {
+ return null;
+ }
+ let earliestDate = date[0].getResultByName("timestamp");
+
+ // All of our dates are recorded as 00:00 GMT, add 12 hours to the timestamp
+ // to ensure we display the correct date no matter the user's location.
+ let hoursInMS12 = 12 * 60 * 60 * 1000;
+ let earliestDateInMS = new Date(earliestDate).getTime() + hoursInMS12;
+
+ return earliestDateInMS || null;
+ },
+};
diff --git a/toolkit/components/antitracking/URLDecorationAnnotationsService.sys.mjs b/toolkit/components/antitracking/URLDecorationAnnotationsService.sys.mjs
new file mode 100644
index 0000000000..ca285e972d
--- /dev/null
+++ b/toolkit/components/antitracking/URLDecorationAnnotationsService.sys.mjs
@@ -0,0 +1,70 @@
+/* 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/. */
+
+export function URLDecorationAnnotationsService() {}
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ RemoteSettings: "resource://services-settings/remote-settings.sys.mjs",
+});
+
+const COLLECTION_NAME = "anti-tracking-url-decoration";
+const PREF_NAME = "privacy.restrict3rdpartystorage.url_decorations";
+
+URLDecorationAnnotationsService.prototype = {
+ classID: Components.ID("{5874af6d-5719-4e1b-b155-ef4eae7fcb32}"),
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIObserver",
+ "nsIURLDecorationAnnotationsService",
+ ]),
+
+ _initialized: false,
+ _prefBranch: null,
+
+ onDataAvailable(entries) {
+ // Use this technique in order to ensure the pref cannot be changed by the
+ // user e.g. through about:config. This preferences is only intended as a
+ // mechanism for reflecting this data to content processes.
+ if (this._prefBranch === null) {
+ this._prefBranch = Services.prefs.getDefaultBranch("");
+ }
+
+ const branch = this._prefBranch;
+ branch.unlockPref(PREF_NAME);
+ branch.setStringPref(
+ PREF_NAME,
+ entries.map(x => x.token.replace(/ /, "%20")).join(" ")
+ );
+ branch.lockPref(PREF_NAME);
+ },
+
+ observe(aSubject, aTopic, aData) {
+ if (aTopic == "profile-after-change") {
+ this.ensureUpdated();
+ }
+ },
+
+ ensureUpdated() {
+ if (this._initialized) {
+ return Promise.resolve();
+ }
+ this._initialized = true;
+
+ const client = lazy.RemoteSettings(COLLECTION_NAME);
+ client.on("sync", event => {
+ let {
+ data: { current },
+ } = event;
+ this.onDataAvailable(current);
+ });
+
+ // Now trigger an update from the server if necessary to get a fresh copy
+ // of the data
+ return client.get({}).then(entries => {
+ this.onDataAvailable(entries);
+ return undefined;
+ });
+ },
+};
diff --git a/toolkit/components/antitracking/URLDecorationStripper.cpp b/toolkit/components/antitracking/URLDecorationStripper.cpp
new file mode 100644
index 0000000000..38af391945
--- /dev/null
+++ b/toolkit/components/antitracking/URLDecorationStripper.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 "URLDecorationStripper.h"
+
+#include "mozilla/Preferences.h"
+#include "nsCharSeparatedTokenizer.h"
+#include "nsEffectiveTLDService.h"
+#include "nsIURI.h"
+#include "nsIURIMutator.h"
+#include "nsURLHelper.h"
+
+namespace {
+static const char* kPrefName =
+ "privacy.restrict3rdpartystorage.url_decorations";
+} // namespace
+
+namespace mozilla {
+
+nsresult URLDecorationStripper::StripTrackingIdentifiers(nsIURI* aURI,
+ nsACString& aOutSpec) {
+ nsAutoString tokenList;
+ nsresult rv = Preferences::GetString(kPrefName, tokenList);
+ ToLowerCase(tokenList);
+
+ nsAutoCString path;
+ rv = aURI->GetPathQueryRef(path);
+ NS_ENSURE_SUCCESS(rv, rv);
+ ToLowerCase(path);
+
+ int32_t queryBegins = path.FindChar('?');
+ // Only positive values are valid since the path must begin with a '/'.
+ if (queryBegins > 0) {
+ for (const nsAString& token : tokenList.Split(' ')) {
+ if (token.IsEmpty()) {
+ continue;
+ }
+
+ nsAutoString value;
+ if (URLParams::Extract(Substring(path, queryBegins + 1), token, value) &&
+ !value.IsVoid()) {
+ // Tracking identifier found in the URL!
+ return StripToRegistrableDomain(aURI, aOutSpec);
+ }
+ }
+ }
+
+ return aURI->GetSpec(aOutSpec);
+}
+
+nsresult URLDecorationStripper::StripToRegistrableDomain(nsIURI* aURI,
+ nsACString& aOutSpec) {
+ NS_MutateURI mutator(aURI);
+ mutator.SetPathQueryRef(""_ns).SetUserPass(""_ns);
+
+ RefPtr<nsEffectiveTLDService> etldService =
+ nsEffectiveTLDService::GetInstance();
+ NS_ENSURE_TRUE(etldService, NS_ERROR_FAILURE);
+ nsAutoCString baseDomain;
+ nsresult rv = etldService->GetBaseDomain(aURI, 0, baseDomain);
+ if (NS_SUCCEEDED(rv)) {
+ mutator.SetHost(baseDomain);
+ } else {
+ // If this is an IP address or something like "localhost", ignore the error.
+ if (rv != NS_ERROR_HOST_IS_IP_ADDRESS &&
+ rv != NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS) {
+ return rv;
+ }
+ }
+
+ nsCOMPtr<nsIURI> uri;
+ rv = mutator.Finalize(uri);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return uri->GetSpec(aOutSpec);
+}
+
+} // namespace mozilla
diff --git a/toolkit/components/antitracking/URLDecorationStripper.h b/toolkit/components/antitracking/URLDecorationStripper.h
new file mode 100644
index 0000000000..9bad66f250
--- /dev/null
+++ b/toolkit/components/antitracking/URLDecorationStripper.h
@@ -0,0 +1,26 @@
+/* -*- 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_URLDecorationStripper_h
+#define mozilla_URLDecorationStripper_h
+
+#include "nsStringFwd.h"
+
+class nsIURI;
+
+namespace mozilla {
+
+class URLDecorationStripper final {
+ public:
+ static nsresult StripTrackingIdentifiers(nsIURI* aURI, nsACString& aOutSpec);
+
+ private:
+ static nsresult StripToRegistrableDomain(nsIURI* aURI, nsACString& aOutSpec);
+};
+
+} // namespace mozilla
+
+#endif // mozilla_URLDecorationStripper_h
diff --git a/toolkit/components/antitracking/URLQueryStringStripper.cpp b/toolkit/components/antitracking/URLQueryStringStripper.cpp
new file mode 100644
index 0000000000..481e9a316b
--- /dev/null
+++ b/toolkit/components/antitracking/URLQueryStringStripper.cpp
@@ -0,0 +1,283 @@
+/* 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 "URLQueryStringStripper.h"
+
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/StaticPrefs_privacy.h"
+#include "mozilla/StaticPtr.h"
+#include "mozilla/Unused.h"
+#include "mozilla/Telemetry.h"
+
+#include "nsEffectiveTLDService.h"
+#include "nsISupportsImpl.h"
+#include "nsIURI.h"
+#include "nsIURIMutator.h"
+#include "nsUnicharUtils.h"
+#include "nsURLHelper.h"
+
+namespace {
+
+mozilla::StaticRefPtr<mozilla::URLQueryStringStripper> gQueryStringStripper;
+
+static const char kQueryStrippingEnabledPref[] =
+ "privacy.query_stripping.enabled";
+static const char kQueryStrippingEnabledPBMPref[] =
+ "privacy.query_stripping.enabled.pbmode";
+static const char kQueryStrippingOnShareEnabledPref[] =
+ "privacy.query_stripping.strip_on_share.enabled";
+
+} // namespace
+
+namespace mozilla {
+
+NS_IMPL_ISUPPORTS(URLQueryStringStripper, nsIObserver,
+ nsIURLQueryStringStripper, nsIURLQueryStrippingListObserver)
+
+// static
+already_AddRefed<URLQueryStringStripper>
+URLQueryStringStripper::GetSingleton() {
+ if (!gQueryStringStripper) {
+ gQueryStringStripper = new URLQueryStringStripper();
+ // Check initial pref state and enable service. We can pass nullptr, because
+ // OnPrefChange doesn't rely on the args.
+ URLQueryStringStripper::OnPrefChange(nullptr, nullptr);
+
+ RunOnShutdown(
+ [&] {
+ DebugOnly<nsresult> rv = gQueryStringStripper->Shutdown();
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "URLQueryStringStripper::Shutdown failed");
+ gQueryStringStripper = nullptr;
+ },
+ ShutdownPhase::XPCOMShutdown);
+ }
+
+ return do_AddRef(gQueryStringStripper);
+}
+
+URLQueryStringStripper::URLQueryStringStripper() {
+ mIsInitialized = false;
+
+ nsresult rv = Preferences::RegisterCallback(
+ &URLQueryStringStripper::OnPrefChange, kQueryStrippingEnabledPBMPref);
+ NS_ENSURE_SUCCESS_VOID(rv);
+
+ rv = Preferences::RegisterCallback(&URLQueryStringStripper::OnPrefChange,
+ kQueryStrippingEnabledPref);
+
+ rv = Preferences::RegisterCallback(&URLQueryStringStripper::OnPrefChange,
+ kQueryStrippingOnShareEnabledPref);
+ NS_ENSURE_SUCCESS_VOID(rv);
+}
+
+NS_IMETHODIMP
+URLQueryStringStripper::StripForCopyOrShare(nsIURI* aURI,
+ nsIURI** strippedURI) {
+ if (!StaticPrefs::privacy_query_stripping_strip_on_share_enabled()) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+ uint32_t numStripped;
+ return StripQueryString(aURI, strippedURI, &numStripped);
+}
+
+NS_IMETHODIMP
+URLQueryStringStripper::Strip(nsIURI* aURI, bool aIsPBM, nsIURI** aOutput,
+ uint32_t* aStripCount) {
+ NS_ENSURE_ARG_POINTER(aURI);
+ NS_ENSURE_ARG_POINTER(aOutput);
+ NS_ENSURE_ARG_POINTER(aStripCount);
+
+ *aStripCount = 0;
+
+ if (aIsPBM) {
+ if (!StaticPrefs::privacy_query_stripping_enabled_pbmode()) {
+ return NS_OK;
+ }
+ } else {
+ if (!StaticPrefs::privacy_query_stripping_enabled()) {
+ return NS_OK;
+ }
+ }
+
+ if (CheckAllowList(aURI)) {
+ return NS_OK;
+ }
+
+ return StripQueryString(aURI, aOutput, aStripCount);
+}
+
+// static
+void URLQueryStringStripper::OnPrefChange(const char* aPref, void* aData) {
+ MOZ_ASSERT(gQueryStringStripper);
+
+ bool prefEnablesComponent =
+ StaticPrefs::privacy_query_stripping_enabled() ||
+ StaticPrefs::privacy_query_stripping_enabled_pbmode() ||
+ StaticPrefs::privacy_query_stripping_strip_on_share_enabled();
+
+ nsresult rv;
+ if (prefEnablesComponent) {
+ rv = gQueryStringStripper->Init();
+ } else {
+ rv = gQueryStringStripper->Shutdown();
+ }
+ NS_ENSURE_SUCCESS_VOID(rv);
+}
+
+nsresult URLQueryStringStripper::Init() {
+ if (mIsInitialized) {
+ return NS_OK;
+ }
+ mIsInitialized = true;
+
+ mListService = do_GetService("@mozilla.org/query-stripping-list-service;1");
+ NS_ENSURE_TRUE(mListService, NS_ERROR_FAILURE);
+
+ return mListService->RegisterAndRunObserver(gQueryStringStripper);
+}
+
+nsresult URLQueryStringStripper::Shutdown() {
+ if (!mIsInitialized) {
+ return NS_OK;
+ }
+ mIsInitialized = false;
+
+ mList.Clear();
+ mAllowList.Clear();
+
+ MOZ_ASSERT(mListService);
+ mListService = do_GetService("@mozilla.org/query-stripping-list-service;1");
+
+ mListService->UnregisterObserver(this);
+ mListService = nullptr;
+
+ return NS_OK;
+}
+
+nsresult URLQueryStringStripper::StripQueryString(nsIURI* aURI,
+ nsIURI** aOutput,
+ uint32_t* aStripCount) {
+ NS_ENSURE_ARG_POINTER(aURI);
+ NS_ENSURE_ARG_POINTER(aOutput);
+ NS_ENSURE_ARG_POINTER(aStripCount);
+
+ *aStripCount = 0;
+
+ nsCOMPtr<nsIURI> uri(aURI);
+
+ nsAutoCString query;
+ nsresult rv = aURI->GetQuery(query);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // We don't need to do anything if there is no query string.
+ if (query.IsEmpty()) {
+ return NS_OK;
+ }
+
+ URLParams params;
+
+ URLParams::Parse(query, [&](nsString&& name, nsString&& value) {
+ nsAutoString lowerCaseName;
+
+ ToLowerCase(name, lowerCaseName);
+
+ if (mList.Contains(lowerCaseName)) {
+ *aStripCount += 1;
+
+ // Count how often a specific query param is stripped. For privacy reasons
+ // this will only count query params listed in the Histogram definition.
+ // Calls for any other query params will be discarded.
+ nsAutoCString telemetryLabel("param_");
+ AppendUTF16toUTF8(lowerCaseName, telemetryLabel);
+ Telemetry::AccumulateCategorical(
+ Telemetry::QUERY_STRIPPING_COUNT_BY_PARAM, telemetryLabel);
+
+ return true;
+ }
+
+ params.Append(name, value);
+ return true;
+ });
+
+ // Return if there is no parameter has been stripped.
+ if (!*aStripCount) {
+ return NS_OK;
+ }
+
+ nsAutoString newQuery;
+ params.Serialize(newQuery, false);
+
+ Unused << NS_MutateURI(uri)
+ .SetQuery(NS_ConvertUTF16toUTF8(newQuery))
+ .Finalize(aOutput);
+
+ return NS_OK;
+}
+
+bool URLQueryStringStripper::CheckAllowList(nsIURI* aURI) {
+ MOZ_ASSERT(aURI);
+
+ // Get the site(eTLD+1) from the URI.
+ nsAutoCString baseDomain;
+ nsresult rv =
+ nsEffectiveTLDService::GetInstance()->GetBaseDomain(aURI, 0, baseDomain);
+ if (rv == NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS) {
+ return false;
+ }
+ NS_ENSURE_SUCCESS(rv, false);
+
+ return mAllowList.Contains(baseDomain);
+}
+
+void URLQueryStringStripper::PopulateStripList(const nsAString& aList) {
+ mList.Clear();
+
+ for (const nsAString& item : aList.Split(' ')) {
+ mList.Insert(item);
+ }
+}
+
+void URLQueryStringStripper::PopulateAllowList(const nsACString& aList) {
+ mAllowList.Clear();
+
+ for (const nsACString& item : aList.Split(',')) {
+ mAllowList.Insert(item);
+ }
+}
+
+NS_IMETHODIMP
+URLQueryStringStripper::OnQueryStrippingListUpdate(
+ const nsAString& aStripList, const nsACString& aAllowList) {
+ PopulateStripList(aStripList);
+ PopulateAllowList(aAllowList);
+ return NS_OK;
+}
+
+// static
+NS_IMETHODIMP
+URLQueryStringStripper::TestGetStripList(nsACString& aStripList) {
+ aStripList.Truncate();
+
+ StringJoinAppend(aStripList, " "_ns, mList,
+ [](auto& aResult, const auto& aValue) {
+ aResult.Append(NS_ConvertUTF16toUTF8(aValue));
+ });
+ return NS_OK;
+}
+
+/* nsIObserver */
+NS_IMETHODIMP
+URLQueryStringStripper::Observe(nsISupports*, const char* aTopic,
+ const char16_t*) {
+ // Since this class is created at profile-after-change by the Category
+ // Manager, it's expected to implement nsIObserver; however, we have nothing
+ // interesting to do here.
+ MOZ_ASSERT(strcmp(aTopic, "profile-after-change") == 0);
+
+ return NS_OK;
+}
+
+} // namespace mozilla
diff --git a/toolkit/components/antitracking/URLQueryStringStripper.h b/toolkit/components/antitracking/URLQueryStringStripper.h
new file mode 100644
index 0000000000..c2da0023c9
--- /dev/null
+++ b/toolkit/components/antitracking/URLQueryStringStripper.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_URLQueryStringStripper_h
+#define mozilla_URLQueryStringStripper_h
+
+#include "nsIURLQueryStringStripper.h"
+#include "nsIURLQueryStrippingListService.h"
+#include "nsIObserver.h"
+
+#include "nsStringFwd.h"
+#include "nsTHashSet.h"
+
+class nsIURI;
+
+namespace mozilla {
+
+class URLQueryStringStripper final : public nsIObserver,
+ public nsIURLQueryStringStripper,
+ public nsIURLQueryStrippingListObserver {
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIOBSERVER
+ NS_DECL_NSIURLQUERYSTRIPPINGLISTOBSERVER
+
+ NS_DECL_NSIURLQUERYSTRINGSTRIPPER
+
+ public:
+ static already_AddRefed<URLQueryStringStripper> GetSingleton();
+
+ private:
+ URLQueryStringStripper();
+ ~URLQueryStringStripper() = default;
+
+ static void OnPrefChange(const char* aPref, void* aData);
+
+ [[nodiscard]] nsresult Init();
+ [[nodiscard]] nsresult Shutdown();
+
+ [[nodiscard]] nsresult StripQueryString(nsIURI* aURI, nsIURI** aOutput,
+ uint32_t* aStripCount);
+
+ bool CheckAllowList(nsIURI* aURI);
+
+ void PopulateStripList(const nsAString& aList);
+ void PopulateAllowList(const nsACString& aList);
+
+ nsTHashSet<nsString> mList;
+ nsTHashSet<nsCString> mAllowList;
+ nsCOMPtr<nsIURLQueryStrippingListService> mListService;
+ bool mIsInitialized;
+};
+
+} // namespace mozilla
+
+#endif // mozilla_URLQueryStringStripper_h
diff --git a/toolkit/components/antitracking/URLQueryStrippingListService.sys.mjs b/toolkit/components/antitracking/URLQueryStrippingListService.sys.mjs
new file mode 100644
index 0000000000..e9be0a3ac4
--- /dev/null
+++ b/toolkit/components/antitracking/URLQueryStrippingListService.sys.mjs
@@ -0,0 +1,307 @@
+/* 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/. */
+
+import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ RemoteSettings: "resource://services-settings/remote-settings.sys.mjs",
+});
+
+const COLLECTION_NAME = "query-stripping";
+const SHARED_DATA_KEY = "URLQueryStripping";
+const PREF_STRIP_LIST_NAME = "privacy.query_stripping.strip_list";
+const PREF_ALLOW_LIST_NAME = "privacy.query_stripping.allow_list";
+const PREF_TESTING_ENABLED = "privacy.query_stripping.testing";
+
+XPCOMUtils.defineLazyGetter(lazy, "logger", () => {
+ return console.createInstance({
+ prefix: "URLQueryStrippingListService",
+ maxLogLevelPref: "privacy.query_stripping.listService.logLevel",
+ });
+});
+
+export class URLQueryStrippingListService {
+ classId = Components.ID("{afff16f0-3fd2-4153-9ccd-c6d9abd879e4}");
+ QueryInterface = ChromeUtils.generateQI(["nsIURLQueryStrippingListService"]);
+
+ #isInitialized = false;
+ #pendingInit = null;
+ #initResolver;
+
+ #rs;
+ #onSyncCallback;
+
+ constructor() {
+ lazy.logger.debug("constructor");
+ this.observers = new Set();
+ this.prefStripList = new Set();
+ this.prefAllowList = new Set();
+ this.remoteStripList = new Set();
+ this.remoteAllowList = new Set();
+ this.isParentProcess =
+ Services.appinfo.processType === Services.appinfo.PROCESS_TYPE_DEFAULT;
+ }
+
+ #onSync(event) {
+ lazy.logger.debug("onSync", event);
+ let {
+ data: { current },
+ } = event;
+ this._onRemoteSettingsUpdate(current);
+ }
+
+ async #init() {
+ // If there is already an init pending wait for it to complete.
+ if (this.#pendingInit) {
+ lazy.logger.debug("#init: Waiting for pending init");
+ await this.#pendingInit;
+ return;
+ }
+
+ if (this.#isInitialized) {
+ lazy.logger.debug("#init: Skip, already initialized");
+ return;
+ }
+ // Create a promise that resolves when init is complete. This allows us to
+ // handle incoming init calls while we're still initializing.
+ this.#pendingInit = new Promise(initResolve => {
+ this.#initResolver = initResolve;
+ });
+ this.#isInitialized = true;
+
+ lazy.logger.debug("#init: Run");
+
+ // We can only access the remote settings in the parent process. For content
+ // processes, we will use sharedData to sync the list to content processes.
+ if (this.isParentProcess) {
+ this.#rs = lazy.RemoteSettings(COLLECTION_NAME);
+
+ if (!this.#onSyncCallback) {
+ this.#onSyncCallback = this.#onSync.bind(this);
+ this.#rs.on("sync", this.#onSyncCallback);
+ }
+
+ // Get the initially available entries for remote settings.
+ let entries;
+ try {
+ entries = await this.#rs.get();
+ } catch (e) {}
+ this._onRemoteSettingsUpdate(entries || []);
+ } else {
+ // Register the message listener for the remote settings update from the
+ // sharedData.
+ Services.cpmm.sharedData.addEventListener("change", this);
+
+ // Get the remote settings data from the shared data.
+ let data = this._getListFromSharedData();
+
+ this._onRemoteSettingsUpdate(data);
+ }
+
+ // Get the list from pref.
+ this._onPrefUpdate(
+ PREF_STRIP_LIST_NAME,
+ Services.prefs.getStringPref(PREF_STRIP_LIST_NAME, "")
+ );
+ this._onPrefUpdate(
+ PREF_ALLOW_LIST_NAME,
+ Services.prefs.getStringPref(PREF_ALLOW_LIST_NAME, "")
+ );
+
+ Services.prefs.addObserver(PREF_STRIP_LIST_NAME, this);
+ Services.prefs.addObserver(PREF_ALLOW_LIST_NAME, this);
+
+ Services.obs.addObserver(this, "xpcom-shutdown");
+
+ this.#initResolver();
+ this.#pendingInit = null;
+ }
+
+ async #shutdown() {
+ // Ensure any pending init is done before shutdown.
+ if (this.#pendingInit) {
+ await this.#pendingInit;
+ }
+
+ // Already shut down.
+ if (!this.#isInitialized) {
+ return;
+ }
+ this.#isInitialized = false;
+
+ lazy.logger.debug("#shutdown");
+
+ // Unregister RemoteSettings listener (if it was registered).
+ if (this.#onSyncCallback) {
+ this.#rs.off("sync", this.#onSyncCallback);
+ this.#onSyncCallback = null;
+ }
+
+ Services.obs.removeObserver(this, "xpcom-shutdown");
+ Services.prefs.removeObserver(PREF_STRIP_LIST_NAME, this);
+ Services.prefs.removeObserver(PREF_ALLOW_LIST_NAME, this);
+ }
+
+ _onRemoteSettingsUpdate(entries) {
+ this.remoteStripList.clear();
+ this.remoteAllowList.clear();
+
+ for (let entry of entries) {
+ for (let item of entry.stripList) {
+ this.remoteStripList.add(item);
+ }
+
+ for (let item of entry.allowList) {
+ this.remoteAllowList.add(item);
+ }
+ }
+
+ // Because only the parent process will get the remote settings update, so
+ // we will sync the list to the shared data so that content processes can
+ // get the list.
+ if (this.isParentProcess) {
+ Services.ppmm.sharedData.set(SHARED_DATA_KEY, {
+ stripList: this.remoteStripList,
+ allowList: this.remoteAllowList,
+ });
+
+ if (Services.prefs.getBoolPref(PREF_TESTING_ENABLED, false)) {
+ Services.ppmm.sharedData.flush();
+ }
+ }
+
+ this._notifyObservers();
+ }
+
+ _onPrefUpdate(pref, value) {
+ switch (pref) {
+ case PREF_STRIP_LIST_NAME:
+ this.prefStripList = new Set(value ? value.split(" ") : []);
+ break;
+
+ case PREF_ALLOW_LIST_NAME:
+ this.prefAllowList = new Set(value ? value.split(",") : []);
+ break;
+
+ default:
+ console.error(`Unexpected pref name ${pref}`);
+ return;
+ }
+
+ this._notifyObservers();
+ }
+
+ _getListFromSharedData() {
+ let data = Services.cpmm.sharedData.get(SHARED_DATA_KEY);
+
+ return data ? [data] : [];
+ }
+
+ _notifyObservers(observer) {
+ let stripEntries = new Set([
+ ...this.prefStripList,
+ ...this.remoteStripList,
+ ]);
+ let allowEntries = new Set([
+ ...this.prefAllowList,
+ ...this.remoteAllowList,
+ ]);
+ let stripEntriesAsString = Array.from(stripEntries).join(" ").toLowerCase();
+ let allowEntriesAsString = Array.from(allowEntries).join(",").toLowerCase();
+
+ let observers = observer ? [observer] : this.observers;
+
+ if (observer || this.observers.size) {
+ lazy.logger.debug("_notifyObservers", {
+ observerCount: observers.length,
+ runObserverAfterRegister: observer != null,
+ stripEntriesAsString,
+ allowEntriesAsString,
+ });
+ }
+
+ for (let obs of observers) {
+ obs.onQueryStrippingListUpdate(
+ stripEntriesAsString,
+ allowEntriesAsString
+ );
+ }
+ }
+
+ async registerAndRunObserver(observer) {
+ lazy.logger.debug("registerAndRunObserver", {
+ isInitialized: this.#isInitialized,
+ pendingInit: this.#pendingInit,
+ });
+
+ await this.#init();
+ this.observers.add(observer);
+ this._notifyObservers(observer);
+ }
+
+ async unregisterObserver(observer) {
+ this.observers.delete(observer);
+
+ if (!this.observers.size) {
+ lazy.logger.debug("Last observer unregistered, shutting down...");
+ await this.#shutdown();
+ }
+ }
+
+ async clearLists() {
+ if (!this.isParentProcess) {
+ return;
+ }
+
+ // Ensure init.
+ await this.#init();
+
+ // Clear the lists of remote settings.
+ this._onRemoteSettingsUpdate([]);
+
+ // Clear the user pref for the strip list. The pref change observer will
+ // handle the rest of the work.
+ Services.prefs.clearUserPref(PREF_STRIP_LIST_NAME);
+ Services.prefs.clearUserPref(PREF_ALLOW_LIST_NAME);
+ }
+
+ observe(subject, topic, data) {
+ lazy.logger.debug("observe", { topic, data });
+ switch (topic) {
+ case "xpcom-shutdown":
+ this.#shutdown();
+ break;
+ case "nsPref:changed":
+ let prefValue = Services.prefs.getStringPref(data, "");
+ this._onPrefUpdate(data, prefValue);
+ break;
+ default:
+ console.error(`Unexpected event ${topic}`);
+ }
+ }
+
+ handleEvent(event) {
+ if (event.type != "change") {
+ return;
+ }
+
+ if (!event.changedKeys.includes(SHARED_DATA_KEY)) {
+ return;
+ }
+
+ let data = this._getListFromSharedData();
+ this._onRemoteSettingsUpdate(data);
+ this._notifyObservers();
+ }
+
+ async testWaitForInit() {
+ if (this.#pendingInit) {
+ await this.#pendingInit;
+ }
+
+ return this.#isInitialized;
+ }
+}
diff --git a/toolkit/components/antitracking/antitracking.manifest b/toolkit/components/antitracking/antitracking.manifest
new file mode 100644
index 0000000000..5eb37f9a3f
--- /dev/null
+++ b/toolkit/components/antitracking/antitracking.manifest
@@ -0,0 +1 @@
+category profile-after-change URLDecorationAnnotationsService @mozilla.org/tracking-url-decoration-service;1 process=main
diff --git a/toolkit/components/antitracking/components.conf b/toolkit/components/antitracking/components.conf
new file mode 100644
index 0000000000..6a584b493f
--- /dev/null
+++ b/toolkit/components/antitracking/components.conf
@@ -0,0 +1,64 @@
+# -*- 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/.
+
+Classes = [
+ {
+ 'cid': '{3c9c43b6-09eb-4ed2-9b87-e29f4221eef0}',
+ 'contract_ids': ['@mozilla.org/tracking-db-service;1'],
+ 'esModule': 'resource://gre/modules/TrackingDBService.sys.mjs',
+ 'constructor': 'TrackingDBService',
+ },
+ {
+ 'cid': '{5874af6d-5719-4e1b-b155-ef4eae7fcb32}',
+ 'contract_ids': ['@mozilla.org/tracking-url-decoration-service;1'],
+ 'esModule': 'resource://gre/modules/URLDecorationAnnotationsService.sys.mjs',
+ 'constructor': 'URLDecorationAnnotationsService',
+ 'processes': ProcessSelector.MAIN_PROCESS_ONLY,
+ },
+ {
+ 'cid': '{90d1fd17-2018-4e16-b73c-a04a26fa6dd4}',
+ 'contract_ids': ['@mozilla.org/purge-tracker-service;1'],
+ 'esModule': 'resource://gre/modules/PurgeTrackerService.sys.mjs',
+ 'constructor': 'PurgeTrackerService',
+ 'categories': {'profile-after-change': 'PurgeTrackerService'},
+ },
+ {
+ 'cid': '{ab94809d-33f0-4f28-af38-01efbd3baf22}',
+ 'contract_ids': ['@mozilla.org/partitioning/exception-list-service;1'],
+ 'esModule': 'resource://gre/modules/PartitioningExceptionListService.sys.mjs',
+ 'constructor': 'PartitioningExceptionListService',
+ },
+ {
+ 'cid': '{42906796-d16a-44a1-b518-0f108ab38eba}',
+ 'contract_ids': ['@mozilla.org/content-blocking-telemetry-service;1'],
+ 'singleton': True,
+ 'type': 'mozilla::ContentBlockingTelemetryService',
+ 'headers': ['mozilla/ContentBlockingTelemetryService.h'],
+ 'constructor': 'mozilla::ContentBlockingTelemetryService::GetSingleton',
+ 'categories': {'idle-daily': 'ContentBlockingTelemetryService'},
+ 'processes': ProcessSelector.MAIN_PROCESS_ONLY,
+ },
+ {
+ 'cid': '{afff16f0-3fd2-4153-9ccd-c6d9abd879e4}',
+ 'contract_ids': ['@mozilla.org/query-stripping-list-service;1'],
+ 'singleton': True,
+ 'esModule': 'resource://gre/modules/URLQueryStrippingListService.sys.mjs',
+ 'constructor': 'URLQueryStrippingListService',
+ },
+ {
+ 'name': 'URLQueryStringStripper',
+ 'cid': '{6b42a890-2624-4560-99c4-b25380e8cd77}',
+ 'interfaces': ['nsIURLQueryStringStripper'],
+ 'contract_ids': ['@mozilla.org/url-query-string-stripper;1'],
+ 'type': 'mozilla::URLQueryStringStripper',
+ 'headers': ['mozilla/URLQueryStringStripper.h'],
+ 'singleton': True,
+ 'constructor': 'mozilla::URLQueryStringStripper::GetSingleton',
+ 'categories': {
+ 'profile-after-change': 'URLQueryStringStripper',
+ }
+ },
+]
diff --git a/toolkit/components/antitracking/docs/cookie-purging/index.md b/toolkit/components/antitracking/docs/cookie-purging/index.md
new file mode 100644
index 0000000000..7b8c76cd39
--- /dev/null
+++ b/toolkit/components/antitracking/docs/cookie-purging/index.md
@@ -0,0 +1,217 @@
+# Cookie Purging
+
+“Cookie Purging” describes a technique that will periodically clear
+cookies and site data of known tracking domains without user interaction
+to protect against [bounce
+tracking](https://privacycg.github.io/nav-tracking-mitigations/#bounce-tracking).
+
+## Protection Background
+
+### What similar protections do other browsers have?
+
+**Safari** classifies sites as redirect trackers which directly or
+shortly after navigation redirect the user to other sites. Sites which
+receive user interaction are exempt from this. To detect bigger redirect
+networks, sites may also inherit redirect tracker
+[classification](https://privacycg.github.io/nav-tracking-mitigations/#mitigations-safari).
+If a site is classified as a redirect tracker, any site pointing to it
+will inherit this classification. Safari does not use tracker lists.
+
+When the source site is classified as a tracker, Safari will purge all
+storage, excluding cookies. Sites which receive user interaction within
+seven days of browser use are exempt. If the destination site's URL
+includes query parameters or URL fragments, Safari caps the lifetime of
+client-side set cookies of the destination site to 24 hours.
+
+**Brave** uses lists to classify redirect trackers. Recently, they have
+rolled out a new protection, [Unlinkable Bouncing](https://brave.com/privacy-updates/16-unlinkable-bouncing/),
+which limits first party storage lifetime. The underlying mechanism is
+called “first-party ephemeral storage”. If a user visits a known
+bounce-tracker which doesn’t have any pre-existing storage, the browser
+will create a temporary first-party storage bucket for the destination
+site. This temporary storage is cleared 30 seconds after the user closes
+the last tab of the site.
+
+**Chrome** and **Edge** currently do not implement any navigational
+tracking protections.
+
+### Is it standardized?
+
+At this time there are no standardized navigational tracking
+protections. The PrivacyCG has a [work item for Navigation-based Tracking Mitigations](https://privacycg.github.io/nav-tracking-mitigations/).
+Also see Apple’s proposal
+[here](https://github.com/privacycg/proposals/issues/6).
+
+### How does it fit into our vision of “Zero Privacy Leaks?”
+
+Existing tracking protections mechanisms focus mostly on third-party
+trackers. Redirect tracking can circumvent these mechanisms and utilize
+first-party storage for tracking. Cookie purging contributes to the
+“Zero Privacy Leaks” vision by mitigating this cross-site tracking
+vector.
+
+## Firefox Status
+
+Metabug: [Bug 1594226 - \[Meta\] Purging Tracking Cookies](https://bugzilla.mozilla.org/show_bug.cgi?id=1594226)
+
+### What is the ship state of this protection in Firefox?
+
+Shipped to Release in standard ETP mode
+
+### Is there outstanding work?
+
+The mechanism of storing user interaction as a permission via
+nsIPermissionManager has shown to be brittle and has led to users
+getting logged out of sites in the past. The concept of a permission
+doesn’t fully match that of a user interaction flag. Permissions may be
+separated between normal browsing and PBM (Bug
+[1692567](https://bugzilla.mozilla.org/show_bug.cgi?id=1692567)).
+They may also get purged when clearing site data (Bug
+[1675018](https://bugzilla.mozilla.org/show_bug.cgi?id=1675018)).
+
+A proposed solution to this is to create a dedicated data store for
+keeping track of user interaction. This could also enable tracking user
+interaction relative to browser usage time, rather than absolute time
+([Bug 1637146](https://bugzilla.mozilla.org/show_bug.cgi?id=1637146)).
+
+Important outstanding bugs:
+- [Bug 1637146 - Use use-time rather than absolute time when computing whether to purge cookies](https://bugzilla.mozilla.org/show_bug.cgi?id=1637146)
+
+### Existing Documentation
+
+- [https://developer.mozilla.org/en-US/docs/Web/Privacy/Redirect\_tracking\_protection](https://developer.mozilla.org/en-US/docs/Web/Privacy/Redirect_tracking_protection)
+
+- [PrivacyCG: Navigational-Tracking Mitigations](https://privacycg.github.io/nav-tracking-mitigations/)
+
+
+## Technical Information
+
+### Feature Prefs
+
+Cookie purging can be enabled or disabled by flipping the
+`privacy.purge_trackers.enabled` preference. Further, it will only run if
+the `network.cookie.cookieBehavior` pref is set to `4` or `5` ([bug 1643045](https://bugzilla.mozilla.org/show_bug.cgi?id=1643045) adds
+support for behaviors `1` and `3`).
+
+Different log levels can be set via the pref
+`privacy.purge_trackers.logging.level`.
+
+The time until user interaction permissions expire can be set to a lower
+amount of time using the `privacy.userInteraction.expiration` pref. Note
+that you will have to set this pref before visiting the sites you want
+to test on, it will not apply retroactively.
+
+### How does it work?
+
+Cookie purging periodically clears first-party storage of known
+trackers, which the user has not interacted with recently. It is
+implemented in the
+[PurgeTrackerService](https://searchfox.org/mozilla-central/rev/cf77e656ef36453e154bd45a38eea08b13d6a53e/toolkit/components/antitracking/PurgeTrackerService.jsm),
+which implements the
+[nsIPurgeTrackerService](https://searchfox.org/mozilla-central/rev/cf77e656ef36453e154bd45a38eea08b13d6a53e/toolkit/components/antitracking/nsIPurgeTrackerService.idl)
+IDL interface.
+
+#### What origins are cleared?
+
+An origin will be cleared if it fulfills the following conditions:
+
+1. It has stored cookies or accessed other site storage (e.g.
+ [localStorage](https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API),
+ [IndexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API),
+ or the [Cache API](https://developer.mozilla.org/en-US/docs/Web/API/CacheStorage))
+ within the last 72 hours. Since cookies are per-host, we will
+ clear both the http and https origin variants of a cookie host.
+
+2. The origin is [classified as a tracker](https://developer.mozilla.org/en-US/docs/Web/Privacy/Storage_Access_Policy#tracking_protection_explained)
+ in our Tracking Protection list.
+
+3. No origin with the same base domain (eTLD+1) has a user-interaction
+ permission.
+
+ - This permission is granted to an origin for 45 days once a user
+ interacts with a top-level document from that origin.
+ "Interacting" includes scrolling.
+
+ - Although this permission is stored on a per-origin level, we
+ will check whether any origin with the same base domain has
+ it, to avoid breaking sites with subdomains and a
+ corresponding cookie setup.
+
+#### What data is cleared?
+
+Firefox will clear the [following data](https://searchfox.org/mozilla-central/rev/cf77e656ef36453e154bd45a38eea08b13d6a53e/toolkit/components/antitracking/PurgeTrackerService.jsm#205-213):
+
+- Network cache and image cache
+
+- Cookies
+
+- DOM Quota Storage (localStorage, IndexedDB, ServiceWorkers, DOM
+ Cache, etc.)
+
+- DOM Push notifications
+
+- Reporting API Reports
+
+- Security Settings (i.e. HSTS)
+
+- EME Media Plugin Data
+
+- Plugin Data (e.g. Flash)
+
+- Media Devices
+
+- Storage Access permissions granted to the origin
+
+- HTTP Authentication Tokens
+
+- HTTP Authentication Cache
+
+**Note:** Even though we're clearing all of this data, we currently only
+flag origins for clearing when they use cookies or other site storage.
+
+Storage clearing ignores origin attributes. This means that storage will
+be cleared across
+[containers](https://wiki.mozilla.org/Security/Contextual_Identity_Project/Containers)
+and isolated storage (i.e. from [First-Party Isolation](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/cookies#first-party_isolation)).
+
+#### How frequently is data cleared?
+
+Firefox clears storage based on the firing of an internal event called
+[idle-daily](https://searchfox.org/mozilla-central/rev/cf77e656ef36453e154bd45a38eea08b13d6a53e/toolkit/components/antitracking/PurgeTrackerService.jsm#60,62,65),
+which is defined by the following conditions:
+
+- It will, at the earliest, fire 24h after the last idle-daily event
+ fired.
+
+- It will only fire if the user has been idle for at least 3min (for
+ 24-48h after the last idle-daily) or 1 min (for &gt;48h after the
+ last idle-daily).
+
+This means that there are at least 24 hours between each storage
+clearance, and storage will only be cleared when the browser is idle.
+When clearing cookies, we sort cookies by creation date and batch them
+into sets of 100 (controlled by the pref
+`privacy.purge_trackers.max_purge_count`) for performance reasons.
+
+#### Debugging
+
+For debugging purposes, it's easiest to trigger storage clearing by
+triggering the service directly via the [Browser Console command line](/devtools-user/browser_console/index.rst#browser_console_command_line).
+Note that this is different from the normal [Web Console](/devtools-user/web_console/index.rst)
+you might use to debug a website, and requires the
+`devtools.chrome.enabled` pref to be set to true to use it interactively.
+Once you've enabled the Browser Console you can trigger storage clearing
+by running the following command:
+
+``` javascript
+await Components.classes["@mozilla.org/purge-tracker-service;1"]
+.getService(Components.interfaces.nsIPurgeTrackerService)
+.purgeTrackingCookieJars()
+```
+
+<!---
+TODO: consider integrating
+[https://developer.mozilla.org/en-US/docs/Web/Privacy/Redirect\_tracking\_protection](https://developer.mozilla.org/en-US/docs/Web/Privacy/Redirect_tracking_protection)
+into firefox source docs. The article doesn’t really belong into MDN,
+because it’s very specific to Firefox.
+-->
diff --git a/toolkit/components/antitracking/docs/data-sanitization/index.md b/toolkit/components/antitracking/docs/data-sanitization/index.md
new file mode 100644
index 0000000000..412896c200
--- /dev/null
+++ b/toolkit/components/antitracking/docs/data-sanitization/index.md
@@ -0,0 +1,443 @@
+# Data Sanitization
+
+<!-- TODO: This doesn't strictly talk only about toolkit code. Consider splitting the article up and moving to relevant components -->
+
+Firefox has several Data Sanitization features. They allow users to
+clear preferences and website data. Clearing data is an essential
+feature for user privacy. There are two major privacy issues data
+clearing helps mitigate:
+
+1. Websites tracking the user via web-exposed APIs and storages. This
+ can be traditional storages, e.g. localStorage, or cookies.
+ However, sites can also use Supercookies, e.g. caches, to persist
+ storage in the browser.
+
+2. Attackers who have control over a computer can exfiltrate data from
+ Firefox, such as history, passwords, etc.
+
+## Protection Background
+
+### What similar protections do other browsers have?
+
+All major browsers implement data clearing features
+([Chrome](https://support.google.com/chrome/answer/2392709?hl=en&co=GENIE.Platform%3DDesktop&oco=0#zippy=),
+[Edge](https://support.microsoft.com/en-us/microsoft-edge/view-and-delete-browser-history-in-microsoft-edge-00cf7943-a9e1-975a-a33d-ac10ce454ca4),
+[Safari](https://support.apple.com/guide/safari/clear-your-browsing-history-sfri47acf5d6/mac),
+[Brave](https://support.brave.com/hc/en-us/articles/360054509991-How-do-I-clear-Cookies-and-Site-data-in-Brave-on-Android-)).
+They usually include a way for users to clear site data within a
+configurable time-span along with a list of data categories to be
+cleared.
+
+Chrome, Edge and Brave all share Chromium’s data clearing dialog with
+smaller adjustments. Notably, Brave extends it with a clear-on-shutdown
+mechanism similar to Firefox, while Chrome only supports clearing
+specifically site data on shutdown.
+
+Safari’s history clearing feature only allows users to specify a time
+span. It does not allow filtering by categories, but clears all website
+related data.
+
+All browsers allow fine grained control over website cookies and
+storages via the developer tools.
+
+### Is it standardized?
+
+This is a browser UX feature and is therefore not standardized. It is
+not part of the web platform.
+
+There is a standardized HTTP header sites can send to clear associated
+browser cache, cookies and storage:
+[Clear-Site-Data](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Clear-Site-Data).
+However, Firefox no longer allows sites to clear caches via the header
+since [Bug
+1671182](https://bugzilla.mozilla.org/show_bug.cgi?id=1671182).
+
+### How does it fit into our vision of “Zero Privacy Leaks?”
+
+Clearing site data protects users against various tracking techniques
+that rely on browser state to (re-)identify users. While Total Cookie
+Protection covers many cross-site tracking scenarios, clearing site data
+can additionally protect against first-party tracking and other tracking
+methods that bypass TCP such as [navigational
+tracking](https://privacycg.github.io/nav-tracking-mitigations/#intro).
+
+## Firefox Status
+
+### What is the ship state of this protection in Firefox?
+
+This long standing set of features is shipped in Release in default ETP
+mode. In Firefox 91 we introduced [Enhanced Cookie
+Clearing](https://blog.mozilla.org/security/2021/08/10/firefox-91-introduces-enhanced-cookie-clearing/)
+which makes use of TCP’s cookie jars. This feature only benefits users
+who have TCP enabled - in ETP strict mode or Private Browsing Mode.
+
+### Is there outstanding work?
+
+Since [Bug
+1422365](https://bugzilla.mozilla.org/show_bug.cgi?id=1422365) the
+ClearDataService provides a common interface to clear data of various
+storage implementations. However, we don’t have full coverage of all
+browser state yet. There are several smaller blind spots, most of which
+are listed in this [meta
+bug](https://bugzilla.mozilla.org/show_bug.cgi?id=1102808). There is
+also a long backlog of data sanitization bugs
+[here](https://bugzilla.mozilla.org/show_bug.cgi?id=1550317).
+
+From a user perspective it’s difficult to understand what kind of data
+is cleared from which UI. The category selection in the “Clear recent
+history” dialog is especially confusing.
+
+Data clearing can take a long time on bigger Firefox profiles. Since
+these operations mostly run on the main thread, this can lock up the UI
+making the browser unresponsive until the operation has completed.
+
+Generally it would be worth revisiting cleaner implementations in the
+ClearDataService and beyond to see where we can improve clearing
+performance.
+
+Slow data clearing is especially problematic on shutdown. If the
+sanitize-on-shutdown feature takes too long to clear storage, the parent
+process will be terminated, resulting in a shutdown crash. [Bug
+1756724](https://bugzilla.mozilla.org/show_bug.cgi?id=1756724)
+proposes a solution to this: We could show a progress dialog when
+clearing data. This way we can allow a longer shutdown phase, since the
+user is aware that we’re clearing data.
+
+Important outstanding bugs:
+
+- [Bug 1550317 - \[meta\] Broken data
+ sanitization](https://bugzilla.mozilla.org/show_bug.cgi?id=1550317)
+
+- [Bug 1102808 - \[meta\] Clear Recent History / Forget button
+ blind
+ spots](https://bugzilla.mozilla.org/show_bug.cgi?id=1102808)
+
+- [Bug 1756724 - Show a data clearing progress dialog when
+ sanitizing data at shutdown due to "delete cookies and site data
+ when Firefox is
+ closed"](https://bugzilla.mozilla.org/show_bug.cgi?id=1756724)
+
+### Existing Documentation
+<!-- TODO: link existing documentation, if any -->
+
+\-
+
+## Technical Information
+
+### Feature Prefs
+
+| Pref | Description |
+| ---- | ----------- |
+| places.forgetThisSite.clearByBaseDomain | Switches “Forget about this site” to clear for the whole base domain rather than just the host. |
+| privacy.sanitize.sanitizeOnShutdown | Whether to clear data on Firefox shutdown. |
+| privacy.clearOnShutdown.* | Categories of data to be cleared on shutdown. True = clear category. Data is only cleared if privacy.sanitize.sanitizeOnShutdown is enabled.|
+
+### How does it work?
+
+The following section lists user facing data sanitization features in
+Firefox, along with a brief description and a diagram how they tie into
+the main clearing logic in `nsIClearDataService`.
+
+#### Clear Data
+
+- Accessible via `about:preferences#privacy`
+
+- Clears site data and caches depending on user selection
+
+- Clears
+
+ - Cookies
+
+ - DOM storages
+
+ - HSTS
+
+ - EME
+
+ - Caches: CSS, Preflight, HSTS
+
+- Source
+
+ - [clearSiteData.xhtml](https://searchfox.org/mozilla-central/source/browser/components/preferences/dialogs/clearSiteData.xhtml)
+
+ - [clearSiteData.js](https://searchfox.org/mozilla-central/source/browser/components/preferences/dialogs/clearSiteData.js)
+
+ - [clearSiteData.css](https://searchfox.org/mozilla-central/source/browser/components/preferences/dialogs/clearSiteData.css)
+
+![image3](media/image3.png)
+
+![image1](media/image1.png)
+
+#### Clear Recent History
+
+- Accessible via hamburger menu =&gt; History =&gt; Clear Recent
+ history or `about:preferences#privacy` =&gt; History =&gt; Clear
+ History
+
+- Clears a configurable list of categories as [defined in
+ Sanitizer.jsm](https://searchfox.org/mozilla-central/rev/fbb1e8462ad82b0e76b5c13dd0d6280cfb69e68d/browser/modules/Sanitizer.jsm#356)
+
+- Can clear everything or a specific time range
+
+- Source
+
+ - [sanitize.xhtml](https://searchfox.org/mozilla-central/source/browser/base/content/sanitize.xhtml)
+
+ - [sanitizeDialog.js](https://searchfox.org/mozilla-central/source/browser/base/content/sanitizeDialog.js)
+
+![image4](media/image4.png)
+
+#### Forget About this Site
+
+- Accessible via hamburger menu =&gt; History =&gt; Contextmenu of an
+ item =&gt; Forget About This Site
+
+- Clears all data associated with the base domain of the selected site
+
+- \[With TCP\] Also clears data of any third-party sites embedded
+ under the top level base domain
+
+- The goal is to remove all traces of the associated site from Firefox
+
+- Clears
+ \[[flags](https://searchfox.org/mozilla-central/rev/fbb1e8462ad82b0e76b5c13dd0d6280cfb69e68d/toolkit/components/cleardata/nsIClearDataService.idl#302-307)\]
+
+ - History, session history, download history
+
+ - All caches
+
+ - Site data (cookies, dom storages)
+
+ - Encrypted Media Extensions (EME)
+
+ - Passwords (See [Bug
+ 702925](https://bugzilla.mozilla.org/show_bug.cgi?id=702925))
+
+ - Permissions
+
+ - Content preferences (e.g. page zoom level)
+
+ - Predictor network data
+
+ - Reports (Reporting API)
+
+ - Client-Auth-Remember flag, Certificate exceptions
+
+ - Does **not** clear bookmarks
+
+- Source
+
+ - [ForgetAboutSite.sys.mjs](https://searchfox.org/mozilla-central/source/toolkit/components/forgetaboutsite/ForgetAboutSite.sys.mjs)
+
+ - [nsIClearDataService flags
+ used](https://searchfox.org/mozilla-central/rev/fbb1e8462ad82b0e76b5c13dd0d6280cfb69e68d/toolkit/components/cleardata/nsIClearDataService.idl#302-307)
+
+![image6](media/image6.png)
+
+![image2](media/image2.png)
+
+#### Sanitize on Shutdown
+
+- Can be enabled via `about:preferences#privacy` =&gt; History: Firefox
+ will: Use custom settings for history =&gt; Check “Clear history
+ when Firefox closes”
+
+ - After [Bug
+ 1681493](https://bugzilla.mozilla.org/show_bug.cgi?id=1681493)
+ it can also be controlled via the checkbox “Delete cookies and
+ site data when Firefox is closed”
+
+- On shutdown of Firefox, will clear all data for the selected
+ categories. The list of categories is defined in
+ [Sanitizer.jsm](https://searchfox.org/mozilla-central/rev/fbb1e8462ad82b0e76b5c13dd0d6280cfb69e68d/browser/modules/Sanitizer.jsm#356)
+
+- Categories are the same as for the “Clear recent history” dialog
+
+- Exceptions
+
+ - Sites which have a “cookie” permission, set to
+ [ACCESS\_SESSION](https://searchfox.org/mozilla-central/rev/fbb1e8462ad82b0e76b5c13dd0d6280cfb69e68d/netwerk/cookie/nsICookiePermission.idl#28)
+ always get cleared, even if sanitize-on-shutdown is disabled
+
+ - Sites which have a “cookie” permission set to
+ [ACCESS\_ALLOW](https://searchfox.org/mozilla-central/rev/fbb1e8462ad82b0e76b5c13dd0d6280cfb69e68d/netwerk/cookie/nsICookiePermission.idl#19)
+ are exempt from data clearing
+
+ - Caveat: When “site settings” is selected in the categories to be
+ cleared, the Sanitizer will remove exception permissions too.
+ This results in the above exceptions being cleared.
+
+- Uses PrincipalsCollector to obtain a list of principals which have
+ site data associated with them
+
+ - [getAllPrincipals](https://searchfox.org/mozilla-central/rev/fbb1e8462ad82b0e76b5c13dd0d6280cfb69e68d/toolkit/components/cleardata/PrincipalsCollector.jsm#72)
+ queries the QuotaManager, the cookie service and the service
+ worker manager for principals
+
+- The list of principals obtained is checked for permission
+ exceptions. Principals which set a cookie
+ [ACCESS\_ALLOW](https://searchfox.org/mozilla-central/rev/fbb1e8462ad82b0e76b5c13dd0d6280cfb69e68d/netwerk/cookie/nsICookiePermission.idl#19)
+ permission are removed from the list.
+
+- Sanitizer.jsm [calls the
+ ClearDataService](https://searchfox.org/mozilla-central/rev/fbb1e8462ad82b0e76b5c13dd0d6280cfb69e68d/browser/modules/Sanitizer.jsm#1022,1027-1032)
+ to clear data for every principal from the filtered list
+
+- Source
+
+ - Most of the sanitize-on-shutdown logic is implemented in
+ [Sanitizer.jsm](https://searchfox.org/mozilla-central/rev/fbb1e8462ad82b0e76b5c13dd0d6280cfb69e68d/browser/modules/Sanitizer.jsm)
+
+ - The main entry point is
+ [sanitizeOnShutdown](https://searchfox.org/mozilla-central/rev/fbb1e8462ad82b0e76b5c13dd0d6280cfb69e68d/browser/modules/Sanitizer.jsm#790)
+
+ - [Parts of
+ sanitize-on-shutdown](https://searchfox.org/mozilla-central/rev/fbb1e8462ad82b0e76b5c13dd0d6280cfb69e68d/browser/modules/Sanitizer.jsm#904-911)
+ always have to run, even if the rest of the feature is
+ disabled, to support clearing storage of sites which have
+ “cookie” set to
+ [ACCESS\_SESSION](https://searchfox.org/mozilla-central/rev/fbb1e8462ad82b0e76b5c13dd0d6280cfb69e68d/netwerk/cookie/nsICookiePermission.idl#28)
+ (see exceptions above)
+
+#### Manage Cookies and Site Data
+
+- Accessible via `about:preferences#privacy` =&gt; Cookies and Site Data
+ =&gt; Manage Data
+
+- Clears
+ \[[flags](https://searchfox.org/mozilla-central/rev/fbb1e8462ad82b0e76b5c13dd0d6280cfb69e68d/browser/modules/SiteDataManager.jsm#499,510-514)\]
+
+ - Cookies
+
+ - DOM storages
+
+ - EME
+
+ - Caches: CSS, Preflight, HSTS
+
+- Lists site cookies and storage grouped by base domain.
+
+- Clearing data on a more granular (host or origin) level is not
+ possible. This is a deliberate decision to make this UI more
+ thorough in cleaning and easier to understand. If users need very
+ granular data management capabilities, they can install an addon
+ or use the devtools.
+
+- Allows users to clear storage for specific sites, or all sites
+
+- \[With TCP\] Also clears data of any third-party sites embedded
+ under the top level base domain
+
+- Collects list of sites via
+ [SiteDataManager.getSites](https://searchfox.org/mozilla-central/rev/fbb1e8462ad82b0e76b5c13dd0d6280cfb69e68d/browser/modules/SiteDataManager.jsm#366)
+
+- Before removal, prompts via SiteDataManger.promptSiteDataRemoval
+
+- On removal calls SiteDataManager.removeAll() if all sites have been
+ selected or SiteDataManager.remove() passing a list of sites to be
+ removed.
+
+- Source
+
+ - [siteDataSettings.xhtml](https://searchfox.org/mozilla-central/rev/fbb1e8462ad82b0e76b5c13dd0d6280cfb69e68d/browser/components/preferences/dialogs/siteDataSettings.xhtml)
+
+ - [siteDataSettings.js](https://searchfox.org/mozilla-central/rev/fbb1e8462ad82b0e76b5c13dd0d6280cfb69e68d/browser/components/preferences/dialogs/siteDataSettings.js)
+
+#### Clear Cookies and Site Data
+
+- Accessible via the identity panel (click on lock icon in the URL
+ bar)
+
+- Clears
+ \[[flags](https://searchfox.org/mozilla-central/rev/fbb1e8462ad82b0e76b5c13dd0d6280cfb69e68d/browser/modules/SiteDataManager.jsm#499,510-514)\]
+
+ - Cookies
+
+ - DOM storages
+
+ - EME
+
+ - Caches: CSS, Preflight, HSTS
+
+- Button handler method:
+ [clearSiteData](https://searchfox.org/mozilla-central/rev/fbb1e8462ad82b0e76b5c13dd0d6280cfb69e68d/browser/base/content/browser-siteIdentity.js#364-385)
+
+- Calls SiteDataManager.remove() with the base domain of the currently
+ selected tab
+
+- The button is only shown if a site has any cookies or quota storage.
+ This is checked
+ [here](https://searchfox.org/mozilla-central/rev/3269d4c928ef0d8310c2f57634e9b6057aa636e9/browser/base/content/browser-siteIdentity.js#923).
+
+- Source
+
+ - [identityPanel.inc.xhtml](https://searchfox.org/mozilla-central/rev/3269d4c928ef0d8310c2f57634e9b6057aa636e9/browser/components/controlcenter/content/identityPanel.inc.xhtml#97)
+
+ - [browser-siteIdentity.js](https://searchfox.org/mozilla-central/rev/3269d4c928ef0d8310c2f57634e9b6057aa636e9/browser/base/content/browser-siteIdentity.js#364)
+
+![image7](media/image7.png)
+
+![image5](media/image5.png)
+
+A broad overview of the different data clearing features accessible via
+about:preferences#privacy.
+
+The user can clear data on demand or choose to clear data on shutdown.
+For the latter the user may make exceptions for specific origins not to
+be cleared or to be always cleared on shutdown.
+
+#### ClearDataService
+
+This service serves as a unified module to hold all data clearing logic
+in Firefox / Gecko. Callers can use the
+[nsIClearDataService](https://searchfox.org/mozilla-central/rev/cf77e656ef36453e154bd45a38eea08b13d6a53e/toolkit/components/cleardata/nsIClearDataService.idl)
+interface to clear data. From JS the service is accessible via
+Services.clearData.
+
+To specify which state to clear pass a combination of
+[flags](https://searchfox.org/mozilla-central/rev/cf77e656ef36453e154bd45a38eea08b13d6a53e/toolkit/components/cleardata/nsIClearDataService.idl#161-308)
+into aFlags.
+
+Every category of browser state should have its own cleaner
+implementation which exposes the following methods to the
+ClearDataService:
+
+- **deleteAll**: Deletes all data owned by the cleaner
+
+- **deleteByPrincipal**: Deletes data associated with a specific
+ principal.
+
+- **deleteByBaseDomain**: Deletes all entries which are associated
+ with the given base domain. This includes data partitioned by
+ Total Cookie Protection.
+
+- **deleteByHost**: Clears data associated with a host. Does not clear
+ partitioned data.
+
+- **deleteByRange**: Clear data which matches a given time-range.
+
+- **deleteByLocalFiles**: Delete data held for local files and other
+ hostless origins.
+
+- **deleteByOriginAttributes**: Clear entries which match an
+ [OriginAttributesPattern](https://searchfox.org/mozilla-central/rev/cf77e656ef36453e154bd45a38eea08b13d6a53e/caps/OriginAttributes.h#153).
+
+Some of these methods are optional. See [comment
+here](https://searchfox.org/mozilla-central/rev/cf77e656ef36453e154bd45a38eea08b13d6a53e/toolkit/components/cleardata/ClearDataService.jsm#85-105).
+If a cleaner does not support a specific method, we will usually try to
+fall back to deleteAll. For privacy reasons we try to over-clear storage
+rather than under-clear it or not clear it at all because we can’t
+target individual entries.
+
+![image8](media/image8.png)
+
+Overview of the most important cleaning methods of the ClearDataService
+called by other Firefox / Gecko components. deleteDataFromPrincipal is
+called programmatically, while user exposed data clearing features clear
+by base domain, host or all data.
+
+<!--
+TODO: For firefox-source-docs, import JSdoc for relevant modules
+[like
+so](https://searchfox.org/mozilla-central/rev/fbb1e8462ad82b0e76b5c13dd0d6280cfb69e68d/toolkit/components/prompts/docs/nsIPromptService-reference.rst#9)
+-->
diff --git a/toolkit/components/antitracking/docs/data-sanitization/media/image1.png b/toolkit/components/antitracking/docs/data-sanitization/media/image1.png
new file mode 100644
index 0000000000..c9029753a3
--- /dev/null
+++ b/toolkit/components/antitracking/docs/data-sanitization/media/image1.png
Binary files differ
diff --git a/toolkit/components/antitracking/docs/data-sanitization/media/image2.png b/toolkit/components/antitracking/docs/data-sanitization/media/image2.png
new file mode 100644
index 0000000000..451b297b7e
--- /dev/null
+++ b/toolkit/components/antitracking/docs/data-sanitization/media/image2.png
Binary files differ
diff --git a/toolkit/components/antitracking/docs/data-sanitization/media/image3.png b/toolkit/components/antitracking/docs/data-sanitization/media/image3.png
new file mode 100644
index 0000000000..8b37d5f160
--- /dev/null
+++ b/toolkit/components/antitracking/docs/data-sanitization/media/image3.png
Binary files differ
diff --git a/toolkit/components/antitracking/docs/data-sanitization/media/image4.png b/toolkit/components/antitracking/docs/data-sanitization/media/image4.png
new file mode 100644
index 0000000000..566b0d3b36
--- /dev/null
+++ b/toolkit/components/antitracking/docs/data-sanitization/media/image4.png
Binary files differ
diff --git a/toolkit/components/antitracking/docs/data-sanitization/media/image5.png b/toolkit/components/antitracking/docs/data-sanitization/media/image5.png
new file mode 100644
index 0000000000..21f34894fa
--- /dev/null
+++ b/toolkit/components/antitracking/docs/data-sanitization/media/image5.png
Binary files differ
diff --git a/toolkit/components/antitracking/docs/data-sanitization/media/image6.png b/toolkit/components/antitracking/docs/data-sanitization/media/image6.png
new file mode 100644
index 0000000000..719227d2ca
--- /dev/null
+++ b/toolkit/components/antitracking/docs/data-sanitization/media/image6.png
Binary files differ
diff --git a/toolkit/components/antitracking/docs/data-sanitization/media/image7.png b/toolkit/components/antitracking/docs/data-sanitization/media/image7.png
new file mode 100644
index 0000000000..9259380188
--- /dev/null
+++ b/toolkit/components/antitracking/docs/data-sanitization/media/image7.png
Binary files differ
diff --git a/toolkit/components/antitracking/docs/data-sanitization/media/image8.png b/toolkit/components/antitracking/docs/data-sanitization/media/image8.png
new file mode 100644
index 0000000000..469aa398cd
--- /dev/null
+++ b/toolkit/components/antitracking/docs/data-sanitization/media/image8.png
Binary files differ
diff --git a/toolkit/components/antitracking/docs/index.rst b/toolkit/components/antitracking/docs/index.rst
new file mode 100644
index 0000000000..45d888b989
--- /dev/null
+++ b/toolkit/components/antitracking/docs/index.rst
@@ -0,0 +1,12 @@
+=================================
+Anti-Tracking
+=================================
+
+This page is an overview of various anti-tracking components.
+
+.. toctree::
+ :maxdepth: 1
+
+ Cookie Purging <cookie-purging/index.md>
+ Data Sanitization <data-sanitization/index.md>
+ Query Stripping <query-stripping/index.md>
diff --git a/toolkit/components/antitracking/docs/query-stripping/index.md b/toolkit/components/antitracking/docs/query-stripping/index.md
new file mode 100644
index 0000000000..e49d8513ba
--- /dev/null
+++ b/toolkit/components/antitracking/docs/query-stripping/index.md
@@ -0,0 +1,153 @@
+# Query Parameter Stripping
+
+To combat [Navigational
+Tracking](https://privacycg.github.io/nav-tracking-mitigations/#navigational-tracking)
+through [link
+decoration](https://privacycg.github.io/nav-tracking-mitigations/#link-decoration),
+Firefox can strip known tracking query parameters from URLs before the
+user navigates to them.
+
+## Protection Background
+
+### What similar protections do other browsers have?
+
+Brave also has a list-based query parameter stripping mechanism. A list
+of query parameters stripped can be found
+[here](https://github.com/brave/brave-core/blob/5fcad3e35bac6fea795941fd8189a59d79d488bc/browser/net/brave_site_hacks_network_delegate_helper.cc#L29-L67).
+Brave also has a strip-on-copy feature which allows users to copy a
+stripped version of the current URL.
+
+### Is it standardized?
+
+At this time there are no standardized navigational tracking
+protections. The PrivacyCG has a [work item for Navigation-based
+Tracking
+Mitigations](https://privacycg.github.io/nav-tracking-mitigations/).
+Also see Apple’s proposal
+[here](https://github.com/privacycg/proposals/issues/6).
+
+### How does it fit into our vision of “Zero Privacy Leaks?”
+
+Existing tracking protections mechanisms in Firefox, such as ETP and TCP
+focus mostly on third-party trackers. Redirect tracking can circumvent
+these mechanisms by passing identifiers through link decoration and
+first-party storage. Query parameter stripping contributes to the “Zero
+Privacy Leaks” vision by mitigating this cross-site tracking vector.
+
+## Firefox Status
+
+Metabug: [Bug 1706602 - \[meta\] Implement URL query string stripping
+prototype](https://bugzilla.mozilla.org/show_bug.cgi?id=1706602)
+
+### What is the ship state of this protection in Firefox?
+
+Query stripping is enabled in release in ETP strict with an initial list
+of query params:
+
+- mc\_eid
+
+- oly\_anon\_id
+
+- oly\_enc\_id
+
+- \_\_s
+
+- vero\_id
+
+- \_hsenc
+
+- mkt\_tok
+
+- fbclid
+
+It is enabled in Nightly by default in all modes with an extended
+strip-list. You can find the current list of parameters that are
+stripped
+[here](https://firefox.settings.services.mozilla.com/v1/buckets/main/collections/query-stripping/records).
+Note that some records have a *filter\_expression* that limits where
+they apply.
+
+### Is there outstanding work?
+
+After our initial release on ETP strict, we are considering to ship the
+feature to Private Browsing Mode and possibly also to enable it by default
+in release in the future.
+
+Other possible improvements:
+
+- Extend the list of query parameters stripped, in accordance with our policy.
+
+- Extend the protection to cover different kinds of link decoration, beyond just query parameters.
+
+- Ability to identify and strip hashed link decoration fields
+
+- Strip query params for urls shared / copied out from the browser
+
+Outstanding bugs:
+
+- See dependencies of [Bug 1706602 - \[meta\] Implement URL query
+ string stripping
+ prototype](https://bugzilla.mozilla.org/show_bug.cgi?id=1706602)
+
+### Existing Documentation
+
+- [Anti-Tracking Policy: Navigational cross-site
+ tracking](https://wiki.mozilla.org/Security/Anti_tracking_policy#2._Navigational_cross-site_tracking)
+
+## Technical Information
+
+### Feature Prefs
+
+| Pref | Description |
+| ---- | ----------- |
+| privacy.query_stripping.enabled | Enable / disable the feature in normal browsing. |
+| privacy.query_stripping.enabled.pbmode | Enable / disable the feature in private browsing. |
+| privacy.query_stripping.allow_list | Comma separated list of sites (without scheme) which should not have their query parameters stripped. |
+| privacy.query_stripping.redirect | Whether to perform stripping for redirects. |
+| privacy.query_stripping.strip_list | List of space delimited query parameters to be stripped. |
+
+### How does it work?
+
+![Architecture](overview.png "Overview")
+
+[**UrlQueryStrippingListService**](https://searchfox.org/mozilla-central/rev/3269d4c928ef0d8310c2f57634e9b6057aa636e9/toolkit/components/antitracking/URLQueryStrippingListService.jsm)
+
+- Collects list of query parameters to be stripped and allow-list from
+ the *privacy.query\_stripping.strip\_list/allow\_list* preference
+ and the *query-stripping* Remote Settings collection
+
+- Lists from the two sources are
+ [concatenated](https://searchfox.org/mozilla-central/rev/3269d4c928ef0d8310c2f57634e9b6057aa636e9/toolkit/components/antitracking/URLQueryStrippingListService.jsm#150-151)
+
+- Lists are distributed via [observer
+ notification](https://searchfox.org/mozilla-central/rev/3269d4c928ef0d8310c2f57634e9b6057aa636e9/toolkit/components/antitracking/URLQueryStrippingListService.jsm#158-161)
+ via the
+ [nsIUrlQueryStrippingListService](https://searchfox.org/mozilla-central/rev/3269d4c928ef0d8310c2f57634e9b6057aa636e9/toolkit/components/antitracking/nsIURLQueryStrippingListService.idl#25).
+ [onQueryStrippingListUpdate](https://searchfox.org/mozilla-central/rev/3269d4c928ef0d8310c2f57634e9b6057aa636e9/toolkit/components/antitracking/nsIURLQueryStrippingListService.idl#25)
+ is called initially on registration and whenever the preferences
+ or the Remote Settings collection updates.
+
+[**URLQueryStringStripper**](https://searchfox.org/mozilla-central/rev/3269d4c928ef0d8310c2f57634e9b6057aa636e9/toolkit/components/antitracking/URLQueryStringStripper.h)
+
+- Only subscriber of the
+ [UrlQueryStrippingListService](https://searchfox.org/mozilla-central/rev/3269d4c928ef0d8310c2f57634e9b6057aa636e9/toolkit/components/antitracking/URLQueryStrippingListService.jsm)
+
+- Holds [hash set
+ representations](https://searchfox.org/mozilla-central/rev/3269d4c928ef0d8310c2f57634e9b6057aa636e9/toolkit/components/antitracking/URLQueryStringStripper.h#56-57)
+ of the strip- and allow-list.
+
+- [URLQueryStringStripper::Strip](https://searchfox.org/mozilla-central/rev/3269d4c928ef0d8310c2f57634e9b6057aa636e9/toolkit/components/antitracking/URLQueryStringStripper.cpp#45):
+ takes a nsIURI as input and strips any query parameters that are
+ on the strip-list. If the given URI matches a site on the
+ allow-list no query parameters are stripped.
+
+**Consumers**
+
+- [nsDocShell::DoURILoad](https://searchfox.org/mozilla-central/rev/3269d4c928ef0d8310c2f57634e9b6057aa636e9/docshell/base/nsDocShell.cpp#10569):
+ Strips in the content, before creating the channel.
+
+- [BrowsingContext::LoadURI](https://searchfox.org/mozilla-central/rev/3269d4c928ef0d8310c2f57634e9b6057aa636e9/docshell/base/BrowsingContext.cpp#2019):
+ Strips before loading the URI in the parent.
+
+- [nsHttpChannel::AsyncProcessRedirection](https://searchfox.org/mozilla-central/rev/3269d4c928ef0d8310c2f57634e9b6057aa636e9/netwerk/protocol/http/nsHttpChannel.cpp#5154):
+ Strips query parameters for HTTP redirects (e.g. 301).
diff --git a/toolkit/components/antitracking/docs/query-stripping/overview.png b/toolkit/components/antitracking/docs/query-stripping/overview.png
new file mode 100644
index 0000000000..63cf495202
--- /dev/null
+++ b/toolkit/components/antitracking/docs/query-stripping/overview.png
Binary files differ
diff --git a/toolkit/components/antitracking/moz.build b/toolkit/components/antitracking/moz.build
new file mode 100644
index 0000000000..94073953bf
--- /dev/null
+++ b/toolkit/components/antitracking/moz.build
@@ -0,0 +1,97 @@
+# -*- 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", "Privacy: Anti-Tracking")
+
+XPIDL_SOURCES += [
+ "nsIContentBlockingAllowList.idl",
+ "nsIPartitioningExceptionListService.idl",
+ "nsIPurgeTrackerService.idl",
+ "nsITrackingDBService.idl",
+ "nsIURLDecorationAnnotationsService.idl",
+ "nsIURLQueryStringStripper.idl",
+ "nsIURLQueryStrippingListService.idl",
+]
+
+XPIDL_MODULE = "toolkit_antitracking"
+
+EXTRA_COMPONENTS += [
+ "antitracking.manifest",
+]
+
+EXTRA_JS_MODULES += [
+ "ContentBlockingAllowList.sys.mjs",
+ "PartitioningExceptionListService.sys.mjs",
+ "PurgeTrackerService.sys.mjs",
+ "TrackingDBService.sys.mjs",
+ "URLDecorationAnnotationsService.sys.mjs",
+ "URLQueryStrippingListService.sys.mjs",
+]
+
+XPCOM_MANIFESTS += [
+ "components.conf",
+]
+
+EXPORTS.mozilla = [
+ "AntiTrackingIPCUtils.h",
+ "AntiTrackingRedirectHeuristic.h",
+ "AntiTrackingUtils.h",
+ "ContentBlockingAllowList.h",
+ "ContentBlockingLog.h",
+ "ContentBlockingNotifier.h",
+ "ContentBlockingTelemetryService.h",
+ "ContentBlockingUserInteraction.h",
+ "DynamicFpiRedirectHeuristic.h",
+ "PartitioningExceptionList.h",
+ "RejectForeignAllowList.h",
+ "StorageAccess.h",
+ "StorageAccessAPIHelper.h",
+ "StoragePrincipalHelper.h",
+ "URLDecorationStripper.h",
+ "URLQueryStringStripper.h",
+]
+
+UNIFIED_SOURCES += [
+ "AntiTrackingRedirectHeuristic.cpp",
+ "AntiTrackingUtils.cpp",
+ "ContentBlockingAllowList.cpp",
+ "ContentBlockingLog.cpp",
+ "ContentBlockingNotifier.cpp",
+ "ContentBlockingTelemetryService.cpp",
+ "ContentBlockingUserInteraction.cpp",
+ "DynamicFpiRedirectHeuristic.cpp",
+ "PartitioningExceptionList.cpp",
+ "RejectForeignAllowList.cpp",
+ "SettingsChangeObserver.cpp",
+ "StorageAccess.cpp",
+ "StorageAccessAPIHelper.cpp",
+ "StoragePrincipalHelper.cpp",
+ "TemporaryAccessGrantObserver.cpp",
+ "URLDecorationStripper.cpp",
+ "URLQueryStringStripper.cpp",
+]
+
+LOCAL_INCLUDES += [
+ "/netwerk/base",
+ "/netwerk/protocol/http",
+]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+FINAL_LIBRARY = "xul"
+
+if CONFIG["MOZ_BUILD_APP"] != "mobile/android":
+ BROWSER_CHROME_MANIFESTS += [
+ "test/browser/browser-blocking.ini",
+ "test/browser/browser.ini",
+ ]
+
+XPCSHELL_TESTS_MANIFESTS += ["test/xpcshell/xpcshell.ini"]
+
+TEST_DIRS += ["test/gtest"]
+
+SPHINX_TREES["anti-tracking"] = "docs"
diff --git a/toolkit/components/antitracking/nsIContentBlockingAllowList.idl b/toolkit/components/antitracking/nsIContentBlockingAllowList.idl
new file mode 100644
index 0000000000..90c2c60492
--- /dev/null
+++ b/toolkit/components/antitracking/nsIContentBlockingAllowList.idl
@@ -0,0 +1,20 @@
+/* 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 "nsISupports.idl"
+#include "nsIPrincipal.idl"
+/**
+ * This file contains an interface to the ContentBlockingAllowList.
+ */
+[scriptable, uuid(00ed5d73-9de5-42cf-868c-e739a94f6b37)]
+interface nsIContentBlockingAllowList : nsISupports {
+
+ /**
+ * Computes a contentBlockingAllowList principal for a given content principal.
+ *
+ * @param aPrincipal the content principal for which the contentBlockingAllowList principal is computed.
+ * @return a contentBlockingAllowList principal.
+ */
+ nsIPrincipal computeContentBlockingAllowListPrincipal(in nsIPrincipal aPrincipal);
+};
diff --git a/toolkit/components/antitracking/nsIPartitioningExceptionListService.idl b/toolkit/components/antitracking/nsIPartitioningExceptionListService.idl
new file mode 100644
index 0000000000..6a6f97a9c9
--- /dev/null
+++ b/toolkit/components/antitracking/nsIPartitioningExceptionListService.idl
@@ -0,0 +1,51 @@
+/* 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 "nsISupports.idl"
+
+/**
+ * Observer for exception list updates.
+ */
+[scriptable, function, uuid(d8db1086-7b59-44d3-9f88-f31a7e642637)]
+interface nsIPartitioningExceptionListObserver : nsISupports
+{
+ /**
+ * Called by nsIPartitioningExceptionListService when the exception list
+ * changes and when the observer is first registered.
+ *
+ * @param aList
+ * A semicolon-separated list of comma-separated url pairs.
+ */
+ void onExceptionListUpdate(in ACString aList);
+};
+
+/**
+ * A service that monitors updates to the exception list of partitioning
+ * from sources such as a local pref and remote settings updates.
+ */
+[scriptable, uuid(cf83a9af-dd3f-43a2-88bb-489a22bca124)]
+interface nsIPartitioningExceptionListService : nsISupports
+{
+ /**
+ * Register a new observer to exception list updates. When the observer is
+ * registered it is called immediately once. Afterwards it will be called
+ * whenever the specified pref changes or when remote settings for
+ * partitioning updates.
+ *
+ * @param aObserver
+ * An nsIPartitioningExceptionListObserver object or function that
+ * will receive updates to the exception list as a comma-separated
+ * string. Will be called immediately with the current exception
+ * list value.
+ */
+ void registerAndRunExceptionListObserver(in nsIPartitioningExceptionListObserver aObserver);
+
+ /**
+ * Unregister an observer.
+ *
+ * @param aObserver
+ * The nsIPartitioningExceptionListObserver object to unregister.
+ */
+ void unregisterExceptionListObserver(in nsIPartitioningExceptionListObserver aObserver);
+};
diff --git a/toolkit/components/antitracking/nsIPurgeTrackerService.idl b/toolkit/components/antitracking/nsIPurgeTrackerService.idl
new file mode 100644
index 0000000000..01955da874
--- /dev/null
+++ b/toolkit/components/antitracking/nsIPurgeTrackerService.idl
@@ -0,0 +1,15 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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 "nsISupports.idl"
+
+[scriptable, uuid(cd68d61e-9a44-402d-9671-838ac0872176)]
+interface nsIPurgeTrackerService : nsISupports
+{
+ /**
+ * Purge cookies and associated data of sites which no longer have the user interaction permission.
+ */
+ Promise purgeTrackingCookieJars();
+};
diff --git a/toolkit/components/antitracking/nsITrackingDBService.idl b/toolkit/components/antitracking/nsITrackingDBService.idl
new file mode 100644
index 0000000000..f755eedbac
--- /dev/null
+++ b/toolkit/components/antitracking/nsITrackingDBService.idl
@@ -0,0 +1,64 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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 "nsISupports.idl"
+
+interface nsIPrincipal;
+interface nsIAsyncInputStream;
+
+[scriptable, uuid(650934db-1939-4424-be26-6ffb0375424d)]
+interface nsITrackingDBService : nsISupports
+{
+ /**
+ * Record entries from a content blocking log in the tracking database.
+ * This function is typically called at the end of the document lifecycle,
+ * since calling it multiple times results in multiple new entries.
+ *
+ * @param data a json string containing the content blocking log.
+ */
+ void recordContentBlockingLog(in ACString data);
+
+ /**
+ * Save new events in the content blocking database
+ * @param data a json string containing the content blocking log.
+ */
+ Promise saveEvents(in AString data);
+
+ /**
+ * Clear all content blocking database entries.
+ */
+ Promise clearAll();
+
+ /**
+ * Clear all content blocking database entries added since the specified time.
+ * @param since a unix timestamp representing the number of milliseconds from
+ * Jan 1, 1970 00:00:00 UTC.
+ */
+ Promise clearSince(in int64_t since);
+
+ /**
+ * Fetch events from the content blocking database
+ * @param dateFrom a unix timestamp.
+ * @param dateTo a unix timestamp.
+ */
+ Promise getEventsByDateRange(in int64_t dateFrom, in int64_t dateTo);
+
+ /**
+ * Return a count of all tracking events.
+ */
+ Promise sumAllEvents();
+
+ /**
+ * Return the earliest recorded date.
+ */
+ Promise getEarliestRecordedDate();
+
+ const unsigned long OTHER_COOKIES_BLOCKED_ID = 0;
+ const unsigned long TRACKERS_ID = 1;
+ const unsigned long TRACKING_COOKIES_ID = 2;
+ const unsigned long CRYPTOMINERS_ID = 3;
+ const unsigned long FINGERPRINTERS_ID = 4;
+ const unsigned long SOCIAL_ID = 5;
+};
diff --git a/toolkit/components/antitracking/nsIURLDecorationAnnotationsService.idl b/toolkit/components/antitracking/nsIURLDecorationAnnotationsService.idl
new file mode 100644
index 0000000000..357b8baaa2
--- /dev/null
+++ b/toolkit/components/antitracking/nsIURLDecorationAnnotationsService.idl
@@ -0,0 +1,27 @@
+/* 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 "nsISupports.idl"
+
+/**
+ * A service that monitors updates to the anti-tracking URL decoration
+ * annotations from remote settings.
+ */
+[scriptable, uuid(937d0c66-6821-4e3f-9e04-50dbc2b2b476)]
+interface nsIURLDecorationAnnotationsService : nsISupports
+{
+ /**
+ * Ensures that the list is updated and resolves the returned promise when
+ * the update is finished.
+ *
+ * The new list will be written to a space-separated list of tokens inside
+ * the following string preference:
+ * privacy.restrict3rdpartystorage.url_decorations
+ *
+ * This preference will be kept up to date with future list updates from
+ * the remote settings server. This preference cannot be modified by any
+ * external component and is managed by this service.
+ */
+ Promise ensureUpdated();
+};
diff --git a/toolkit/components/antitracking/nsIURLQueryStringStripper.idl b/toolkit/components/antitracking/nsIURLQueryStringStripper.idl
new file mode 100644
index 0000000000..372cc0f94a
--- /dev/null
+++ b/toolkit/components/antitracking/nsIURLQueryStringStripper.idl
@@ -0,0 +1,35 @@
+/* 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 "nsISupports.idl"
+#include "nsIURI.idl"
+
+/**
+ * nsIURLQueryStringStripper is responsible for stripping certain part of the
+ * query string of the given URI to address the bounce(redirect) tracking
+ * issues. It will strip every query parameter which matches the strip list
+ * defined in the pref 'privacy.query_stripping.strip_list'. Note that It's
+ * different from URLDecorationStripper which strips the entire query string
+ * from the referrer if there is a tracking query parameter present in the URI.
+ *
+ * TODO: Given that nsIURLQueryStringStripper and URLDecorationStripper are
+ * doing similar things. We could somehow combine these two modules into
+ * one. We will improve this in the future.
+ */
+[scriptable, uuid(6b42a890-2624-4560-99c4-b25380e8cd77)]
+interface nsIURLQueryStringStripper : nsISupports {
+
+ // Strip the query parameters that are in the strip list. Return the amount of
+ // query parameters that have been stripped. Returns 0 if no query parameters
+ // have been stripped or the feature is disabled.
+ uint32_t strip(in nsIURI aURI, in bool aIsPBM, out nsIURI aOutput);
+
+ // Strip the query parameters that are in the stripForCopy/Share strip list.
+ // Returns ether the stripped URI or null if no query parameters have been stripped
+ // Thorws NS_ERROR_NOT_AVAILABLE if the feature is disabled.
+ [must_use] nsIURI stripForCopyOrShare(in nsIURI aURI);
+
+ // Test-only method to get the current strip list.
+ ACString testGetStripList();
+};
diff --git a/toolkit/components/antitracking/nsIURLQueryStrippingListService.idl b/toolkit/components/antitracking/nsIURLQueryStrippingListService.idl
new file mode 100644
index 0000000000..d8b10943cd
--- /dev/null
+++ b/toolkit/components/antitracking/nsIURLQueryStrippingListService.idl
@@ -0,0 +1,71 @@
+/* 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 "nsISupports.idl"
+
+/**
+ * Observer for query stripping list updates.
+ */
+[scriptable, function, uuid(ef56ae12-b1bb-43e6-b1d8-16459cb98dfd)]
+interface nsIURLQueryStrippingListObserver : nsISupports
+{
+ /**
+ * Called by nsIQueryStrippingListService when the list of query stripping
+ * changes and when the observer is first registered. Note that the lists
+ * could have duplicate entries because we would combine the lists from the
+ * pref and remote settings.
+ *
+ * @param aStripList
+ * A space-separated list of query parameters that will be stripped.
+ * @param aAllowList
+ * A comma-separated list of hosts (eTLD+1) that are exempt from query
+ * stripping.
+ */
+ void onQueryStrippingListUpdate(in AString aStripList, in ACString aAllowList);
+};
+
+/**
+ * A service that monitors updates to the query stripping list from sources such
+ * as a local pref and remote settings updates.
+ */
+[scriptable, uuid(afff16f0-3fd2-4153-9ccd-c6d9abd879e4)]
+interface nsIURLQueryStrippingListService : nsISupports
+{
+ /**
+ * Register a new observer to query stripping list updates. When the observer
+ * is registered it is called immediately once. Afterwards it will be called
+ * whenever the specified pref changes or when remote settings for
+ * partitioning updates.
+ *
+ * @param aObserver
+ * An nsIURLQueryStrippingListObserver object or function that
+ * will receive updates to the strip list and the allow list. Will be
+ * called immediately with the current list value.
+ */
+ void registerAndRunObserver(in nsIURLQueryStrippingListObserver aObserver);
+
+ /**
+ * Unregister an observer.
+ *
+ * @param aObserver
+ * The nsIURLQueryStrippingListObserver object to unregister.
+ */
+ void unregisterObserver(in nsIURLQueryStrippingListObserver aObserver);
+
+ /**
+ * Clear all Lists.
+ *
+ * Note that this is for testing purpose.
+ */
+ void clearLists();
+
+ /**
+ * Test-only method used to wait for the list service to initialize fully.
+ * Resolves once the service has reached a fully disabled (false) or fully
+ * enabled state (true).
+ * May also be called when the service is already fully initialized or
+ * disabled, in this case it will resolve immediately.
+ */
+ Promise testWaitForInit();
+};
diff --git a/toolkit/components/antitracking/test/browser/.eslintrc.js b/toolkit/components/antitracking/test/browser/.eslintrc.js
new file mode 100644
index 0000000000..e57058ecb1
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ env: {
+ webextensions: true,
+ },
+};
diff --git a/toolkit/components/antitracking/test/browser/3rdParty.html b/toolkit/components/antitracking/test/browser/3rdParty.html
new file mode 100644
index 0000000000..7cb88ea17a
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/3rdParty.html
@@ -0,0 +1,53 @@
+<html>
+<head>
+ <title>3rd party content!</title>
+ <script type="text/javascript" src="https://example.com/browser/toolkit/components/antitracking/test/browser/storageAccessAPIHelpers.js"></script>
+</head>
+<body>
+<h1>Here the 3rd party content!</h1>
+<script>
+
+function info(msg) {
+ parent.postMessage({ type: "info", msg }, "*");
+}
+
+function ok(what, msg) {
+ parent.postMessage({ type: "ok", what: !!what, msg }, "*");
+}
+
+function is(a, b, msg) {
+ ok(a === b, msg);
+}
+
+onmessage = function(e) {
+ let data = e.data;
+ if (data.includes("!!!")) {
+ // The data argument may be packed with information about whether we are on
+ // the allow list. In that case, extract that information and prepare it
+ // for our callbacks to access it.
+ let parts = data.split("!!!");
+ // Only consider ourselves allow-listed when the cookie policy is set to
+ // 'block third-party trackers or 'block third-party trackers and partition
+ // third-party cookies', since otherwise we won't obtain storage access by
+ // default, which is what this data is used for in tests.
+ let cookieBehavior = SpecialPowers.isContentWindowPrivate(window)
+ ? SpecialPowers.Services.prefs.getIntPref("network.cookie.cookieBehavior.pbmode")
+ : SpecialPowers.Services.prefs.getIntPref("network.cookie.cookieBehavior");
+ window.allowListed =
+ parts[0] === "true" &&
+ (cookieBehavior == SpecialPowers.Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER ||
+ cookieBehavior == SpecialPowers.Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN ||
+ (cookieBehavior == SpecialPowers.Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN &&
+ SpecialPowers.Services.prefs.getBoolPref("network.cookie.rejectForeignWithExceptions.enabled")));
+ data = parts[1];
+ }
+ let runnableStr = `(() => {return (${data});})();`;
+ let runnable = eval(runnableStr); // eslint-disable-line no-eval
+ runnable.call(this, /* Phase */ 1).then(_ => {
+ parent.postMessage({ type: "finish" }, "*");
+ });
+};
+
+</script>
+</body>
+</html>
diff --git a/toolkit/components/antitracking/test/browser/3rdPartyOpen.html b/toolkit/components/antitracking/test/browser/3rdPartyOpen.html
new file mode 100644
index 0000000000..1110834c0e
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/3rdPartyOpen.html
@@ -0,0 +1,16 @@
+<html>
+<head>
+ <title>A popup!</title>
+</head>
+<body>
+<h1>hi!</h1>
+<script>
+
+if (opener) {
+ opener.postMessage("hello!", "*");
+}
+window.close();
+
+</script>
+</body>
+</html>
diff --git a/toolkit/components/antitracking/test/browser/3rdPartyOpenUI.html b/toolkit/components/antitracking/test/browser/3rdPartyOpenUI.html
new file mode 100644
index 0000000000..5afbb5d89b
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/3rdPartyOpenUI.html
@@ -0,0 +1,17 @@
+<html>
+<head>
+ <title>A popup!</title>
+</head>
+<body>
+<h1>hi!</h1>
+<script>
+
+SpecialPowers.wrap(document).userInteractionForTesting();
+if (window.location.search == "?messageme") {
+ window.opener.postMessage("done", "*");
+}
+window.close();
+
+</script>
+</body>
+</html>
diff --git a/toolkit/components/antitracking/test/browser/3rdPartyPartitioned.html b/toolkit/components/antitracking/test/browser/3rdPartyPartitioned.html
new file mode 100644
index 0000000000..f97ed4791f
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/3rdPartyPartitioned.html
@@ -0,0 +1,29 @@
+<html>
+<head>
+ <title>3rd party content!</title>
+</head>
+<body>
+<h1>Here the 3rd party content!</h1>
+<script>
+
+onmessage = async function(e) {
+ let cb = e.data.cb;
+ let runnableStr = `(() => {return (${cb});})();`;
+ let runnable = eval(runnableStr); // eslint-disable-line no-eval
+ let variant = (new URL(location.href)).searchParams.get("variant");
+ let win = this;
+ if (variant == "initial-aboutblank") {
+ let i = win.document.createElement("iframe");
+ i.src = "about:blank";
+ win.document.body.appendChild(i);
+ // override win to make it point to the initial about:blank window
+ win = i.contentWindow;
+ }
+
+ let result = await runnable.call(this, win, e.data.value);
+ parent.postMessage(result, "*");
+};
+
+</script>
+</body>
+</html>
diff --git a/toolkit/components/antitracking/test/browser/3rdPartyRelay.html b/toolkit/components/antitracking/test/browser/3rdPartyRelay.html
new file mode 100644
index 0000000000..64e713913b
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/3rdPartyRelay.html
@@ -0,0 +1,41 @@
+<html>
+<head>
+ <title>Tracker</title>
+ <script type="text/javascript" src="https://example.com/browser/toolkit/components/antitracking/test/browser/storageAccessAPIHelpers.js"></script>
+</head>
+<body>
+<h1>Relay</h1>
+<iframe></iframe>
+<script>
+
+function info(msg) {
+ parent.postMessage({ type: "info", msg }, "*");
+}
+
+function ok(what, msg) {
+ parent.postMessage({ type: "ok", what: !!what, msg }, "*");
+}
+
+function is(a, b, msg) {
+ ok(a === b, msg);
+}
+
+onmessage = function(e) {
+ switch (e.data.type || "") {
+ case "finish":
+ case "ok":
+ case "info":
+ parent.postMessage(e.data, "*");
+ break;
+ default:
+ let iframe = document.querySelector("iframe");
+ iframe.contentWindow.postMessage(e.data, "*");
+ break;
+ }
+};
+
+document.querySelector("iframe").src = location.search.substr(1);
+
+</script>
+</body>
+</html>
diff --git a/toolkit/components/antitracking/test/browser/3rdPartySVG.html b/toolkit/components/antitracking/test/browser/3rdPartySVG.html
new file mode 100644
index 0000000000..df791f355f
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/3rdPartySVG.html
@@ -0,0 +1,20 @@
+<html>
+<head>
+ <title>3rd party content!</title>
+ <style>
+ body {
+ background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><path fill="context-fill" d="M28.5,8.1c0-1.1-1-1.9-2.1-2.4V3.7c-0.2-0.2-0.3-0.3-0.6-0.3c-0.6,0-1.1,0.8-1.3,2.1c-0.2,0-0.3,0-0.5,0l0,0c0-0.2,0-0.3-0.2-0.5c-0.3-1.1-0.8-1.9-1.3-2.6C22,2.6,21.7,3.2,21.7,4L22,6.3c-0.3,0.2-0.6,0.3-1,0.6l-3.5,3.7l0,0c0,0-6.3-0.8-10.9,0.2c-0.6,0-1,0.2-1.1,0.3c-0.5,0.2-0.8,0.3-1.1,0.6c-1.1-0.8-2.2-2.1-3.2-4c0-0.3-0.5-0.5-0.8-0.5s-0.5,0.6-0.3,1c0.8,2.1,2.1,3.5,3.4,4.5c-0.5,0.5-0.8,1-1,1.6c0,0-0.3,2.2-0.3,5.5l1.4,8c0,1,0.8,1.8,1.9,1.8c1,0,1.9-0.8,1.9-1.8V23l0.5-1.3h8.8l0.8,1.3v4.7c0,1,0.8,1.8,1.9,1.8c1,0,1.6-0.6,1.8-1.4l0,0l1.9-9l0,0l2.1-6.4h3c3.4,0,3.7-2.9,3.7-2.9L28.5,8.1z"/></svg>');
+ }
+ </style>
+</head>
+<body>
+<h1>3rd party content with an SVG image background</h1>
+<script>
+
+onload = function(e) {
+ parent.postMessage({ type: "finish" }, "*");
+};
+
+</script>
+</body>
+</html>
diff --git a/toolkit/components/antitracking/test/browser/3rdPartyStorage.html b/toolkit/components/antitracking/test/browser/3rdPartyStorage.html
new file mode 100644
index 0000000000..749ead7c20
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/3rdPartyStorage.html
@@ -0,0 +1,44 @@
+<html>
+<head>
+ <title>3rd party content!</title>
+ <script type="text/javascript" src="https://example.com/browser/toolkit/components/antitracking/test/browser/storageAccessAPIHelpers.js"></script>
+</head>
+<body>
+<h1>Here the 3rd party content!</h1>
+<script>
+
+function info(msg) {
+ parent.postMessage({ type: "info", msg }, "*");
+}
+
+function ok(what, msg) {
+ parent.postMessage({ type: "ok", what: !!what, msg }, "*");
+}
+
+function is(a, b, msg) {
+ ok(a === b, msg);
+}
+
+onmessage = function(e) {
+ let data = e.data;
+ let runnableStr = `(() => {return (${data});})();`;
+ let runnable = eval(runnableStr); // eslint-disable-line no-eval
+
+ let win = window.open("3rdPartyStorageWO.html");
+ win.onload = async _ => {
+ /* import-globals-from storageAccessAPIHelpers.js */
+ await noStorageAccessInitially();
+
+ await runnable.call(this, this, win, false /* allowed */);
+ /* import-globals-from storageAccessAPIHelpers.js */
+ await callRequestStorageAccess();
+ await runnable.call(this, this, win, true /* allowed */);
+
+ win.close();
+ parent.postMessage({ type: "finish" }, "*");
+ };
+};
+
+</script>
+</body>
+</html>
diff --git a/toolkit/components/antitracking/test/browser/3rdPartyStorageWO.html b/toolkit/components/antitracking/test/browser/3rdPartyStorageWO.html
new file mode 100644
index 0000000000..b04916103d
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/3rdPartyStorageWO.html
@@ -0,0 +1,8 @@
+<html>
+<head>
+ <title>1st party content!</title>
+</head>
+<body>
+<h1>Here the 1st party content!</h1>
+</body>
+</html>
diff --git a/toolkit/components/antitracking/test/browser/3rdPartyUI.html b/toolkit/components/antitracking/test/browser/3rdPartyUI.html
new file mode 100644
index 0000000000..57693fbcbd
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/3rdPartyUI.html
@@ -0,0 +1,32 @@
+<html>
+<head>
+ <title>Tracker</title>
+ <script type="text/javascript" src="https://example.com/browser/toolkit/components/antitracking/test/browser/storageAccessAPIHelpers.js"></script>
+</head>
+<body>
+<h1>Tracker</h1>
+<script>
+
+function info(msg) {
+ parent.postMessage({ type: "info", msg }, "*");
+}
+
+function ok(what, msg) {
+ parent.postMessage({ type: "ok", what: !!what, msg }, "*");
+}
+
+function is(a, b, msg) {
+ ok(a === b, msg);
+}
+
+onmessage = function(e) {
+ let runnableStr = `(() => {return (${e.data.callback});})();`;
+ let runnable = eval(runnableStr); // eslint-disable-line no-eval
+ runnable.call(this, e.data.arg || /* Phase */ 3).then(_ => {
+ parent.postMessage({ type: "finish" }, "*");
+ });
+};
+
+</script>
+</body>
+</html>
diff --git a/toolkit/components/antitracking/test/browser/3rdPartyWO.html b/toolkit/components/antitracking/test/browser/3rdPartyWO.html
new file mode 100644
index 0000000000..7986b31063
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/3rdPartyWO.html
@@ -0,0 +1,80 @@
+<html>
+<head>
+ <title>Interact with me!</title>
+ <script type="text/javascript" src="https://example.com/browser/toolkit/components/antitracking/test/browser/storageAccessAPIHelpers.js"></script>
+</head>
+<body>
+<h1>Interact with me!</h1>
+<script>
+
+function info(msg) {
+ parent.postMessage({ type: "info", msg }, "*");
+}
+
+function ok(what, msg) {
+ parent.postMessage({ type: "ok", what: !!what, msg }, "*");
+}
+
+function is(a, b, msg) {
+ ok(a === b, msg);
+}
+
+onmessage = function(e) {
+ let runnableStr = `(() => {return (${e.data.blockingCallback});})();`;
+ let runnable = eval(runnableStr); // eslint-disable-line no-eval
+ runnable.call(this, /* Phase */ 2).then(_ => {
+ info("Let's do a window.open()");
+ return new Promise(resolve => {
+ if (location.search == "?noopener") {
+ let features = "noopener";
+
+ window.open("3rdPartyOpen.html", undefined, features);
+ setTimeout(resolve, 1000);
+ } else {
+ onmessage = resolve;
+
+ window.open("3rdPartyOpen.html");
+ }
+ });
+ }).then(_ => {
+ info("The popup has been dismissed!");
+ // First time storage access should not be granted because the tracker has
+ // not had user interaction yet.
+ let runnableStr = `(() => {return (${e.data.blockingCallback});})();`;
+ let runnable = eval(runnableStr); // eslint-disable-line no-eval
+ return runnable.call(this, /* Phase */ 2);
+ }).then(_ => {
+ info("Let's interact with the tracker");
+ return new Promise(resolve => {
+ onmessage = resolve;
+
+ window.open("3rdPartyOpenUI.html?messageme");
+ });
+ }).then(_ => {
+ info("Let's do another window.open()");
+ return new Promise(resolve => {
+ if (location.search == "?noopener") {
+ let features = "noopener";
+
+ window.open("3rdPartyOpen.html", undefined, features);
+ setTimeout(resolve, 1000);
+ } else {
+ onmessage = resolve;
+
+ window.open("3rdPartyOpen.html");
+ }
+ });
+ }).then(_ => {
+ // This time the tracker must have been able to obtain first-party storage
+ // access because it has had user interaction before.
+ let runnableStr = `(() => {return (${e.data.nonBlockingCallback});})();`;
+ let runnable = eval(runnableStr); // eslint-disable-line no-eval
+ return runnable.call(this, /* Phase */ 2);
+ }).then(_ => {
+ parent.postMessage({ type: "finish" }, "*");
+ });
+};
+
+</script>
+</body>
+</html>
diff --git a/toolkit/components/antitracking/test/browser/3rdPartyWorker.html b/toolkit/components/antitracking/test/browser/3rdPartyWorker.html
new file mode 100644
index 0000000000..e79a992660
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/3rdPartyWorker.html
@@ -0,0 +1,55 @@
+<html>
+<head>
+ <title>Tracker</title>
+ <script type="text/javascript" src="https://example.com/browser/toolkit/components/antitracking/test/browser/storageAccessAPIHelpers.js"></script>
+</head>
+<body>
+<h1>Tracker</h1>
+<script>
+
+function info(msg) {
+ parent.postMessage({ type: "info", msg }, "*");
+}
+
+function ok(what, msg) {
+ parent.postMessage({ type: "ok", what: !!what, msg }, "*");
+}
+
+function is(a, b, msg) {
+ ok(a === b, msg);
+}
+
+function workerCode() {
+ onmessage = e => {
+ try {
+ indexedDB.open("test", "1");
+ postMessage(true);
+ } catch (e) {
+ postMessage(false);
+ }
+ };
+}
+
+var worker;
+function createWorker() {
+ let blob = new Blob([workerCode.toString() + "; workerCode();"]);
+ let blobURL = URL.createObjectURL(blob);
+ info("Blob created");
+
+ worker = new Worker(blobURL);
+ info("Worker created");
+}
+
+onmessage = function(e) {
+ let runnableStr = `(() => {return (${e.data.callback});})();`;
+ let runnable = eval(runnableStr); // eslint-disable-line no-eval
+ runnable.call(this, e.data.arg || /* Phase */ 3).then(_ => {
+ parent.postMessage({ type: "finish" }, "*");
+ });
+};
+
+createWorker();
+
+</script>
+</body>
+</html>
diff --git a/toolkit/components/antitracking/test/browser/antitracking_head.js b/toolkit/components/antitracking/test/browser/antitracking_head.js
new file mode 100644
index 0000000000..1b98e38f83
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/antitracking_head.js
@@ -0,0 +1,1448 @@
+/* vim: set ts=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/. */
+
+/* import-globals-from head.js */
+
+"use strict";
+
+var gFeatures = undefined;
+var gTestTrackersCleanedUp = false;
+var gTestTrackersCleanupRegistered = false;
+
+/**
+ * Force garbage collection.
+ */
+function forceGC() {
+ SpecialPowers.gc();
+ SpecialPowers.forceShrinkingGC();
+ SpecialPowers.forceCC();
+ SpecialPowers.gc();
+ SpecialPowers.forceShrinkingGC();
+ SpecialPowers.forceCC();
+}
+
+this.AntiTracking = {
+ runTestInNormalAndPrivateMode(
+ name,
+ callbackTracking,
+ callbackNonTracking,
+ cleanupFunction,
+ extraPrefs,
+ windowOpenTest = true,
+ userInteractionTest = true,
+ expectedBlockingNotifications = Ci.nsIWebProgressListener
+ .STATE_COOKIES_BLOCKED_TRACKER,
+ iframeSandbox = null,
+ accessRemoval = null,
+ callbackAfterRemoval = null
+ ) {
+ // Normal mode
+ this.runTest(
+ name,
+ callbackTracking,
+ callbackNonTracking,
+ cleanupFunction,
+ extraPrefs,
+ windowOpenTest,
+ userInteractionTest,
+ expectedBlockingNotifications,
+ false,
+ iframeSandbox,
+ accessRemoval,
+ callbackAfterRemoval
+ );
+
+ // Private mode
+ this.runTest(
+ name,
+ callbackTracking,
+ callbackNonTracking,
+ cleanupFunction,
+ extraPrefs,
+ windowOpenTest,
+ userInteractionTest,
+ expectedBlockingNotifications,
+ true,
+ iframeSandbox,
+ accessRemoval,
+ callbackAfterRemoval
+ );
+ },
+
+ runTest(
+ name,
+ callbackTracking,
+ callbackNonTracking,
+ cleanupFunction,
+ extraPrefs,
+ windowOpenTest = true,
+ userInteractionTest = true,
+ expectedBlockingNotifications = Ci.nsIWebProgressListener
+ .STATE_COOKIES_BLOCKED_TRACKER,
+ runInPrivateWindow = false,
+ iframeSandbox = null,
+ accessRemoval = null,
+ callbackAfterRemoval = null
+ ) {
+ let runExtraTests = true;
+ let options = {};
+ if (typeof callbackNonTracking == "object" && !!callbackNonTracking) {
+ options.callback = callbackNonTracking.callback;
+ runExtraTests = callbackNonTracking.runExtraTests;
+ if ("cookieBehavior" in callbackNonTracking) {
+ options.cookieBehavior = callbackNonTracking.cookieBehavior;
+ } else {
+ options.cookieBehavior = BEHAVIOR_ACCEPT;
+ }
+ if ("expectedBlockingNotifications" in callbackNonTracking) {
+ options.expectedBlockingNotifications =
+ callbackNonTracking.expectedBlockingNotifications;
+ } else {
+ options.expectedBlockingNotifications = 0;
+ }
+ if ("blockingByAllowList" in callbackNonTracking) {
+ options.blockingByAllowList = callbackNonTracking.blockingByAllowList;
+ if (options.blockingByAllowList) {
+ // If we're on the allow list, there won't be any blocking!
+ options.expectedBlockingNotifications = 0;
+ }
+ } else {
+ options.blockingByAllowList = false;
+ }
+ callbackNonTracking = options.callback;
+ options.accessRemoval = null;
+ options.callbackAfterRemoval = null;
+ }
+
+ // Here we want to test that a 3rd party context is simply blocked.
+ this._createTask({
+ name,
+ cookieBehavior: BEHAVIOR_REJECT_TRACKER,
+ allowList: false,
+ callback: callbackTracking,
+ extraPrefs,
+ expectedBlockingNotifications,
+ runInPrivateWindow,
+ iframeSandbox,
+ accessRemoval,
+ callbackAfterRemoval,
+ });
+ this._createCleanupTask(cleanupFunction);
+
+ if (callbackNonTracking) {
+ // Phase 1: Here we want to test that a 3rd party context is not blocked if pref is off.
+ if (runExtraTests) {
+ // There are five ways in which the third-party context may not be blocked:
+ // * If the cookieBehavior pref causes it to not be blocked.
+ // * If the contentBlocking pref causes it to not be blocked.
+ // * If both of these prefs cause it to not be blocked.
+ // * If the top-level page is on the content blocking allow list.
+ // * If the contentBlocking third-party cookies UI pref is off, the allow list will be ignored.
+ // All of these cases are tested here.
+ this._createTask({
+ name,
+ cookieBehavior: BEHAVIOR_ACCEPT,
+ allowList: false,
+ callback: callbackNonTracking,
+ extraPrefs,
+ expectedBlockingNotifications: 0,
+ runInPrivateWindow,
+ iframeSandbox,
+ accessRemoval: null, // only passed with non-blocking callback
+ callbackAfterRemoval: null,
+ });
+ this._createCleanupTask(cleanupFunction);
+
+ this._createTask({
+ name,
+ cookieBehavior: BEHAVIOR_ACCEPT,
+ allowList: true,
+ callback: callbackNonTracking,
+ extraPrefs,
+ expectedBlockingNotifications: 0,
+ runInPrivateWindow,
+ iframeSandbox,
+ accessRemoval: null, // only passed with non-blocking callback
+ callbackAfterRemoval: null,
+ });
+ this._createCleanupTask(cleanupFunction);
+
+ this._createTask({
+ name,
+ cookieBehavior: BEHAVIOR_REJECT,
+ allowList: false,
+ callback: callbackTracking,
+ extraPrefs,
+ expectedBlockingNotifications: expectedBlockingNotifications
+ ? Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_ALL
+ : 0,
+ runInPrivateWindow,
+ iframeSandbox,
+ accessRemoval: null, // only passed with non-blocking callback
+ callbackAfterRemoval: null,
+ });
+ this._createCleanupTask(cleanupFunction);
+
+ this._createTask({
+ name,
+ cookieBehavior: BEHAVIOR_LIMIT_FOREIGN,
+ allowList: true,
+ callback: callbackNonTracking,
+ extraPrefs,
+ expectedBlockingNotifications: 0,
+ runInPrivateWindow,
+ iframeSandbox,
+ accessRemoval: null, // only passed with non-blocking callback
+ callbackAfterRemoval: null,
+ });
+ this._createCleanupTask(cleanupFunction);
+
+ this._createTask({
+ name: name + " reject foreign without exception",
+ cookieBehavior: BEHAVIOR_REJECT_FOREIGN,
+ allowList: true,
+ callback: callbackNonTracking,
+ extraPrefs: [
+ ["network.cookie.rejectForeignWithExceptions.enabled", false],
+ ...(extraPrefs || []),
+ ],
+ expectedBlockingNotifications: 0,
+ runInPrivateWindow,
+ iframeSandbox,
+ accessRemoval: null, // only passed with non-blocking callback
+ callbackAfterRemoval: null,
+ });
+ this._createCleanupTask(cleanupFunction);
+
+ this._createTask({
+ name: name + " reject foreign with exception",
+ cookieBehavior: BEHAVIOR_REJECT_FOREIGN,
+ allowList: true,
+ callback: callbackNonTracking,
+ extraPrefs: [
+ ["network.cookie.rejectForeignWithExceptions.enabled", true],
+ ...(extraPrefs || []),
+ ],
+ expectedBlockingNotifications: 0,
+ runInPrivateWindow,
+ iframeSandbox,
+ accessRemoval,
+ callbackAfterRemoval,
+ });
+ this._createCleanupTask(cleanupFunction);
+
+ this._createTask({
+ name,
+ cookieBehavior: BEHAVIOR_REJECT_FOREIGN,
+ allowList: false,
+ callback: callbackTracking,
+ extraPrefs: [
+ ["network.cookie.rejectForeignWithExceptions.enabled", false],
+ ...(extraPrefs || []),
+ ],
+ expectedBlockingNotifications: expectedBlockingNotifications
+ ? Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_FOREIGN
+ : 0,
+ runInPrivateWindow,
+ iframeSandbox,
+ accessRemoval,
+ callbackAfterRemoval,
+ });
+ this._createCleanupTask(cleanupFunction);
+
+ this._createTask({
+ name,
+ cookieBehavior: BEHAVIOR_REJECT_FOREIGN,
+ allowList: false,
+ callback: callbackNonTracking,
+ extraPrefs: [
+ ["network.cookie.rejectForeignWithExceptions.enabled", true],
+ ...(extraPrefs || []),
+ ],
+ expectedBlockingNotifications: false,
+ runInPrivateWindow,
+ iframeSandbox,
+ accessRemoval: null, // only passed with non-blocking callback
+ callbackAfterRemoval: null,
+ thirdPartyPage: TEST_ANOTHER_3RD_PARTY_PAGE,
+ });
+ this._createCleanupTask(cleanupFunction);
+
+ this._createTask({
+ name,
+ cookieBehavior: BEHAVIOR_REJECT_TRACKER,
+ allowList: true,
+ callback: callbackNonTracking,
+ extraPrefs,
+ expectedBlockingNotifications: 0,
+ runInPrivateWindow,
+ iframeSandbox,
+ accessRemoval,
+ callbackAfterRemoval,
+ });
+ this._createCleanupTask(cleanupFunction);
+
+ this._createTask({
+ name,
+ cookieBehavior: BEHAVIOR_REJECT_TRACKER,
+ allowList: false,
+ callback: callbackNonTracking,
+ extraPrefs,
+ expectedBlockingNotifications: false,
+ runInPrivateWindow,
+ iframeSandbox,
+ accessRemoval: null, // only passed with non-blocking callback
+ callbackAfterRemoval: null,
+ thirdPartyPage: TEST_ANOTHER_3RD_PARTY_PAGE,
+ });
+ this._createCleanupTask(cleanupFunction);
+ } else {
+ // This is only used for imageCacheWorker.js tests
+ this._createTask({
+ name,
+ cookieBehavior: options.cookieBehavior,
+ allowList: options.blockingByAllowList,
+ callback: options.callback,
+ extraPrefs,
+ expectedBlockingNotifications: options.expectedBlockingNotifications,
+ runInPrivateWindow,
+ iframeSandbox,
+ accessRemoval: options.accessRemoval,
+ callbackAfterRemoval: options.callbackAfterRemoval,
+ });
+ this._createCleanupTask(cleanupFunction);
+ }
+
+ // Phase 2: Here we want to test that a third-party context doesn't
+ // get blocked with when the same origin is opened through window.open().
+ if (windowOpenTest) {
+ this._createWindowOpenTask(
+ name,
+ BEHAVIOR_REJECT_TRACKER,
+ callbackTracking,
+ callbackNonTracking,
+ runInPrivateWindow,
+ iframeSandbox,
+ false,
+ extraPrefs
+ );
+ this._createCleanupTask(cleanupFunction);
+
+ this._createWindowOpenTask(
+ name,
+ BEHAVIOR_REJECT_FOREIGN,
+ callbackTracking,
+ callbackNonTracking,
+ runInPrivateWindow,
+ iframeSandbox,
+ false,
+ [
+ ["network.cookie.rejectForeignWithExceptions.enabled", true],
+ ...(extraPrefs || []),
+ ]
+ );
+ this._createCleanupTask(cleanupFunction);
+
+ // Now, check if it works for nested iframes.
+ this._createWindowOpenTask(
+ name,
+ BEHAVIOR_REJECT_TRACKER,
+ callbackTracking,
+ callbackNonTracking,
+ runInPrivateWindow,
+ iframeSandbox,
+ true,
+ extraPrefs
+ );
+ this._createCleanupTask(cleanupFunction);
+
+ this._createWindowOpenTask(
+ name,
+ BEHAVIOR_REJECT_FOREIGN,
+ callbackTracking,
+ callbackNonTracking,
+ runInPrivateWindow,
+ iframeSandbox,
+ true,
+ [
+ ["network.cookie.rejectForeignWithExceptions.enabled", true],
+ ...(extraPrefs || []),
+ ]
+ );
+ this._createCleanupTask(cleanupFunction);
+ }
+
+ // Phase 3: Here we want to test that a third-party context doesn't
+ // get blocked with user interaction present
+ if (userInteractionTest) {
+ this._createUserInteractionTask(
+ name,
+ BEHAVIOR_REJECT_TRACKER,
+ callbackTracking,
+ callbackNonTracking,
+ runInPrivateWindow,
+ iframeSandbox,
+ false,
+ extraPrefs
+ );
+ this._createCleanupTask(cleanupFunction);
+
+ this._createUserInteractionTask(
+ name,
+ BEHAVIOR_REJECT_FOREIGN,
+ callbackTracking,
+ callbackNonTracking,
+ runInPrivateWindow,
+ iframeSandbox,
+ false,
+ [
+ ["network.cookie.rejectForeignWithExceptions.enabled", true],
+ ...(extraPrefs || []),
+ ]
+ );
+ this._createCleanupTask(cleanupFunction);
+
+ // Now, check if it works for nested iframes.
+ this._createUserInteractionTask(
+ name,
+ BEHAVIOR_REJECT_TRACKER,
+ callbackTracking,
+ callbackNonTracking,
+ runInPrivateWindow,
+ iframeSandbox,
+ true,
+ extraPrefs
+ );
+ this._createCleanupTask(cleanupFunction);
+
+ this._createUserInteractionTask(
+ name,
+ BEHAVIOR_REJECT_FOREIGN,
+ callbackTracking,
+ callbackNonTracking,
+ runInPrivateWindow,
+ iframeSandbox,
+ true,
+ [
+ ["network.cookie.rejectForeignWithExceptions.enabled", true],
+ ...(extraPrefs || []),
+ ]
+ );
+ this._createCleanupTask(cleanupFunction);
+ }
+ }
+ },
+
+ _waitObserver(targetTopic, expectedCount) {
+ let cnt = 0;
+
+ return new Promise(resolve => {
+ Services.obs.addObserver(function observer(subject, topic, data) {
+ if (topic != targetTopic) {
+ return;
+ }
+ cnt++;
+
+ if (cnt != expectedCount) {
+ return;
+ }
+
+ Services.obs.removeObserver(observer, targetTopic);
+ resolve();
+ }, targetTopic);
+ });
+ },
+
+ _waitUserInteractionPerm() {
+ return this._waitObserver(
+ "antitracking-test-user-interaction-perm-added",
+ 1
+ );
+ },
+
+ _waitStorageAccessPerm(expectedCount) {
+ return this._waitObserver(
+ "antitracking-test-storage-access-perm-added",
+ expectedCount
+ );
+ },
+
+ async interactWithTracker() {
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+ await BrowserTestUtils.withNewTab(
+ { gBrowser: win.gBrowser, url: TEST_3RD_PARTY_PAGE },
+ async function (browser) {
+ info("Let's interact with the tracker");
+
+ await SpecialPowers.spawn(browser, [], async function () {
+ SpecialPowers.wrap(content.document).userInteractionForTesting();
+ });
+ }
+ );
+ await BrowserTestUtils.closeWindow(win);
+ },
+
+ async _setupTest(win, cookieBehavior, runInPrivateWindow, extraPrefs) {
+ await SpecialPowers.flushPrefEnv();
+
+ await setCookieBehaviorPref(cookieBehavior, runInPrivateWindow);
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.storage_access.enabled", true],
+ ["privacy.trackingprotection.enabled", false],
+ ["privacy.trackingprotection.pbmode.enabled", false],
+ ["dom.security.https_first_pbm", false],
+ [
+ "privacy.trackingprotection.annotate_channels",
+ cookieBehavior != BEHAVIOR_ACCEPT,
+ ],
+ ["privacy.restrict3rdpartystorage.console.lazy", false],
+ [
+ "privacy.restrict3rdpartystorage.userInteractionRequiredForHosts",
+ "tracking.example.com,tracking.example.org",
+ ],
+ ["privacy.antitracking.testing", true],
+ ],
+ });
+
+ if (extraPrefs && Array.isArray(extraPrefs) && extraPrefs.length) {
+ await SpecialPowers.pushPrefEnv({ set: extraPrefs });
+
+ let enableWebcompat = Services.prefs.getBoolPref(
+ "privacy.antitracking.enableWebcompat"
+ );
+
+ // If the skip list is disabled by pref, it will always return an empty
+ // list.
+ if (enableWebcompat) {
+ for (let item of extraPrefs) {
+ // When setting up exception URLs, we need to wait to ensure our prefs
+ // actually take effect. In order to do this, we set up a exception
+ // list observer and wait until it calls us back.
+ if (item[0] == "urlclassifier.trackingAnnotationSkipURLs") {
+ info("Waiting for the exception list service to initialize...");
+ let classifier = Cc[
+ "@mozilla.org/url-classifier/dbservice;1"
+ ].getService(Ci.nsIURIClassifier);
+ let feature = classifier.getFeatureByName("tracking-annotation");
+ await TestUtils.waitForCondition(() => {
+ for (let x of item[1].toLowerCase().split(",")) {
+ if (feature.exceptionHostList.split(",").includes(x)) {
+ return true;
+ }
+ }
+ return false;
+ }, "Exception list service initialized");
+ break;
+ }
+ }
+ }
+ }
+
+ await UrlClassifierTestUtils.addTestTrackers();
+ if (!gTestTrackersCleanupRegistered) {
+ registerCleanupFunction(_ => {
+ if (gTestTrackersCleanedUp) {
+ return;
+ }
+ UrlClassifierTestUtils.cleanupTestTrackers();
+ gTestTrackersCleanedUp = true;
+ });
+ gTestTrackersCleanupRegistered = true;
+ }
+ },
+
+ _createTask(options) {
+ add_task(async function () {
+ info(
+ "Starting " +
+ (options.cookieBehavior != BEHAVIOR_ACCEPT
+ ? "blocking"
+ : "non-blocking") +
+ " cookieBehavior (" +
+ options.cookieBehavior +
+ ") with" +
+ (options.allowList ? "" : "out") +
+ " allow list test " +
+ options.name +
+ " running in a " +
+ (options.runInPrivateWindow ? "private" : "normal") +
+ " window " +
+ " with iframe sandbox set to " +
+ options.iframeSandbox +
+ " and access removal set to " +
+ options.accessRemoval +
+ (typeof options.thirdPartyPage == "string"
+ ? " and third party page set to " + options.thirdPartyPage
+ : "") +
+ (typeof options.topPage == "string"
+ ? " and top page set to " + options.topPage
+ : "")
+ );
+
+ is(
+ !!options.callbackAfterRemoval,
+ !!options.accessRemoval,
+ "callbackAfterRemoval must be passed when accessRemoval is non-null"
+ );
+
+ let win = window;
+ if (options.runInPrivateWindow) {
+ win = OpenBrowserWindow({ private: true });
+ await TestUtils.topicObserved("browser-delayed-startup-finished");
+ }
+
+ await AntiTracking._setupTest(
+ win,
+ options.cookieBehavior,
+ options.runInPrivateWindow,
+ options.extraPrefs
+ );
+
+ let topPage;
+ if (typeof options.topPage == "string") {
+ topPage = options.topPage;
+ } else {
+ topPage = TEST_TOP_PAGE;
+ }
+
+ let thirdPartyPage, thirdPartyDomainURI;
+ if (typeof options.thirdPartyPage == "string") {
+ thirdPartyPage = options.thirdPartyPage;
+ let url = new URL(thirdPartyPage);
+ thirdPartyDomainURI = Services.io.newURI(url.origin);
+ } else {
+ thirdPartyPage = TEST_3RD_PARTY_PAGE;
+ thirdPartyDomainURI = Services.io.newURI(TEST_3RD_PARTY_DOMAIN);
+ }
+
+ // It's possible that the third-party domain has been exceptionlisted
+ // through extraPrefs, so let's try annotating it here and adjust our
+ // blocking expectations as necessary.
+ if (
+ options.expectedBlockingNotifications ==
+ Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER
+ ) {
+ if (
+ !(await AntiTracking._isThirdPartyPageClassifiedAsTracker(
+ topPage,
+ thirdPartyDomainURI
+ ))
+ ) {
+ options.expectedBlockingNotifications = 0;
+ }
+ }
+
+ let cookieBlocked = 0;
+ let { expectedBlockingNotifications } = options;
+ if (!Array.isArray(expectedBlockingNotifications)) {
+ expectedBlockingNotifications = [expectedBlockingNotifications];
+ }
+ let listener = {
+ onContentBlockingEvent(webProgress, request, event) {
+ for (const notification of expectedBlockingNotifications) {
+ if (event & notification) {
+ ++cookieBlocked;
+ }
+ }
+ },
+ };
+ function prepareTestEnvironmentOnPage() {
+ win.gBrowser.addProgressListener(listener);
+
+ Services.console.reset();
+ }
+
+ if (!options.allowList) {
+ prepareTestEnvironmentOnPage();
+ }
+
+ let consoleWarningPromise;
+
+ if (options.expectedBlockingNotifications) {
+ consoleWarningPromise = new Promise(resolve => {
+ let consoleListener = {
+ observe(msg) {
+ if (
+ msg
+ .QueryInterface(Ci.nsIScriptError)
+ .category.startsWith("cookieBlocked")
+ ) {
+ Services.console.unregisterListener(consoleListener);
+ resolve();
+ }
+ },
+ };
+
+ Services.console.registerListener(consoleListener);
+ });
+ } else {
+ consoleWarningPromise = Promise.resolve();
+ }
+
+ info("Creating a new tab");
+ let tab = BrowserTestUtils.addTab(win.gBrowser, topPage);
+ win.gBrowser.selectedTab = tab;
+
+ let browser = win.gBrowser.getBrowserForTab(tab);
+ await BrowserTestUtils.browserLoaded(browser);
+
+ info("Check the cookieJarSettings of the browser object");
+ ok(
+ browser.cookieJarSettings,
+ "The browser object has the cookieJarSettings."
+ );
+ is(
+ browser.cookieJarSettings.cookieBehavior,
+ options.cookieBehavior,
+ "The cookieJarSettings has the correct cookieBehavior"
+ );
+
+ if (options.allowList) {
+ info("Disabling content blocking for this page");
+ win.gProtectionsHandler.disableForCurrentPage();
+
+ prepareTestEnvironmentOnPage();
+
+ // The previous function reloads the browser, so wait for it to load again!
+ await BrowserTestUtils.browserLoaded(browser);
+ }
+
+ info("Creating a 3rd party content");
+ let doAccessRemovalChecks =
+ typeof options.accessRemoval == "string" &&
+ options.cookieBehavior == BEHAVIOR_REJECT_TRACKER &&
+ !options.allowList;
+ await SpecialPowers.spawn(
+ browser,
+ [
+ {
+ page: thirdPartyPage,
+ nextPage: TEST_4TH_PARTY_PAGE,
+ callback: options.callback.toString(),
+ callbackAfterRemoval: options.callbackAfterRemoval
+ ? options.callbackAfterRemoval.toString()
+ : null,
+ accessRemoval: options.accessRemoval,
+ iframeSandbox: options.iframeSandbox,
+ allowList: options.allowList,
+ doAccessRemovalChecks,
+ },
+ ],
+ async function (obj) {
+ let id = "id" + Math.random();
+ await new content.Promise(resolve => {
+ let ifr = content.document.createElement("iframe");
+ ifr.id = id;
+ ifr.onload = function () {
+ info("Sending code to the 3rd party content");
+ let callback = obj.allowList + "!!!" + obj.callback;
+ ifr.contentWindow.postMessage(callback, "*");
+ };
+ if (typeof obj.iframeSandbox == "string") {
+ ifr.setAttribute("sandbox", obj.iframeSandbox);
+ }
+
+ content.addEventListener("message", function msg(event) {
+ if (event.data.type == "finish") {
+ content.removeEventListener("message", msg);
+ resolve();
+ return;
+ }
+
+ if (event.data.type == "ok") {
+ ok(event.data.what, event.data.msg);
+ return;
+ }
+
+ if (event.data.type == "info") {
+ info(event.data.msg);
+ return;
+ }
+
+ ok(false, "Unknown message");
+ });
+
+ content.document.body.appendChild(ifr);
+ ifr.src = obj.page;
+ });
+
+ if (obj.doAccessRemovalChecks) {
+ info(`Running after removal checks (${obj.accessRemoval})`);
+ switch (obj.accessRemoval) {
+ case "navigate-subframe":
+ await new content.Promise(resolve => {
+ let ifr = content.document.getElementById(id);
+ let oldWindow = ifr.contentWindow;
+ ifr.onload = function () {
+ info("Sending code to the old 3rd party content");
+ oldWindow.postMessage(obj.callbackAfterRemoval, "*");
+ };
+ if (typeof obj.iframeSandbox == "string") {
+ ifr.setAttribute("sandbox", obj.iframeSandbox);
+ }
+
+ content.addEventListener("message", function msg(event) {
+ if (event.data.type == "finish") {
+ content.removeEventListener("message", msg);
+ resolve();
+ return;
+ }
+
+ if (event.data.type == "ok") {
+ ok(event.data.what, event.data.msg);
+ return;
+ }
+
+ if (event.data.type == "info") {
+ info(event.data.msg);
+ return;
+ }
+
+ ok(false, "Unknown message");
+ });
+
+ ifr.src = obj.nextPage;
+ });
+ break;
+ case "navigate-topframe":
+ // pass-through
+ break;
+ default:
+ ok(
+ false,
+ "Unexpected accessRemoval code passed: " + obj.accessRemoval
+ );
+ break;
+ }
+ }
+ }
+ );
+
+ if (
+ doAccessRemovalChecks &&
+ options.accessRemoval == "navigate-topframe"
+ ) {
+ BrowserTestUtils.loadURIString(browser, TEST_4TH_PARTY_PAGE);
+ await BrowserTestUtils.browserLoaded(browser);
+
+ let pageshow = BrowserTestUtils.waitForContentEvent(
+ tab.linkedBrowser,
+ "pageshow"
+ );
+ gBrowser.goBack();
+ await pageshow;
+
+ await SpecialPowers.spawn(
+ browser,
+ [
+ {
+ page: thirdPartyPage,
+ callbackAfterRemoval: options.callbackAfterRemoval
+ ? options.callbackAfterRemoval.toString()
+ : null,
+ iframeSandbox: options.iframeSandbox,
+ },
+ ],
+ async function (obj) {
+ let ifr = content.document.createElement("iframe");
+ ifr.onload = function () {
+ info(
+ "Sending code to the 3rd party content to verify accessRemoval"
+ );
+ ifr.contentWindow.postMessage(obj.callbackAfterRemoval, "*");
+ };
+ if (typeof obj.iframeSandbox == "string") {
+ ifr.setAttribute("sandbox", obj.iframeSandbox);
+ }
+
+ content.addEventListener("message", function msg(event) {
+ if (event.data.type == "finish") {
+ content.removeEventListener("message", msg);
+ return;
+ }
+
+ if (event.data.type == "ok") {
+ ok(event.data.what, event.data.msg);
+ return;
+ }
+
+ if (event.data.type == "info") {
+ info(event.data.msg);
+ return;
+ }
+
+ ok(false, "Unknown message");
+ });
+
+ content.document.body.appendChild(ifr);
+ ifr.src = obj.page;
+ }
+ );
+ }
+
+ // Wait until the message appears on the console.
+ await consoleWarningPromise;
+
+ let allMessages = Services.console.getMessageArray().filter(msg => {
+ try {
+ // Select all messages that the anti-tracking backend could generate.
+ return msg
+ .QueryInterface(Ci.nsIScriptError)
+ .category.startsWith("cookieBlocked");
+ } catch (e) {
+ return false;
+ }
+ });
+ // When changing this list, please make sure to update the corresponding
+ // code in ReportBlockingToConsole().
+ let expectedCategories = [];
+ let rawExpectedCategories = options.expectedBlockingNotifications;
+ if (!Array.isArray(rawExpectedCategories)) {
+ // if given a single value to match, expect each message to match it
+ rawExpectedCategories = Array(allMessages.length).fill(
+ rawExpectedCategories
+ );
+ }
+ for (let category of rawExpectedCategories) {
+ switch (category) {
+ case Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_BY_PERMISSION:
+ expectedCategories.push("cookieBlockedPermission");
+ break;
+ case Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER:
+ expectedCategories.push("cookieBlockedTracker");
+ break;
+ case Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_ALL:
+ expectedCategories.push("cookieBlockedAll");
+ break;
+ case Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_FOREIGN:
+ expectedCategories.push("cookieBlockedForeign");
+ break;
+ }
+ }
+
+ if (!expectedCategories.length) {
+ is(allMessages.length, 0, "No console messages should be generated");
+ } else {
+ ok(!!allMessages.length, "Some console message should be generated");
+ if (options.errorMessageDomains) {
+ is(
+ allMessages.length,
+ options.errorMessageDomains.length,
+ "Enough items provided in errorMessageDomains"
+ );
+ }
+ }
+ let index = 0;
+ for (let msg of allMessages) {
+ is(
+ msg.category,
+ expectedCategories[index],
+ `Message ${index} should be of expected category`
+ );
+
+ if (options.errorMessageDomains) {
+ ok(
+ msg.errorMessage.includes(options.errorMessageDomains[index]),
+ `Error message domain ${options.errorMessageDomains[index]} (${index}) found in "${msg.errorMessage}"`
+ );
+ index++;
+ }
+ }
+
+ if (options.allowList) {
+ info("Enabling content blocking for this page");
+ win.gProtectionsHandler.enableForCurrentPage();
+
+ // The previous function reloads the browser, so wait for it to load again!
+ await BrowserTestUtils.browserLoaded(browser);
+ }
+
+ win.gBrowser.removeProgressListener(listener);
+
+ if (!!cookieBlocked != !!options.expectedBlockingNotifications) {
+ ok(false, JSON.stringify(cookieBlocked));
+ ok(false, JSON.stringify(options.expectedBlockingNotifications));
+ }
+ is(
+ !!cookieBlocked,
+ !!options.expectedBlockingNotifications,
+ "Checking cookie blocking notifications"
+ );
+
+ info("Removing the tab");
+ BrowserTestUtils.removeTab(tab);
+
+ if (options.runInPrivateWindow) {
+ win.close();
+ }
+ });
+ },
+
+ _createCleanupTask(cleanupFunction) {
+ add_task(async function () {
+ info("Cleaning up.");
+ if (cleanupFunction) {
+ await cleanupFunction();
+ }
+
+ // While running these tests we typically do not have enough idle time to do
+ // GC reliably, so force it here.
+ forceGC();
+ });
+ },
+
+ _createWindowOpenTask(
+ name,
+ cookieBehavior,
+ blockingCallback,
+ nonBlockingCallback,
+ runInPrivateWindow,
+ iframeSandbox,
+ testInSubIFrame,
+ extraPrefs
+ ) {
+ add_task(async function () {
+ info(
+ `Starting window-open${
+ testInSubIFrame ? " sub iframe" : ""
+ } test ${name}`
+ );
+
+ let win = window;
+ if (runInPrivateWindow) {
+ win = OpenBrowserWindow({ private: true });
+ await TestUtils.topicObserved("browser-delayed-startup-finished");
+ }
+
+ await AntiTracking._setupTest(
+ win,
+ cookieBehavior,
+ runInPrivateWindow,
+ extraPrefs
+ );
+
+ info("Creating a new tab");
+ let tab = BrowserTestUtils.addTab(win.gBrowser, TEST_TOP_PAGE);
+ win.gBrowser.selectedTab = tab;
+
+ let browser = win.gBrowser.getBrowserForTab(tab);
+ await BrowserTestUtils.browserLoaded(browser);
+
+ info("Create a first-level iframe to test sub iframes.");
+ if (testInSubIFrame) {
+ let iframeBrowsingContext = await SpecialPowers.spawn(
+ browser,
+ [{ page: TEST_IFRAME_PAGE }],
+ async function (obj) {
+ // Add an iframe.
+ let ifr = content.document.createElement("iframe");
+ let loading = new content.Promise(resolve => {
+ ifr.onload = resolve;
+ });
+ content.document.body.appendChild(ifr);
+ ifr.src = obj.page;
+ await loading;
+
+ return ifr.browsingContext;
+ }
+ );
+
+ browser = iframeBrowsingContext;
+ }
+
+ let pageURL = TEST_3RD_PARTY_PAGE_WO;
+ if (gFeatures == "noopener") {
+ pageURL += "?noopener";
+ }
+
+ info("Creating a 3rd party content");
+ await SpecialPowers.spawn(
+ browser,
+ [
+ {
+ page: pageURL,
+ blockingCallback: blockingCallback.toString(),
+ nonBlockingCallback: nonBlockingCallback.toString(),
+ iframeSandbox,
+ },
+ ],
+ async function (obj) {
+ await new content.Promise(resolve => {
+ let ifr = content.document.createElement("iframe");
+ ifr.onload = function () {
+ info("Sending code to the 3rd party content");
+ ifr.contentWindow.postMessage(obj, "*");
+ };
+ if (typeof obj.iframeSandbox == "string") {
+ ifr.setAttribute("sandbox", obj.iframeSandbox);
+ }
+
+ content.addEventListener("message", function msg(event) {
+ if (event.data.type == "finish") {
+ content.removeEventListener("message", msg);
+ resolve();
+ return;
+ }
+
+ if (event.data.type == "ok") {
+ ok(event.data.what, event.data.msg);
+ return;
+ }
+
+ if (event.data.type == "info") {
+ info(event.data.msg);
+ return;
+ }
+
+ ok(false, "Unknown message");
+ });
+
+ content.document.body.appendChild(ifr);
+ ifr.src = obj.page;
+ });
+ }
+ );
+
+ info("Removing the tab");
+ BrowserTestUtils.removeTab(tab);
+
+ if (runInPrivateWindow) {
+ win.close();
+ }
+ });
+ },
+
+ _createUserInteractionTask(
+ name,
+ cookieBehavior,
+ blockingCallback,
+ nonBlockingCallback,
+ runInPrivateWindow,
+ iframeSandbox,
+ testInSubIFrame,
+ extraPrefs
+ ) {
+ add_task(async function () {
+ info(
+ `Starting user-interaction${
+ testInSubIFrame ? " sub iframe" : ""
+ } test ${name}`
+ );
+
+ let win = window;
+ if (runInPrivateWindow) {
+ win = OpenBrowserWindow({ private: true });
+ await TestUtils.topicObserved("browser-delayed-startup-finished");
+ }
+
+ await AntiTracking._setupTest(
+ win,
+ cookieBehavior,
+ runInPrivateWindow,
+ extraPrefs
+ );
+
+ info("Creating a new tab");
+ let tab = BrowserTestUtils.addTab(win.gBrowser, TEST_TOP_PAGE);
+ win.gBrowser.selectedTab = tab;
+
+ let browser = win.gBrowser.getBrowserForTab(tab);
+ await BrowserTestUtils.browserLoaded(browser);
+
+ if (testInSubIFrame) {
+ info("Create a first-level iframe to test sub iframes.");
+ let iframeBrowsingContext = await SpecialPowers.spawn(
+ browser,
+ [{ page: TEST_IFRAME_PAGE }],
+ async function (obj) {
+ // Add an iframe.
+ let ifr = content.document.createElement("iframe");
+ let loading = new content.Promise(resolve => {
+ ifr.onload = resolve;
+ });
+ content.document.body.appendChild(ifr);
+ ifr.src = obj.page;
+ await loading;
+
+ return ifr.browsingContext;
+ }
+ );
+
+ browser = iframeBrowsingContext;
+ }
+
+ // The following test will open an popup which interacts with the tracker
+ // page. So there will be an user-interaction permission added. We wait
+ // it explicitly.
+ let promiseUIPerm = AntiTracking._waitUserInteractionPerm();
+
+ info("Creating a 3rd party content");
+ await SpecialPowers.spawn(
+ browser,
+ [
+ {
+ page: TEST_3RD_PARTY_PAGE_UI,
+ popup: TEST_POPUP_PAGE,
+ blockingCallback: blockingCallback.toString(),
+ iframeSandbox,
+ },
+ ],
+ async function (obj) {
+ let ifr = content.document.createElement("iframe");
+ let loading = new content.Promise(resolve => {
+ ifr.onload = resolve;
+ });
+ if (typeof obj.iframeSandbox == "string") {
+ ifr.setAttribute("sandbox", obj.iframeSandbox);
+ }
+ content.document.body.appendChild(ifr);
+ ifr.src = obj.page;
+ await loading;
+
+ info(
+ "The 3rd party content should not have access to first party storage."
+ );
+ await new content.Promise(resolve => {
+ content.addEventListener("message", function msg(event) {
+ if (event.data.type == "finish") {
+ content.removeEventListener("message", msg);
+ resolve();
+ return;
+ }
+
+ if (event.data.type == "ok") {
+ ok(event.data.what, event.data.msg);
+ return;
+ }
+
+ if (event.data.type == "info") {
+ info(event.data.msg);
+ return;
+ }
+
+ ok(false, "Unknown message");
+ });
+ ifr.contentWindow.postMessage(
+ { callback: obj.blockingCallback },
+ "*"
+ );
+ });
+
+ let windowClosed = new content.Promise(resolve => {
+ Services.ww.registerNotification(function notification(
+ aSubject,
+ aTopic,
+ aData
+ ) {
+ if (aTopic == "domwindowclosed") {
+ Services.ww.unregisterNotification(notification);
+ resolve();
+ }
+ });
+ });
+
+ info("Opening a window from the iframe.");
+ SpecialPowers.spawn(
+ ifr,
+ [{ popup: obj.popup }],
+ async function (obj) {
+ content.open(obj.popup);
+ }
+ );
+
+ info("Let's wait for the window to be closed");
+ await windowClosed;
+
+ info(
+ "First time, the 3rd party content should not have access to first party storage " +
+ "because the tracker did not have user interaction"
+ );
+ await new content.Promise(resolve => {
+ content.addEventListener("message", function msg(event) {
+ if (event.data.type == "finish") {
+ content.removeEventListener("message", msg);
+ resolve();
+ return;
+ }
+
+ if (event.data.type == "ok") {
+ ok(event.data.what, event.data.msg);
+ return;
+ }
+
+ if (event.data.type == "info") {
+ info(event.data.msg);
+ return;
+ }
+
+ ok(false, "Unknown message");
+ });
+ ifr.contentWindow.postMessage(
+ { callback: obj.blockingCallback },
+ "*"
+ );
+ });
+ }
+ );
+
+ // We wait until the user-interaction permission is added.
+ await promiseUIPerm;
+
+ // We also need to wait the user-interaction permission here.
+ promiseUIPerm = AntiTracking._waitUserInteractionPerm();
+ await AntiTracking.interactWithTracker();
+ await promiseUIPerm;
+
+ // Following test will also open an popup to interact with the page. We
+ // need to explicitly wait it. Without waiting it, it could be added after
+ // we clear up the test and interfere the next test.
+ promiseUIPerm = AntiTracking._waitUserInteractionPerm();
+
+ // We have to wait until the storage access permission is added. This has
+ // the same reason as above user-interaction permission. Note that there
+ // will be two storage access permission added due to the way how we
+ // trigger the heuristic. The first permission is added due to 'Opener'
+ // heuristic and the second one is due to 'Opener after user interaction'.
+ // The page we use to trigger the heuristic will trigger both heuristic,
+ // so we have to wait 2 permissions.
+ let promiseStorageAccessPerm = AntiTracking._waitStorageAccessPerm(2);
+
+ await SpecialPowers.spawn(
+ browser,
+ [
+ {
+ page: TEST_3RD_PARTY_PAGE_UI,
+ popup: TEST_POPUP_PAGE,
+ nonBlockingCallback: nonBlockingCallback.toString(),
+ iframeSandbox,
+ },
+ ],
+ async function (obj) {
+ let ifr = content.document.createElement("iframe");
+ let loading = new content.Promise(resolve => {
+ ifr.onload = resolve;
+ });
+ if (typeof obj.iframeSandbox == "string") {
+ ifr.setAttribute("sandbox", obj.iframeSandbox);
+ }
+ content.document.body.appendChild(ifr);
+ ifr.src = obj.page;
+ await loading;
+
+ let windowClosed = new content.Promise(resolve => {
+ Services.ww.registerNotification(function notification(
+ aSubject,
+ aTopic,
+ aData
+ ) {
+ if (aTopic == "domwindowclosed") {
+ Services.ww.unregisterNotification(notification);
+ resolve();
+ }
+ });
+ });
+
+ info("Opening a window from the iframe.");
+ SpecialPowers.spawn(
+ ifr,
+ [{ popup: obj.popup }],
+ async function (obj) {
+ content.open(obj.popup);
+ }
+ );
+
+ info("Let's wait for the window to be closed");
+ await windowClosed;
+
+ info(
+ "The 3rd party content should now have access to first party storage."
+ );
+ await new content.Promise(resolve => {
+ content.addEventListener("message", function msg(event) {
+ if (event.data.type == "finish") {
+ content.removeEventListener("message", msg);
+ resolve();
+ return;
+ }
+
+ if (event.data.type == "ok") {
+ ok(event.data.what, event.data.msg);
+ return;
+ }
+
+ if (event.data.type == "info") {
+ info(event.data.msg);
+ return;
+ }
+
+ ok(false, "Unknown message");
+ });
+ ifr.contentWindow.postMessage(
+ { callback: obj.nonBlockingCallback },
+ "*"
+ );
+ });
+ }
+ );
+
+ // Explicitly wait the user-interaction and storage access permission
+ // before we do the cleanup.
+ await promiseUIPerm;
+ await promiseStorageAccessPerm;
+
+ info("Removing the tab");
+ BrowserTestUtils.removeTab(tab);
+
+ if (runInPrivateWindow) {
+ win.close();
+ }
+ });
+ },
+
+ async _isThirdPartyPageClassifiedAsTracker(topPage, thirdPartyDomainURI) {
+ let channel;
+ await new Promise((resolve, reject) => {
+ channel = NetUtil.newChannel({
+ uri: thirdPartyDomainURI,
+ loadingPrincipal: Services.scriptSecurityManager.createContentPrincipal(
+ thirdPartyDomainURI,
+ {}
+ ),
+ securityFlags:
+ Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ contentPolicyType: Ci.nsIContentPolicy.TYPE_OTHER,
+ });
+
+ channel
+ .QueryInterface(Ci.nsIHttpChannelInternal)
+ .setTopWindowURIIfUnknown(Services.io.newURI(topPage));
+
+ function Listener() {}
+ Listener.prototype = {
+ onStartRequest(request) {},
+ onDataAvailable(request, stream, off, cnt) {
+ // Consume the data to prevent hitting the assertion.
+ NetUtil.readInputStreamToString(stream, cnt);
+ },
+ onStopRequest(request, st) {
+ let status = request.QueryInterface(Ci.nsIHttpChannel).responseStatus;
+ if (status == 200) {
+ resolve();
+ } else {
+ reject();
+ }
+ },
+ };
+ let listener = new Listener();
+ channel.asyncOpen(listener);
+ });
+
+ return !!(
+ channel.QueryInterface(Ci.nsIClassifiedChannel).classificationFlags &
+ Ci.nsIClassifiedChannel.CLASSIFIED_ANY_BASIC_TRACKING
+ );
+ },
+};
diff --git a/toolkit/components/antitracking/test/browser/browser-blocking.ini b/toolkit/components/antitracking/test/browser/browser-blocking.ini
new file mode 100644
index 0000000000..1533d1273f
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser-blocking.ini
@@ -0,0 +1,72 @@
+[DEFAULT]
+skip-if = os == "linux" && (asan || tsan) # bug 1662229 - task exception
+prefs =
+ # Disable the Storage Access API prompts for all of the tests in this directory
+ dom.storage_access.prompt.testing=true
+ dom.storage_access.prompt.testing.allow=true
+ dom.testing.sync-content-blocking-notifications=true
+ # Enable the window.open() heuristics globally in this directory
+ privacy.restrict3rdpartystorage.heuristic.window_open=true
+ privacy.restrict3rdpartystorage.heuristic.opened_window_after_interaction=true
+ # Disable https-first because of explicit http/https testing
+ dom.security.https_first=false
+
+support-files =
+ head.js
+ antitracking_head.js
+ iframe.html
+ image.sjs
+ page.html
+ 3rdParty.html
+ 3rdPartyRelay.html
+ 3rdPartySVG.html
+ 3rdPartyUI.html
+ 3rdPartyWO.html
+ 3rdPartyWorker.html
+ 3rdPartyOpen.html
+ 3rdPartyOpenUI.html
+ empty.js
+ popup.html
+ server.sjs
+ storageAccessAPIHelpers.js
+ 3rdPartyStorage.html
+ 3rdPartyStorageWO.html
+ 3rdPartyPartitioned.html
+ localStorage.html
+ !/browser/modules/test/browser/head.js
+ !/browser/base/content/test/general/head.js
+ !/browser/base/content/test/protectionsUI/cookieServer.sjs
+ !/browser/base/content/test/protectionsUI/trackingPage.html
+ !/browser/base/content/test/protectionsUI/trackingAPI.js
+
+[browser_blockingCookies.js]
+skip-if = socketprocess_networking
+[browser_blockingDOMCacheSAA.js]
+skip-if = socketprocess_networking
+[browser_blockingDOMCacheAlwaysPartition.js]
+skip-if = socketprocess_networking
+[browser_blockingDOMCacheAlwaysPartitionSAA.js]
+skip-if = socketprocess_networking
+[browser_blockingDOMCache.js]
+[browser_blockingIndexedDb.js]
+skip-if = os == 'linux' && socketprocess_networking
+[browser_blockingIndexedDbInWorkers.js]
+skip-if = os == 'linux' && socketprocess_networking
+[browser_blockingIndexedDbInWorkers2.js]
+[browser_blockingLocalStorage.js]
+skip-if = os == 'linux' && socketprocess_networking
+[browser_blockingSessionStorage.js]
+[browser_blockingServiceWorkers.js]
+[browser_blockingServiceWorkersStorageAccessAPI.js]
+[browser_blockingSharedWorkers.js]
+skip-if = os == 'linux' && socketprocess_networking
+[browser_blockingMessaging.js]
+skip-if = os == "linux" && debug #bug 1627094
+[browser_blockingNoOpener.js]
+[browser_contentBlockingAllowListPrincipal.js]
+support-files =
+ sandboxed.html
+ sandboxed.html^headers^
+[browser_contentBlockingTelemetry.js]
+skip-if =
+ win10_2004 && fission && !debug # high frequency intermittent, same as bug 1727097
diff --git a/toolkit/components/antitracking/test/browser/browser.ini b/toolkit/components/antitracking/test/browser/browser.ini
new file mode 100644
index 0000000000..52b25a4b60
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser.ini
@@ -0,0 +1,226 @@
+[DEFAULT]
+skip-if =
+ os == "linux" && (asan || tsan) # bug 1662229 - task exception
+ os == "win" && os_version == "6.1" # Skip on Azure - frequent failure
+prefs =
+ # Disable the Storage Access API prompts for all of the tests in this directory
+ dom.storage_access.prompt.testing=true
+ dom.storage_access.prompt.testing.allow=true
+ dom.testing.sync-content-blocking-notifications=true
+ # Enable the window.open() heuristics globally in this directory
+ privacy.restrict3rdpartystorage.heuristic.window_open=true
+ privacy.restrict3rdpartystorage.heuristic.opened_window_after_interaction=true
+ # Bug 1617611: Fix all the tests broken by "cookies SameSite=lax by default"
+ network.cookie.sameSite.laxByDefault=false
+ # Disable https-first because of explicit http/https testing
+ dom.security.https_first=false
+
+support-files =
+ container.html
+ container2.html
+ embedder.html
+ embedder2.html
+ head.js
+ antitracking_head.js
+ dynamicfpi_head.js
+ partitionedstorage_head.js
+ storage_access_head.js
+ cookiesCORS.sjs
+ iframe.html
+ image.sjs
+ imageCacheWorker.js
+ page.html
+ 3rdParty.html
+ 3rdPartyRelay.html
+ 3rdPartySVG.html
+ 3rdPartyUI.html
+ 3rdPartyWO.html
+ 3rdPartyWorker.html
+ 3rdPartyOpen.html
+ 3rdPartyOpenUI.html
+ empty.js
+ empty-altsvc.js
+ empty-altsvc.js^headers^
+ empty.html
+ file_iframe_document_open.html
+ file_localStorage.html
+ popup.html
+ redirect.sjs
+ server.sjs
+ storageAccessAPIHelpers.js
+ 3rdPartyStorage.html
+ 3rdPartyStorageWO.html
+ 3rdPartyPartitioned.html
+ localStorage.html
+ raptor.jpg
+ !/browser/modules/test/browser/head.js
+ !/browser/base/content/test/general/head.js
+ !/browser/base/content/test/protectionsUI/cookieServer.sjs
+ !/browser/base/content/test/protectionsUI/trackingPage.html
+ !/browser/base/content/test/protectionsUI/trackingAPI.js
+ !/toolkit/content/tests/browser/common/mockTransfer.js
+
+[browser_aboutblank.js]
+[browser_allowListNotifications.js]
+[browser_allowListNotifications_alwaysPartition.js]
+support-files = subResources.sjs
+[browser_addonHostPermissionIgnoredInTP.js]
+[browser_allowListSeparationInPrivateAndNormalWindows.js]
+skip-if = os == "mac" && !debug # Bug 1503778, 1577362
+[browser_backgroundImageAssertion.js]
+[browser_doublyNestedTracker.js]
+[browser_emailtracking.js]
+[browser_existingCookiesForSubresources.js]
+[browser_fileUrl.js]
+[browser_firstPartyCookieRejectionHonoursAllowList.js]
+[browser_fpiServiceWorkers_fingerprinting.js]
+[browser_hasStorageAccess.js]
+[browser_hasStorageAccess_alwaysPartition.js]
+[browser_iframe_document_open.js]
+[browser_imageCache4.js]
+[browser_imageCache8.js]
+[browser_onBeforeRequestNotificationForTrackingResources.js]
+[browser_onModifyRequestNotificationForTrackingResources.js]
+[browser_permissionInNormalWindows.js]
+[browser_permissionInNormalWindows_alwaysPartition.js]
+[browser_permissionInPrivateWindows.js]
+[browser_permissionInPrivateWindows_alwaysPartition.js]
+[browser_permissionPropagation.js]
+skip-if =
+ os == "linux" && bits == 64 # Bug 1645505
+ os == "win" && debug # Bug 1645505
+ os == "win" && os_version == "6.1" # Skip on Azure - frequent failure
+[browser_referrerDefaultPolicy.js]
+support-files = referrer.sjs
+[browser_siteSpecificWorkArounds.js]
+[browser_subResources.js]
+support-files = subResources.sjs
+[browser_subResourcesPartitioned.js]
+support-files = subResources.sjs
+[browser_subResourcesPartitioned_alwaysPartition.js]
+support-files = subResources.sjs
+[browser_script.js]
+support-files = tracker.js
+[browser_userInteraction.js]
+[browser_serviceWorkersWithStorageAccessGranted.js]
+[browser_storageAccess_TopLevel_Arguments.js]
+[browser_storageAccess_TopLevel_CookieBehavior.js]
+[browser_storageAccess_TopLevel_CookiePermission.js]
+[browser_storageAccess_TopLevel_CrossOriginSameSite.js]
+[browser_storageAccess_TopLevel_Doorhanger.js]
+[browser_storageAccess_TopLevel_Embed.js]
+[browser_storageAccess_TopLevel_Enable.js]
+[browser_storageAccess_TopLevel_RequireIntermediatePermission.js]
+[browser_storageAccess_TopLevel_StorageAccessPermission.js]
+[browser_storageAccess_TopLevel_UserActivation.js]
+skip-if = debug # Bug 1700551
+[browser_storageAccessAutograntRequiresUserInteraction.js]
+[browser_storageAccessDeniedGivesNoUserInteraction.js]
+[browser_storageAccessDoorHanger.js]
+[browser_storageAccessFrameInteractionGrantsUserInteraction.js]
+[browser_storageAccessGrantedGivesUserInteraction.js]
+[browser_storageAccessPrivilegeAPI.js]
+[browser_storageAccessPromiseRejectHandlerUserInteraction.js]
+[browser_storageAccessPromiseRejectHandlerUserInteraction_alwaysPartition.js]
+[browser_storageAccessPromiseResolveHandlerUserInteraction.js]
+[browser_storageAccessRemovalNavigateSubframe.js]
+[browser_storageAccessRemovalNavigateSubframe_alwaysPartition.js]
+[browser_storageAccessRemovalNavigateTopframe.js]
+[browser_storageAccessRemovalNavigateTopframe_alwaysPartition.js]
+[browser_storageAccessSandboxed.js]
+[browser_storageAccessSandboxed_alwaysPartition.js]
+[browser_storageAccessScopeDifferentSite.js]
+[browser_storageAccessScopeSameOrigin.js]
+[browser_storageAccessScopeSameSiteRead.js]
+[browser_storageAccessScopeSameSiteWrite.js]
+[browser_storageAccessThirdPartyChecks.js]
+[browser_storageAccessThirdPartyChecks_alwaysPartition.js]
+[browser_storageAccessWithDynamicFpi.js]
+[browser_storageAccessWithHeuristics.js]
+[browser_allowPermissionForTracker.js]
+[browser_denyPermissionForTracker.js]
+[browser_localStorageEvents.js]
+[browser_partitionedLocalStorage.js]
+[browser_partitionedLocalStorage_events.js]
+support-files = localStorageEvents.html
+[browser_workerPropagation.js]
+support-files = workerIframe.html
+[browser_cookieBetweenTabs.js]
+[browser_partitionedMessaging.js]
+skip-if = true #Bug 1588241
+[browser_partitionedIndexedDB.js]
+[browser_partitionedConsoleMessage.js]
+[browser_partitionedCookies.js]
+support-files = cookies.sjs
+[browser_partitionedDOMCache.js]
+[browser_partitionedServiceWorkers.js]
+support-files = dedicatedWorker.js matchAll.js serviceWorker.js
+[browser_partitionedSharedWorkers.js]
+support-files = sharedWorker.js partitionedSharedWorker.js
+[browser_PBMCookieBehavior.js]
+[browser_socialtracking.js]
+[browser_socialtracking_save_image.js]
+[browser_thirdPartyStorageRejectionForCORS.js]
+[browser_urlDecorationStripping.js]
+[browser_urlDecorationStripping_alwaysPartition.js]
+tags = remote-settings
+[browser_urlQueryStringStripping_allowList.js]
+support-files = file_stripping.html
+[browser_urlQueryStringStripping_telemetry.js]
+support-files = file_stripping.html
+[browser_urlQueryStringStripping_telemetry_2.js]
+support-files = file_stripping.html
+[browser_urlQueryStringStripping.js]
+skip-if =
+ fission && os == "linux" && asan # Bug 1713909 - new Fission platform triage
+support-files = file_stripping.html
+[browser_urlQueryStringStripping_pbmode.js]
+support-files = file_stripping.html
+[browser_urlQueryStringStripping_nimbus.js]
+support-files = file_stripping.html
+[browser_urlQueryStrippingListService.js]
+[browser_staticPartition_cache.js]
+support-files =
+ !/browser/components/originattributes/test/browser/file_cache.html
+ !/browser/components/originattributes/test/browser/file_thirdPartyChild.audio.ogg
+ !/browser/components/originattributes/test/browser/file_thirdPartyChild.embed.png
+ !/browser/components/originattributes/test/browser/file_thirdPartyChild.fetch.html
+ !/browser/components/originattributes/test/browser/file_thirdPartyChild.iframe.html
+ !/browser/components/originattributes/test/browser/file_thirdPartyChild.img.png
+ !/browser/components/originattributes/test/browser/file_thirdPartyChild.favicon.png
+ !/browser/components/originattributes/test/browser/file_thirdPartyChild.import.js
+ !/browser/components/originattributes/test/browser/file_thirdPartyChild.link.css
+ !/browser/components/originattributes/test/browser/file_thirdPartyChild.object.png
+ !/browser/components/originattributes/test/browser/file_thirdPartyChild.request.html
+ !/browser/components/originattributes/test/browser/file_thirdPartyChild.script.js
+ !/browser/components/originattributes/test/browser/file_thirdPartyChild.sharedworker.js
+ !/browser/components/originattributes/test/browser/file_thirdPartyChild.video.ogv
+ !/browser/components/originattributes/test/browser/file_thirdPartyChild.worker.fetch.html
+ !/browser/components/originattributes/test/browser/file_thirdPartyChild.worker.js
+ !/browser/components/originattributes/test/browser/file_thirdPartyChild.worker.request.html
+ !/browser/components/originattributes/test/browser/file_thirdPartyChild.worker.xhr.html
+ !/browser/components/originattributes/test/browser/file_thirdPartyChild.xhr.html
+[browser_staticPartition_network.js]
+[browser_staticPartition_CORS_preflight.js]
+support-files = browser_staticPartition_CORS_preflight.sjs
+[browser_staticPartition_HSTS.js]
+support-files = browser_staticPartition_HSTS.sjs
+[browser_staticPartition_saveAs.js]
+skip-if =
+ os == "linux" && bits == 64 # Bug 1775746
+ win10_2004 && debug # Bug 1775746
+support-files =
+ file_saveAsImage.sjs
+ file_saveAsVideo.sjs
+ file_saveAsPageInfo.html
+ file_video.ogv
+[browser_staticPartition_tls_session.js]
+[browser_staticPartition_websocket.js]
+skip-if =
+ os == 'mac' && verify # Bug 1721210
+support-files =
+ file_ws_handshake_delay_wsh.py
+[browser_partitionedClearSiteDataHeader.js]
+support-files =
+ clearSiteData.sjs
+[browser_AntiTrackingETPHeuristic.js]
diff --git a/toolkit/components/antitracking/test/browser/browser_AntiTrackingETPHeuristic.js b/toolkit/components/antitracking/test/browser/browser_AntiTrackingETPHeuristic.js
new file mode 100644
index 0000000000..f5274d8ba9
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_AntiTrackingETPHeuristic.js
@@ -0,0 +1,261 @@
+"use strict";
+
+const TEST_PAGE = TEST_DOMAIN + TEST_PATH + "page.html";
+const TEST_REDIRECT_PAGE = TEST_DOMAIN + TEST_PATH + "redirect.sjs";
+const TEST_TRACKING_PAGE = TEST_3RD_PARTY_DOMAIN + TEST_PATH + "page.html";
+const TEST_TRACKING_REDIRECT_PAGE =
+ TEST_3RD_PARTY_DOMAIN + TEST_PATH + "redirect.sjs";
+const TEST_ANOTHER_TRACKING_REDIRECT_PAGE =
+ TEST_ANOTHER_3RD_PARTY_DOMAIN_HTTPS + TEST_PATH + "redirect.sjs";
+
+const TEST_CASES = [
+ // Tracker(Interacted) -> Non-Tracker
+ {
+ trackersHasUserInteraction: [TEST_3RD_PARTY_DOMAIN],
+ redirects: [TEST_TRACKING_REDIRECT_PAGE, TEST_PAGE],
+ expectedPermissionNumber: 1,
+ expects: [
+ [
+ TEST_DOMAIN,
+ TEST_3RD_PARTY_DOMAIN,
+ Ci.nsIPermissionManager.ALLOW_ACTION,
+ ],
+ ],
+ },
+ // Tracker(No interaction) -> Non-Tracker
+ {
+ trackersHasUserInteraction: [],
+ redirects: [TEST_TRACKING_REDIRECT_PAGE, TEST_PAGE],
+ expectedPermissionNumber: 0,
+ expects: [
+ [
+ TEST_DOMAIN,
+ TEST_3RD_PARTY_DOMAIN,
+ Ci.nsIPermissionManager.UNKNOWN_ACTION,
+ ],
+ ],
+ },
+ // Non-Tracker -> Tracker(Interacted) -> Non-Tracker
+ {
+ trackersHasUserInteraction: [TEST_3RD_PARTY_DOMAIN],
+ redirects: [TEST_REDIRECT_PAGE, TEST_TRACKING_REDIRECT_PAGE, TEST_PAGE],
+ expectedPermissionNumber: 1,
+ expects: [
+ [
+ TEST_DOMAIN,
+ TEST_3RD_PARTY_DOMAIN,
+ Ci.nsIPermissionManager.ALLOW_ACTION,
+ ],
+ ],
+ },
+ // Tracker(Interacted) -> Tracker(Interacted) -> Tracker(Interacted)
+ {
+ trackersHasUserInteraction: [TEST_3RD_PARTY_DOMAIN],
+ redirects: [
+ TEST_TRACKING_REDIRECT_PAGE,
+ TEST_TRACKING_REDIRECT_PAGE,
+ TEST_TRACKING_PAGE,
+ ],
+ expectedPermissionNumber: 0,
+ expects: [
+ [
+ TEST_3RD_PARTY_DOMAIN,
+ TEST_3RD_PARTY_DOMAIN,
+ Ci.nsIPermissionManager.UNKNOWN_ACTION,
+ ],
+ ],
+ },
+ // Tracker1(Interacted) -> Tracker2(Interacted) -> Non-Tracker
+ {
+ trackersHasUserInteraction: [
+ TEST_3RD_PARTY_DOMAIN,
+ TEST_ANOTHER_3RD_PARTY_DOMAIN_HTTPS,
+ ],
+ redirects: [
+ TEST_TRACKING_REDIRECT_PAGE,
+ TEST_ANOTHER_TRACKING_REDIRECT_PAGE,
+ TEST_PAGE,
+ ],
+ expectedPermissionNumber: 1,
+ expects: [
+ [
+ TEST_DOMAIN,
+ TEST_ANOTHER_3RD_PARTY_DOMAIN_HTTPS,
+ Ci.nsIPermissionManager.ALLOW_ACTION,
+ ],
+ ],
+ },
+ // Tracker1(Interacted) -> Non-Tracker -> Tracker2(No interaction) -> Non-Tracker
+ {
+ trackersHasUserInteraction: [TEST_3RD_PARTY_DOMAIN],
+ redirects: [
+ TEST_TRACKING_REDIRECT_PAGE,
+ TEST_REDIRECT_PAGE,
+ TEST_ANOTHER_TRACKING_REDIRECT_PAGE,
+ TEST_PAGE,
+ ],
+ expectedPermissionNumber: 1,
+ expects: [
+ [
+ TEST_DOMAIN,
+ TEST_3RD_PARTY_DOMAIN,
+ Ci.nsIPermissionManager.ALLOW_ACTION,
+ ],
+ ],
+ },
+ // Tracker1(Interacted) -> Non-Tracker -> Tracker2(Interacted) -> Non-Tracker
+ // Note that the result is not quite correct in this case. We are supposed to
+ // grant access to the least tracker instead of the first one. But, this is
+ // the behavior how we act so far. We would fix this in another bug.
+ {
+ trackersHasUserInteraction: [
+ TEST_3RD_PARTY_DOMAIN,
+ TEST_ANOTHER_3RD_PARTY_DOMAIN_HTTPS,
+ ],
+ redirects: [
+ TEST_TRACKING_REDIRECT_PAGE,
+ TEST_REDIRECT_PAGE,
+ TEST_ANOTHER_TRACKING_REDIRECT_PAGE,
+ TEST_PAGE,
+ ],
+ expectedPermissionNumber: 1,
+ expects: [
+ [
+ TEST_DOMAIN,
+ TEST_3RD_PARTY_DOMAIN,
+ Ci.nsIPermissionManager.ALLOW_ACTION,
+ ],
+ ],
+ },
+ // Tracker(Interacted) -> Non-Tracker (heuristic disabled)
+ {
+ trackersHasUserInteraction: [TEST_3RD_PARTY_DOMAIN],
+ redirects: [TEST_TRACKING_REDIRECT_PAGE, TEST_PAGE],
+ expectedPermissionNumber: 0,
+ expects: [
+ [
+ TEST_DOMAIN,
+ TEST_3RD_PARTY_DOMAIN,
+ Ci.nsIPermissionManager.UNKNOWN_ACTION,
+ ],
+ ],
+ extraPrefs: [["privacy.antitracking.enableWebcompat", false]],
+ },
+];
+
+async function interactWithSpecificTracker(aTracker) {
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+ await BrowserTestUtils.withNewTab(
+ { gBrowser: win.gBrowser, url: aTracker },
+ async function (browser) {
+ info("Let's interact with the tracker");
+
+ await SpecialPowers.spawn(browser, [], async function () {
+ SpecialPowers.wrap(content.document).userInteractionForTesting();
+ });
+ }
+ );
+ await BrowserTestUtils.closeWindow(win);
+}
+
+function getNumberOfStorageAccessPermissions() {
+ let num = 0;
+ for (let perm of Services.perms.all) {
+ if (perm.type.startsWith("3rdPartyStorage^")) {
+ num++;
+ }
+ }
+ return num;
+}
+
+async function verifyStorageAccessPermission(aExpects) {
+ for (let expect of aExpects) {
+ let uri = Services.io.newURI(expect[0]);
+ let principal = Services.scriptSecurityManager.createContentPrincipal(
+ uri,
+ {}
+ );
+ let access = Services.perms.testPermissionFromPrincipal(
+ principal,
+ `3rdPartyStorage^${expect[1].slice(0, -1)}`
+ );
+
+ is(access, expect[2], "The storage access is set correctly");
+ }
+}
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["network.cookie.cookieBehavior", BEHAVIOR_REJECT_TRACKER],
+ ["privacy.trackingprotection.annotate_channels", true],
+ [
+ "privacy.restrict3rdpartystorage.userInteractionRequiredForHosts",
+ "tracking.example.com,tracking.example.org",
+ ],
+ ["privacy.restrict3rdpartystorage.heuristic.redirect", true],
+ ],
+ });
+
+ await UrlClassifierTestUtils.addTestTrackers();
+
+ registerCleanupFunction(_ => {
+ Services.perms.removeAll();
+ });
+});
+
+add_task(async function testETPRedirectHeuristic() {
+ info("Starting testing ETP redirect heuristic ...");
+
+ for (const test of TEST_CASES) {
+ let { extraPrefs } = test;
+ if (extraPrefs) {
+ await SpecialPowers.pushPrefEnv({
+ set: extraPrefs,
+ });
+ }
+
+ // First, clear all permissions.
+ Services.perms.removeAll();
+
+ for (const tracker of test.trackersHasUserInteraction) {
+ info(`Interact with ${tracker} in top-level.`);
+ await interactWithSpecificTracker(tracker);
+ }
+
+ info("Creating a new tab");
+ let tab = BrowserTestUtils.addTab(gBrowser, TEST_PAGE);
+ gBrowser.selectedTab = tab;
+
+ let browser = gBrowser.getBrowserForTab(tab);
+ await BrowserTestUtils.browserLoaded(browser);
+
+ info("Loading the tracking page and trigger the top-level redirect.");
+ SpecialPowers.spawn(browser, [test.redirects], async redirects => {
+ let link = content.document.createElement("a");
+ link.appendChild(content.document.createTextNode("click me!"));
+ link.href = redirects.shift() + "?" + redirects.join("|");
+ content.document.body.appendChild(link);
+ link.click();
+ });
+
+ let finalRedirectDest = test.redirects[test.redirects.length - 1];
+
+ await BrowserTestUtils.browserLoaded(browser, false, finalRedirectDest);
+
+ is(
+ getNumberOfStorageAccessPermissions(),
+ test.expectedPermissionNumber,
+ "The number of storage permissions is correct."
+ );
+
+ await verifyStorageAccessPermission(test.expects);
+
+ info("Removing the tab");
+ BrowserTestUtils.removeTab(tab);
+
+ if (extraPrefs) {
+ await SpecialPowers.popPrefEnv();
+ }
+ }
+});
diff --git a/toolkit/components/antitracking/test/browser/browser_PBMCookieBehavior.js b/toolkit/components/antitracking/test/browser/browser_PBMCookieBehavior.js
new file mode 100644
index 0000000000..ebed00f23c
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_PBMCookieBehavior.js
@@ -0,0 +1,108 @@
+"use strict";
+
+// This test will run all combinations of CookieBehavior. So, request a longer
+// timeout here
+requestLongerTimeout(3);
+
+const COOKIE_BEHAVIORS = [
+ Ci.nsICookieService.BEHAVIOR_ACCEPT,
+ Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN,
+ Ci.nsICookieService.BEHAVIOR_REJECT,
+ Ci.nsICookieService.BEHAVIOR_LIMIT_FOREIGN,
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER,
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN,
+];
+
+async function verifyCookieBehavior(browser, expected) {
+ await SpecialPowers.spawn(
+ browser,
+ [{ expected, page: TEST_3RD_PARTY_PAGE }],
+ async obj => {
+ is(
+ content.document.cookieJarSettings.cookieBehavior,
+ obj.expected,
+ "The tab in the window has the expected CookieBehavior."
+ );
+
+ // Create an 3rd party iframe and check the cookieBehavior.
+ let ifr = content.document.createElement("iframe");
+ let loading = new content.Promise(resolve => {
+ ifr.onload = resolve;
+ });
+ content.document.body.appendChild(ifr);
+ ifr.src = obj.page;
+ await loading;
+
+ await SpecialPowers.spawn(
+ ifr.browsingContext,
+ [obj.expected],
+ async expected => {
+ is(
+ content.document.cookieJarSettings.cookieBehavior,
+ expected,
+ "The iframe in the window has the expected CookieBehavior."
+ );
+ }
+ );
+ }
+ );
+}
+
+add_task(async function () {
+ for (let regularCookieBehavior of COOKIE_BEHAVIORS) {
+ for (let PBMCookieBehavior of COOKIE_BEHAVIORS) {
+ await SpecialPowers.flushPrefEnv();
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["network.cookie.cookieBehavior", regularCookieBehavior],
+ ["network.cookie.cookieBehavior.pbmode", PBMCookieBehavior],
+ ["dom.security.https_first_pbm", false],
+ ],
+ });
+
+ info(
+ ` Start testing with regular cookieBehavior(${regularCookieBehavior}) and PBM cookieBehavior(${PBMCookieBehavior})`
+ );
+
+ info(" Open a tab in regular window.");
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ TEST_TOP_PAGE
+ );
+
+ info(
+ " Verify if the tab in regular window has the expected cookieBehavior."
+ );
+ await verifyCookieBehavior(tab.linkedBrowser, regularCookieBehavior);
+ BrowserTestUtils.removeTab(tab);
+
+ info(" Open a tab in private window.");
+ let pb_win = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ });
+
+ tab = await BrowserTestUtils.openNewForegroundTab(
+ pb_win.gBrowser,
+ TEST_TOP_PAGE
+ );
+
+ let expectPBMCookieBehavior = PBMCookieBehavior;
+
+ // The private cookieBehavior will mirror the regular pref if the regular
+ // pref has a user value and the private pref doesn't have a user pref.
+ if (
+ Services.prefs.prefHasUserValue("network.cookie.cookieBehavior") &&
+ !Services.prefs.prefHasUserValue("network.cookie.cookieBehavior.pbmode")
+ ) {
+ expectPBMCookieBehavior = regularCookieBehavior;
+ }
+
+ info(
+ " Verify if the tab in private window has the expected cookieBehavior."
+ );
+ await verifyCookieBehavior(tab.linkedBrowser, expectPBMCookieBehavior);
+ BrowserTestUtils.removeTab(tab);
+ await BrowserTestUtils.closeWindow(pb_win);
+ }
+ }
+});
diff --git a/toolkit/components/antitracking/test/browser/browser_aboutblank.js b/toolkit/components/antitracking/test/browser/browser_aboutblank.js
new file mode 100644
index 0000000000..f80a948771
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_aboutblank.js
@@ -0,0 +1,44 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function test_aboutblankInIframe() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [
+ "network.cookie.cookieBehavior",
+ BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN,
+ ],
+ [
+ "network.cookie.cookieBehavior.pbmode",
+ BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN,
+ ],
+ ],
+ });
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ TEST_TOP_PAGE
+ );
+ let browser = tab.linkedBrowser;
+
+ await SpecialPowers.spawn(browser, [], async function (obj) {
+ let ifr = content.document.createElement("iframe");
+ let loading = new content.Promise(resolve => {
+ ifr.onload = resolve;
+ });
+ ifr.src = "about:blank";
+ content.document.body.appendChild(ifr);
+ await loading;
+
+ await SpecialPowers.spawn(ifr, [], async function (obj) {
+ ok(
+ content.navigator.cookieEnabled,
+ "Cookie should be enabled in about blank"
+ );
+ });
+ });
+
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/toolkit/components/antitracking/test/browser/browser_addonHostPermissionIgnoredInTP.js b/toolkit/components/antitracking/test/browser/browser_addonHostPermissionIgnoredInTP.js
new file mode 100644
index 0000000000..44664e239a
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_addonHostPermissionIgnoredInTP.js
@@ -0,0 +1,46 @@
+add_task(async function () {
+ info("Starting test");
+
+ await SpecialPowers.flushPrefEnv();
+ await SpecialPowers.pushPrefEnv({
+ set: [["privacy.trackingprotection.enabled", true]],
+ });
+
+ await UrlClassifierTestUtils.addTestTrackers();
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: { permissions: ["https://tracking.example.com/"] },
+ files: {
+ "page.html":
+ '<html><head></head><body><script src="script.js"></script><iframe src="https://tracking.example.com/browser/toolkit/components/antitracking/test/browser/container2.html"></iframe></body></html>',
+ "script.js":
+ 'window.count=0;window.p=new Promise(resolve=>{onmessage=e=>{count=e.data.data;resolve();};});p.then(()=>{document.documentElement.setAttribute("count",count);});',
+ },
+ async background() {
+ browser.test.sendMessage("ready", browser.runtime.getURL("page.html"));
+ },
+ });
+ await extension.startup();
+ let url = await extension.awaitMessage("ready");
+
+ info("Creating a new tab");
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
+ let browser = tab.linkedBrowser;
+
+ info("Verify the number of script nodes found");
+ await ContentTask.spawn(browser, [], async function (obj) {
+ // Need to wait a bit for cross-process postMessage...
+ await ContentTaskUtils.waitForCondition(
+ () => content.document.documentElement.getAttribute("count") !== null,
+ "waiting for 'count' attribute"
+ );
+ let count = content.document.documentElement.getAttribute("count");
+ is(count, 3, "Expected script nodes found");
+ });
+
+ info("Removing the tab");
+ BrowserTestUtils.removeTab(tab);
+
+ UrlClassifierTestUtils.cleanupTestTrackers();
+ await extension.unload();
+});
diff --git a/toolkit/components/antitracking/test/browser/browser_allowListNotifications.js b/toolkit/components/antitracking/test/browser/browser_allowListNotifications.js
new file mode 100644
index 0000000000..999c6f93a0
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_allowListNotifications.js
@@ -0,0 +1,141 @@
+add_task(async function () {
+ info("Starting subResources test");
+
+ await SpecialPowers.flushPrefEnv();
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [
+ "network.cookie.cookieBehavior",
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER,
+ ],
+ [
+ "network.cookie.cookieBehavior.pbmode",
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER,
+ ],
+ ["privacy.trackingprotection.enabled", false],
+ ["privacy.trackingprotection.pbmode.enabled", false],
+ ["privacy.trackingprotection.annotate_channels", true],
+ [
+ "privacy.partition.always_partition_third_party_non_cookie_storage",
+ false,
+ ],
+ ],
+ });
+
+ await UrlClassifierTestUtils.addTestTrackers();
+
+ info("Creating a new tab");
+ let tab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE);
+ gBrowser.selectedTab = tab;
+
+ let browser = gBrowser.getBrowserForTab(tab);
+ await BrowserTestUtils.browserLoaded(browser);
+
+ gProtectionsHandler.disableForCurrentPage();
+
+ // The previous function reloads the browser, so wait for it to load again!
+ await BrowserTestUtils.browserLoaded(browser);
+
+ // Now load subresources from a few third-party origins.
+ // We should expect to see none of these origins in the content blocking log at the end.
+ await fetch(
+ "https://test1.example.com/browser/toolkit/components/antitracking/test/browser/subResources.sjs?result&what=image"
+ )
+ .then(r => r.text())
+ .then(text => {
+ is(text, "0", "Cookies received for images");
+ });
+
+ await fetch(
+ "https://test2.example.com/browser/toolkit/components/antitracking/test/browser/subResources.sjs?result&what=image"
+ )
+ .then(r => r.text())
+ .then(text => {
+ is(text, "0", "Cookies received for images");
+ });
+
+ info("Creating a 3rd party content");
+ await SpecialPowers.spawn(
+ browser,
+ [
+ {
+ page: TEST_3RD_PARTY_PAGE,
+ blockingCallback: (async _ => {}).toString(),
+ nonBlockingCallback: (async _ => {}).toString(),
+ },
+ ],
+ async function (obj) {
+ await new content.Promise(resolve => {
+ let ifr = content.document.createElement("iframe");
+ ifr.onload = function () {
+ info("Sending code to the 3rd party content");
+ ifr.contentWindow.postMessage(obj.blockingCallback, "*");
+ };
+
+ content.addEventListener("message", function msg(event) {
+ if (event.data.type == "finish") {
+ content.removeEventListener("message", msg);
+ resolve();
+ return;
+ }
+
+ if (event.data.type == "ok") {
+ ok(event.data.what, event.data.msg);
+ return;
+ }
+
+ if (event.data.type == "info") {
+ info(event.data.msg);
+ return;
+ }
+
+ ok(false, "Unknown message");
+ });
+
+ content.document.body.appendChild(ifr);
+ ifr.src = obj.page;
+ });
+ }
+ );
+
+ let expectTrackerFound = item => {
+ is(
+ item[0],
+ Ci.nsIWebProgressListener.STATE_LOADED_LEVEL_1_TRACKING_CONTENT,
+ "Correct blocking type reported"
+ );
+ is(item[1], true, "Correct blocking status reported");
+ ok(item[2] >= 1, "Correct repeat count reported");
+ };
+
+ let log = JSON.parse(await browser.getContentBlockingLog());
+ for (let trackerOrigin in log) {
+ is(
+ trackerOrigin + "/",
+ TEST_3RD_PARTY_DOMAIN,
+ "Correct tracker origin must be reported"
+ );
+ let originLog = log[trackerOrigin];
+ is(originLog.length, 1, "We should have 1 entry in the compressed log");
+ expectTrackerFound(originLog[0]);
+ }
+
+ gProtectionsHandler.enableForCurrentPage();
+
+ // The previous function reloads the browser, so wait for it to load again!
+ await BrowserTestUtils.browserLoaded(browser);
+
+ info("Removing the tab");
+ BrowserTestUtils.removeTab(tab);
+
+ UrlClassifierTestUtils.cleanupTestTrackers();
+});
+
+add_task(async function () {
+ info("Cleaning up.");
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+});
diff --git a/toolkit/components/antitracking/test/browser/browser_allowListNotifications_alwaysPartition.js b/toolkit/components/antitracking/test/browser/browser_allowListNotifications_alwaysPartition.js
new file mode 100644
index 0000000000..0fd677e27f
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_allowListNotifications_alwaysPartition.js
@@ -0,0 +1,142 @@
+add_task(async function () {
+ info("Starting subResources test");
+
+ await SpecialPowers.flushPrefEnv();
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [
+ "network.cookie.cookieBehavior",
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER,
+ ],
+ [
+ "network.cookie.cookieBehavior.pbmode",
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER,
+ ],
+ ["privacy.trackingprotection.enabled", false],
+ ["privacy.trackingprotection.pbmode.enabled", false],
+ ["privacy.trackingprotection.annotate_channels", true],
+ [
+ "privacy.partition.always_partition_third_party_non_cookie_storage",
+ true,
+ ],
+ ],
+ });
+
+ await UrlClassifierTestUtils.addTestTrackers();
+
+ info("Creating a new tab");
+ let tab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE);
+ gBrowser.selectedTab = tab;
+
+ let browser = gBrowser.getBrowserForTab(tab);
+ await BrowserTestUtils.browserLoaded(browser);
+
+ gProtectionsHandler.disableForCurrentPage();
+
+ // The previous function reloads the browser, so wait for it to load again!
+ await BrowserTestUtils.browserLoaded(browser);
+
+ // Now load subresources from a few third-party origins.
+ // We should expect to see none of these origins in the content blocking log at the end.
+ await fetch(
+ "https://test1.example.com/browser/toolkit/components/antitracking/test/browser/subResources.sjs?result&what=image"
+ )
+ .then(r => r.text())
+ .then(text => {
+ is(text, "0", "Cookies received for images");
+ });
+
+ await fetch(
+ "https://test2.example.com/browser/toolkit/components/antitracking/test/browser/subResources.sjs?result&what=image"
+ )
+ .then(r => r.text())
+ .then(text => {
+ is(text, "0", "Cookies received for images");
+ });
+
+ info("Creating a 3rd party content");
+ await SpecialPowers.spawn(
+ browser,
+ [
+ {
+ page: TEST_3RD_PARTY_PAGE,
+ blockingCallback: (async _ => {}).toString(),
+ nonBlockingCallback: (async _ => {}).toString(),
+ },
+ ],
+ async function (obj) {
+ await new content.Promise(resolve => {
+ let ifr = content.document.createElement("iframe");
+ ifr.onload = function () {
+ info("Sending code to the 3rd party content");
+ ifr.contentWindow.postMessage(obj.blockingCallback, "*");
+ };
+
+ content.addEventListener("message", function msg(event) {
+ if (event.data.type == "finish") {
+ content.removeEventListener("message", msg);
+ resolve();
+ return;
+ }
+
+ if (event.data.type == "ok") {
+ ok(event.data.what, event.data.msg);
+ return;
+ }
+
+ if (event.data.type == "info") {
+ info(event.data.msg);
+ return;
+ }
+
+ ok(false, "Unknown message");
+ });
+
+ content.document.body.appendChild(ifr);
+ ifr.src = obj.page;
+ });
+ }
+ );
+
+ const nsIWPL = Ci.nsIWebProgressListener;
+ let expectTrackerFound = (item, expect) => {
+ is(item[0], expect, "Correct blocking type reported");
+ is(item[1], true, "Correct blocking status reported");
+ ok(item[2] >= 1, "Correct repeat count reported");
+ };
+
+ let log = JSON.parse(await browser.getContentBlockingLog());
+ for (let trackerOrigin in log) {
+ is(
+ trackerOrigin + "/",
+ TEST_3RD_PARTY_DOMAIN,
+ "Correct tracker origin must be reported"
+ );
+ let originLog = log[trackerOrigin];
+
+ is(originLog.length, 1, "We should have one entry in the compressed log");
+ expectTrackerFound(
+ originLog[0],
+ nsIWPL.STATE_LOADED_LEVEL_1_TRACKING_CONTENT
+ );
+ }
+
+ gProtectionsHandler.enableForCurrentPage();
+
+ // The previous function reloads the browser, so wait for it to load again!
+ await BrowserTestUtils.browserLoaded(browser);
+
+ info("Removing the tab");
+ BrowserTestUtils.removeTab(tab);
+
+ UrlClassifierTestUtils.cleanupTestTrackers();
+});
+
+add_task(async function () {
+ info("Cleaning up.");
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+});
diff --git a/toolkit/components/antitracking/test/browser/browser_allowListSeparationInPrivateAndNormalWindows.js b/toolkit/components/antitracking/test/browser/browser_allowListSeparationInPrivateAndNormalWindows.js
new file mode 100644
index 0000000000..38eb9fc090
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_allowListSeparationInPrivateAndNormalWindows.js
@@ -0,0 +1,52 @@
+// This test works by setting up an exception for the private window allow list
+// manually, and it then expects to see some blocking notifications (note the
+// document.cookie setter in the blocking callback.)
+// If the exception lists aren't handled separately, we'd get confused and put
+// the pages loaded under this test in the allow list, which would result in
+// the test not passing because no blocking notifications would be observed.
+
+// Testing the reverse case would also be interesting, but unfortunately there
+// isn't a super easy way to do that with our antitracking test framework since
+// private windows wouldn't send any blocking notifications as they don't have
+// storage access in the first place.
+
+"use strict";
+add_task(async _ => {
+ let uri = Services.io.newURI("https://example.net");
+ PermissionTestUtils.add(
+ uri,
+ "trackingprotection-pb",
+ Services.perms.ALLOW_ACTION
+ );
+
+ registerCleanupFunction(_ => {
+ Services.perms.removeAll();
+ });
+});
+
+AntiTracking.runTest(
+ "Test that we don't honour a private allow list exception in a normal window",
+ // Blocking callback
+ async _ => {
+ document.cookie = "name=value";
+ },
+
+ // Non blocking callback
+ async _ => {
+ // Nothing to do here.
+ },
+
+ // Cleanup callback
+ async _ => {
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+ },
+ null, // no extra prefs
+ false, // run the window.open() test
+ false, // run the user interaction test
+ Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER, // expect blocking notifications
+ false
+); // run in a normal window
diff --git a/toolkit/components/antitracking/test/browser/browser_allowPermissionForTracker.js b/toolkit/components/antitracking/test/browser/browser_allowPermissionForTracker.js
new file mode 100644
index 0000000000..e628199ead
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_allowPermissionForTracker.js
@@ -0,0 +1,64 @@
+// This test works by setting up an exception for the tracker domain, which
+// disables all the anti-tracking tests.
+
+add_task(async _ => {
+ PermissionTestUtils.add(
+ "https://tracking.example.org",
+ "cookie",
+ Services.perms.ALLOW_ACTION
+ );
+ PermissionTestUtils.add(
+ "https://tracking.example.com",
+ "cookie",
+ Services.perms.ALLOW_ACTION
+ );
+ // Grant interaction permission so we can directly call
+ // requestStorageAccess from the tracker.
+ PermissionTestUtils.add(
+ "https://tracking.example.org",
+ "storageAccessAPI",
+ Services.perms.ALLOW_ACTION
+ );
+
+ registerCleanupFunction(_ => {
+ Services.perms.removeAll();
+ });
+});
+
+AntiTracking._createTask({
+ name: "Test that we do honour a cookie permission for nested windows",
+ cookieBehavior: BEHAVIOR_REJECT_TRACKER,
+ blockingByContentBlockingRTUI: false,
+ allowList: false,
+ callback: async _ => {
+ document.cookie = "name=value";
+ ok(document.cookie != "", "Nothing is blocked");
+
+ // requestStorageAccess should resolve
+ SpecialPowers.wrap(document).notifyUserGestureActivation();
+ await document
+ .requestStorageAccess()
+ .then(() => {
+ ok(true, "Should grant storage access");
+ })
+ .catch(() => {
+ ok(false, "Should grant storage access");
+ });
+ SpecialPowers.wrap(document).clearUserGestureActivation();
+ },
+ // Bug 1617611: Fix all the tests broken by "cookies SameSite=lax by default"
+ extraPrefs: [["network.cookie.sameSite.laxByDefault", false]],
+ expectedBlockingNotifications: 0,
+ runInPrivateWindow: false,
+ iframeSandbox: null,
+ accessRemoval: null,
+ callbackAfterRemoval: null,
+});
+
+add_task(async _ => {
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+});
diff --git a/toolkit/components/antitracking/test/browser/browser_backgroundImageAssertion.js b/toolkit/components/antitracking/test/browser/browser_backgroundImageAssertion.js
new file mode 100644
index 0000000000..16eec0da9e
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_backgroundImageAssertion.js
@@ -0,0 +1,69 @@
+add_task(async function () {
+ info("Starting subResources test");
+
+ await SpecialPowers.flushPrefEnv();
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [
+ "network.cookie.cookieBehavior",
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER,
+ ],
+ [
+ "network.cookie.cookieBehavior.pbmode",
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER,
+ ],
+ ["privacy.trackingprotection.enabled", false],
+ ["privacy.trackingprotection.pbmode.enabled", false],
+ ["privacy.trackingprotection.annotate_channels", true],
+ [
+ "privacy.restrict3rdpartystorage.userInteractionRequiredForHosts",
+ "tracking.example.com,tracking.example.org",
+ ],
+ ],
+ });
+
+ await UrlClassifierTestUtils.addTestTrackers();
+
+ let tab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE);
+ gBrowser.selectedTab = tab;
+
+ let browser = gBrowser.getBrowserForTab(tab);
+ await BrowserTestUtils.browserLoaded(browser);
+
+ await SpecialPowers.spawn(
+ browser,
+ [{ page: TEST_3RD_PARTY_PAGE_WITH_SVG }],
+ async function (obj) {
+ await new content.Promise(resolve => {
+ let ifr = content.document.createElement("iframe");
+
+ content.addEventListener("message", function msg(event) {
+ if (event.data.type == "finish") {
+ content.removeEventListener("message", msg);
+ resolve();
+ return;
+ }
+
+ ok(false, "Unknown message");
+ });
+
+ content.document.body.appendChild(ifr);
+ ifr.src = obj.page;
+ });
+ }
+ );
+
+ ok(true, "No crash, hopefully!");
+ BrowserTestUtils.removeTab(tab);
+
+ UrlClassifierTestUtils.cleanupTestTrackers();
+});
+
+add_task(async function () {
+ info("Cleaning up.");
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+});
diff --git a/toolkit/components/antitracking/test/browser/browser_blockingCookies.js b/toolkit/components/antitracking/test/browser/browser_blockingCookies.js
new file mode 100644
index 0000000000..a8e69b5e15
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_blockingCookies.js
@@ -0,0 +1,183 @@
+requestLongerTimeout(4);
+
+// Bug 1617611: Fix all the tests broken by "cookies SameSite=lax by default"
+Services.prefs.setBoolPref("network.cookie.sameSite.laxByDefault", false);
+registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("network.cookie.sameSite.laxByDefault");
+});
+
+AntiTracking.runTestInNormalAndPrivateMode(
+ "Set/Get Cookies",
+ // Blocking callback
+ async _ => {
+ is(document.cookie, "", "No cookies for me");
+ document.cookie = "name=value";
+ is(document.cookie, "", "No cookies for me");
+
+ for (let arg of ["?checkonly", "?redirect-checkonly"]) {
+ info(`checking with arg=${arg}`);
+ await fetch("server.sjs" + arg)
+ .then(r => r.text())
+ .then(text => {
+ is(text, "cookie-not-present", "We should not have cookies");
+ });
+ // Let's do it twice.
+ await fetch("server.sjs" + arg)
+ .then(r => r.text())
+ .then(text => {
+ is(text, "cookie-not-present", "We should not have cookies");
+ });
+ }
+
+ is(document.cookie, "", "Still no cookies for me");
+ },
+
+ // Non blocking callback
+ async _ => {
+ is(document.cookie, "", "No cookies for me");
+
+ // Note: The ?redirect test is _not_ using checkonly, so it will actually
+ // set our foopy=1 cookie.
+ for (let arg of ["?checkonly", "?redirect"]) {
+ info(`checking with arg=${arg}`);
+ await fetch("server.sjs" + arg)
+ .then(r => r.text())
+ .then(text => {
+ is(text, "cookie-not-present", "We should not have cookies");
+ });
+ }
+
+ document.cookie = "name=value";
+ ok(document.cookie.includes("name=value"), "Some cookies for me");
+ ok(document.cookie.includes("foopy=1"), "Some cookies for me");
+
+ for (let arg of ["", "?redirect"]) {
+ info(`checking with arg=${arg}`);
+ await fetch("server.sjs" + arg)
+ .then(r => r.text())
+ .then(text => {
+ is(text, "cookie-present", "We should have cookies");
+ });
+ }
+
+ ok(document.cookie.length, "Some Cookies for me");
+ },
+
+ // Cleanup callback
+ async _ => {
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+ }
+);
+
+AntiTracking.runTestInNormalAndPrivateMode(
+ "Cookies and Storage Access API",
+ // Blocking callback
+ async _ => {
+ /* import-globals-from storageAccessAPIHelpers.js */
+ await noStorageAccessInitially();
+
+ is(document.cookie, "", "No cookies for me");
+ document.cookie = "name=value";
+ is(document.cookie, "", "No cookies for me");
+
+ for (let arg of ["", "?redirect"]) {
+ info(`checking with arg=${arg}`);
+ await fetch("server.sjs" + arg)
+ .then(r => r.text())
+ .then(text => {
+ is(text, "cookie-not-present", "We should not have cookies");
+ });
+ // Let's do it twice.
+ await fetch("server.sjs" + arg)
+ .then(r => r.text())
+ .then(text => {
+ is(text, "cookie-not-present", "We should not have cookies");
+ });
+ }
+
+ is(document.cookie, "", "Still no cookies for me");
+
+ /* import-globals-from storageAccessAPIHelpers.js */
+ await callRequestStorageAccess();
+
+ is(document.cookie, "", "No cookies for me");
+ document.cookie = "name=value";
+
+ let effectiveCookieBehavior = SpecialPowers.isContentWindowPrivate(window)
+ ? SpecialPowers.Services.prefs.getIntPref(
+ "network.cookie.cookieBehavior.pbmode"
+ )
+ : SpecialPowers.Services.prefs.getIntPref(
+ "network.cookie.cookieBehavior"
+ );
+
+ if (
+ [
+ SpecialPowers.Ci.nsICookieService.BEHAVIOR_REJECT,
+ SpecialPowers.Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN,
+ ].includes(effectiveCookieBehavior)
+ ) {
+ is(document.cookie, "", "No cookies for me");
+ } else {
+ is(document.cookie, "name=value", "I have the cookies!");
+ }
+ },
+
+ // Non blocking callback
+ async _ => {
+ /* import-globals-from storageAccessAPIHelpers.js */
+ await hasStorageAccessInitially();
+
+ is(document.cookie, "", "No cookies for me");
+
+ // Note: The ?redirect test is _not_ using checkonly, so it will actually
+ // set our foopy=1 cookie.
+ for (let arg of ["?checkonly", "?redirect"]) {
+ info(`checking with arg=${arg}`);
+ await fetch("server.sjs" + arg)
+ .then(r => r.text())
+ .then(text => {
+ is(text, "cookie-not-present", "We should not have cookies");
+ });
+ }
+
+ document.cookie = "name=value";
+ ok(document.cookie.includes("name=value"), "Some cookies for me");
+ ok(document.cookie.includes("foopy=1"), "Some cookies for me");
+
+ for (let arg of ["", "?redirect"]) {
+ info(`checking with arg=${arg}`);
+ await fetch("server.sjs" + arg)
+ .then(r => r.text())
+ .then(text => {
+ is(text, "cookie-present", "We should have cookies");
+ });
+ }
+
+ ok(document.cookie.length, "Some Cookies for me");
+
+ /* import-globals-from storageAccessAPIHelpers.js */
+ await callRequestStorageAccess();
+
+ // For non-tracking windows, calling the API is a no-op
+ ok(document.cookie.length, "Still some Cookies for me");
+ ok(document.cookie.includes("name=value"), "Some cookies for me");
+ ok(document.cookie.includes("foopy=1"), "Some cookies for me");
+ },
+
+ // Cleanup callback
+ async _ => {
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+ },
+ null,
+ false,
+ false
+);
diff --git a/toolkit/components/antitracking/test/browser/browser_blockingDOMCache.js b/toolkit/components/antitracking/test/browser/browser_blockingDOMCache.js
new file mode 100644
index 0000000000..a8353cc8f1
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_blockingDOMCache.js
@@ -0,0 +1,39 @@
+requestLongerTimeout(2);
+
+AntiTracking.runTest(
+ "DOM Cache",
+ async _ => {
+ await caches.open("wow").then(
+ _ => {
+ ok(false, "DOM Cache cannot be used!");
+ },
+ _ => {
+ ok(true, "DOM Cache cannot be used!");
+ }
+ );
+ },
+ async _ => {
+ await caches.open("wow").then(
+ _ => {
+ ok(true, "DOM Cache can be used!");
+ },
+ _ => {
+ ok(false, "DOM Cache can be used!");
+ }
+ );
+ },
+ async _ => {
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+ },
+ [
+ ["dom.caches.testing.enabled", true],
+ [
+ "privacy.partition.always_partition_third_party_non_cookie_storage",
+ false,
+ ],
+ ]
+);
diff --git a/toolkit/components/antitracking/test/browser/browser_blockingDOMCacheAlwaysPartition.js b/toolkit/components/antitracking/test/browser/browser_blockingDOMCacheAlwaysPartition.js
new file mode 100644
index 0000000000..4cb90af8ad
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_blockingDOMCacheAlwaysPartition.js
@@ -0,0 +1,54 @@
+requestLongerTimeout(2);
+
+AntiTracking.runTest(
+ "DOM Cache Always Partition Storage",
+ async _ => {
+ let effectiveCookieBehavior = SpecialPowers.isContentWindowPrivate(window)
+ ? SpecialPowers.Services.prefs.getIntPref(
+ "network.cookie.cookieBehavior.pbmode"
+ )
+ : SpecialPowers.Services.prefs.getIntPref(
+ "network.cookie.cookieBehavior"
+ );
+
+ // caches.open still fails for cookieBehavior 2 (reject) and
+ // 1 (reject for third parties), so we need to account for that.
+ let shouldFail =
+ effectiveCookieBehavior ==
+ SpecialPowers.Ci.nsICookieService.BEHAVIOR_REJECT ||
+ (effectiveCookieBehavior ==
+ SpecialPowers.Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN &&
+ !document.location.href.includes("3rdPartyWO") &&
+ !document.location.href.includes("3rdPartyUI"));
+
+ await caches.open("wow").then(
+ _ => {
+ ok(!shouldFail, "DOM Cache can be used!");
+ },
+ _ => {
+ ok(shouldFail, "DOM Cache can be used!");
+ }
+ );
+ },
+ async _ => {
+ await caches.open("wow").then(
+ _ => {
+ ok(true, "DOM Cache can be used!");
+ },
+ _ => {
+ ok(false, "DOM Cache can be used!");
+ }
+ );
+ },
+ async _ => {
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+ },
+ [
+ ["dom.caches.testing.enabled", true],
+ ["privacy.partition.always_partition_third_party_non_cookie_storage", true],
+ ]
+);
diff --git a/toolkit/components/antitracking/test/browser/browser_blockingDOMCacheAlwaysPartitionSAA.js b/toolkit/components/antitracking/test/browser/browser_blockingDOMCacheAlwaysPartitionSAA.js
new file mode 100644
index 0000000000..bef53f2936
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_blockingDOMCacheAlwaysPartitionSAA.js
@@ -0,0 +1,80 @@
+requestLongerTimeout(2);
+
+AntiTracking.runTest(
+ "DOM Cache Always Partition Storage and Storage Access API",
+ async _ => {
+ await noStorageAccessInitially();
+
+ let effectiveCookieBehavior = SpecialPowers.isContentWindowPrivate(window)
+ ? SpecialPowers.Services.prefs.getIntPref(
+ "network.cookie.cookieBehavior.pbmode"
+ )
+ : SpecialPowers.Services.prefs.getIntPref(
+ "network.cookie.cookieBehavior"
+ );
+
+ let shouldThrow = [
+ SpecialPowers.Ci.nsICookieService.BEHAVIOR_REJECT,
+ SpecialPowers.Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN,
+ ].includes(effectiveCookieBehavior);
+
+ await caches.open("wow").then(
+ _ => {
+ ok(!shouldThrow, "DOM Cache can be used!");
+ },
+ _ => {
+ ok(shouldThrow, "DOM Cache can be used!");
+ }
+ );
+
+ await callRequestStorageAccess();
+
+ await caches.open("wow").then(
+ _ => {
+ ok(!shouldThrow, "DOM Cache can be used!");
+ },
+ _ => {
+ ok(shouldThrow, "DOM Cache can be used!");
+ }
+ );
+ },
+ async _ => {
+ /* import-globals-from storageAccessAPIHelpers.js */
+ await hasStorageAccessInitially();
+
+ await caches.open("wow").then(
+ _ => {
+ ok(true, "DOM Cache can be used!");
+ },
+ _ => {
+ ok(false, "DOM Cache can be used!");
+ }
+ );
+
+ /* import-globals-from storageAccessAPIHelpers.js */
+ await callRequestStorageAccess();
+
+ // For non-tracking windows, calling the API is a no-op
+ await caches.open("wow").then(
+ _ => {
+ ok(true, "DOM Cache can be used!");
+ },
+ _ => {
+ ok(false, "DOM Cache can be used!");
+ }
+ );
+ },
+ async _ => {
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+ },
+ [
+ ["dom.caches.testing.enabled", true],
+ ["privacy.partition.always_partition_third_party_non_cookie_storage", true],
+ ],
+ false,
+ false
+);
diff --git a/toolkit/components/antitracking/test/browser/browser_blockingDOMCacheSAA.js b/toolkit/components/antitracking/test/browser/browser_blockingDOMCacheSAA.js
new file mode 100644
index 0000000000..06526972dd
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_blockingDOMCacheSAA.js
@@ -0,0 +1,85 @@
+requestLongerTimeout(2);
+
+AntiTracking.runTest(
+ "DOM Cache and Storage Access API",
+ async _ => {
+ /* import-globals-from storageAccessAPIHelpers.js */
+ await noStorageAccessInitially();
+
+ await caches.open("wow").then(
+ _ => {
+ ok(false, "DOM Cache cannot be used!");
+ },
+ _ => {
+ ok(true, "DOM Cache cannot be used!");
+ }
+ );
+
+ /* import-globals-from storageAccessAPIHelpers.js */
+ await callRequestStorageAccess();
+
+ let effectiveCookieBehavior = SpecialPowers.isContentWindowPrivate(window)
+ ? SpecialPowers.Services.prefs.getIntPref(
+ "network.cookie.cookieBehavior.pbmode"
+ )
+ : SpecialPowers.Services.prefs.getIntPref(
+ "network.cookie.cookieBehavior"
+ );
+
+ let shouldThrow = [
+ SpecialPowers.Ci.nsICookieService.BEHAVIOR_REJECT,
+ SpecialPowers.Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN,
+ ].includes(effectiveCookieBehavior);
+
+ await caches.open("wow").then(
+ _ => {
+ ok(!shouldThrow, "DOM Cache can be used!");
+ },
+ _ => {
+ ok(shouldThrow, "DOM Cache can be used!");
+ }
+ );
+ },
+ async _ => {
+ /* import-globals-from storageAccessAPIHelpers.js */
+ await hasStorageAccessInitially();
+
+ await caches.open("wow").then(
+ _ => {
+ ok(true, "DOM Cache can be used!");
+ },
+ _ => {
+ ok(false, "DOM Cache can be used!");
+ }
+ );
+
+ /* import-globals-from storageAccessAPIHelpers.js */
+ await callRequestStorageAccess();
+
+ // For non-tracking windows, calling the API is a no-op
+ await caches.open("wow").then(
+ _ => {
+ ok(true, "DOM Cache can be used!");
+ },
+ _ => {
+ ok(false, "DOM Cache can be used!");
+ }
+ );
+ },
+ async _ => {
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+ },
+ [
+ ["dom.caches.testing.enabled", true],
+ [
+ "privacy.partition.always_partition_third_party_non_cookie_storage",
+ false,
+ ],
+ ],
+ false,
+ false
+);
diff --git a/toolkit/components/antitracking/test/browser/browser_blockingIndexedDb.js b/toolkit/components/antitracking/test/browser/browser_blockingIndexedDb.js
new file mode 100644
index 0000000000..7fde2365bb
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_blockingIndexedDb.js
@@ -0,0 +1,102 @@
+requestLongerTimeout(4);
+
+AntiTracking.runTestInNormalAndPrivateMode(
+ "IndexedDB",
+ // blocking callback
+ async _ => {
+ try {
+ indexedDB.open("test", "1");
+ ok(false, "IDB should be blocked");
+ } catch (e) {
+ ok(true, "IDB should be blocked");
+ is(e.name, "SecurityError", "We want a security error message.");
+ }
+ },
+ // non-blocking callback
+ async _ => {
+ indexedDB.open("test", "1");
+ ok(true, "IDB should be allowed");
+ },
+ // Cleanup callback
+ async _ => {
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+ },
+ [["dom.indexedDB.hide_in_pbmode.enabled", false]]
+);
+
+AntiTracking.runTestInNormalAndPrivateMode(
+ "IndexedDB and Storage Access API",
+ // blocking callback
+ async _ => {
+ /* import-globals-from storageAccessAPIHelpers.js */
+ await noStorageAccessInitially();
+
+ try {
+ indexedDB.open("test", "1");
+ ok(false, "IDB should be blocked");
+ } catch (e) {
+ ok(true, "IDB should be blocked");
+ is(e.name, "SecurityError", "We want a security error message.");
+ }
+
+ /* import-globals-from storageAccessAPIHelpers.js */
+ await callRequestStorageAccess();
+
+ let effectiveCookieBehavior = SpecialPowers.isContentWindowPrivate(window)
+ ? SpecialPowers.Services.prefs.getIntPref(
+ "network.cookie.cookieBehavior.pbmode"
+ )
+ : SpecialPowers.Services.prefs.getIntPref(
+ "network.cookie.cookieBehavior"
+ );
+
+ let shouldThrow = [
+ SpecialPowers.Ci.nsICookieService.BEHAVIOR_REJECT,
+ SpecialPowers.Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN,
+ ].includes(effectiveCookieBehavior);
+
+ let hasThrown;
+ try {
+ indexedDB.open("test", "1");
+ hasThrown = false;
+ } catch (e) {
+ hasThrown = true;
+ is(e.name, "SecurityError", "We want a security error message.");
+ }
+
+ is(
+ hasThrown,
+ shouldThrow,
+ "IDB should be allowed if not in cookieBehavior pref value BEHAVIOR_REJECT/BEHAVIOR_REJECT_FOREIGN"
+ );
+ },
+ // non-blocking callback
+ async _ => {
+ /* import-globals-from storageAccessAPIHelpers.js */
+ await hasStorageAccessInitially();
+
+ indexedDB.open("test", "1");
+ ok(true, "IDB should be allowed");
+
+ await callRequestStorageAccess();
+
+ // For non-tracking windows, calling the API is a no-op
+ indexedDB.open("test", "1");
+ ok(true, "IDB should be allowed");
+ },
+ // Cleanup callback
+ async _ => {
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+ },
+ [["dom.indexedDB.hide_in_pbmode.enabled", false]],
+ false,
+ false
+);
diff --git a/toolkit/components/antitracking/test/browser/browser_blockingIndexedDbInWorkers.js b/toolkit/components/antitracking/test/browser/browser_blockingIndexedDbInWorkers.js
new file mode 100644
index 0000000000..55db3cc05b
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_blockingIndexedDbInWorkers.js
@@ -0,0 +1,73 @@
+AntiTracking.runTestInNormalAndPrivateMode(
+ "IndexedDB in workers",
+ async _ => {
+ function blockCode() {
+ try {
+ indexedDB.open("test", "1");
+ postMessage(false);
+ } catch (e) {
+ postMessage(e.name == "SecurityError");
+ }
+ }
+
+ let blob = new Blob([blockCode.toString() + "; blockCode();"]);
+ ok(blob, "Blob has been created");
+
+ let blobURL = URL.createObjectURL(blob);
+ ok(blobURL, "Blob URL has been created");
+
+ let worker = new Worker(blobURL);
+ ok(worker, "Worker has been created");
+
+ await new Promise((resolve, reject) => {
+ worker.onmessage = function (e) {
+ if (e.data) {
+ resolve();
+ } else {
+ reject();
+ }
+ };
+
+ worker.onerror = function (e) {
+ reject();
+ };
+ });
+ },
+ async _ => {
+ function nonBlockCode() {
+ indexedDB.open("test", "1");
+ postMessage(true);
+ }
+
+ let blob = new Blob([nonBlockCode.toString() + "; nonBlockCode();"]);
+ ok(blob, "Blob has been created");
+
+ let blobURL = URL.createObjectURL(blob);
+ ok(blobURL, "Blob URL has been created");
+
+ let worker = new Worker(blobURL);
+ ok(worker, "Worker has been created");
+
+ await new Promise((resolve, reject) => {
+ worker.onmessage = function (e) {
+ if (e.data) {
+ resolve();
+ } else {
+ reject();
+ }
+ };
+
+ worker.onerror = function (e) {
+ reject();
+ };
+ });
+ },
+ async _ => {
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+ },
+ [["dom.indexedDB.hide_in_pbmode.enabled", false]]
+);
diff --git a/toolkit/components/antitracking/test/browser/browser_blockingIndexedDbInWorkers2.js b/toolkit/components/antitracking/test/browser/browser_blockingIndexedDbInWorkers2.js
new file mode 100644
index 0000000000..02ce8dc588
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_blockingIndexedDbInWorkers2.js
@@ -0,0 +1,152 @@
+requestLongerTimeout(6);
+
+AntiTracking.runTestInNormalAndPrivateMode(
+ "IndexedDB in workers and Storage Access API",
+ async _ => {
+ function blockCode() {
+ try {
+ indexedDB.open("test", "1");
+ postMessage(false);
+ } catch (e) {
+ postMessage(e.name == "SecurityError");
+ }
+ }
+ function nonBlockCode() {
+ indexedDB.open("test", "1");
+ postMessage(true);
+ }
+
+ /* import-globals-from storageAccessAPIHelpers.js */
+ await noStorageAccessInitially();
+
+ let blob = new Blob([blockCode.toString() + "; blockCode();"]);
+ ok(blob, "Blob has been created");
+
+ let blobURL = URL.createObjectURL(blob);
+ ok(blobURL, "Blob URL has been created");
+
+ let worker = new Worker(blobURL);
+ ok(worker, "Worker has been created");
+
+ await new Promise((resolve, reject) => {
+ worker.onmessage = function (e) {
+ if (e.data) {
+ resolve();
+ } else {
+ reject();
+ }
+ };
+
+ worker.onerror = function (e) {
+ reject();
+ };
+ });
+
+ /* import-globals-from storageAccessAPIHelpers.js */
+ await callRequestStorageAccess();
+
+ let effectiveCookieBehavior = SpecialPowers.isContentWindowPrivate(window)
+ ? SpecialPowers.Services.prefs.getIntPref(
+ "network.cookie.cookieBehavior.pbmode"
+ )
+ : SpecialPowers.Services.prefs.getIntPref(
+ "network.cookie.cookieBehavior"
+ );
+
+ if (
+ [
+ SpecialPowers.Ci.nsICookieService.BEHAVIOR_REJECT,
+ SpecialPowers.Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN,
+ ].includes(effectiveCookieBehavior)
+ ) {
+ blob = new Blob([blockCode.toString() + "; blockCode();"]);
+ } else {
+ blob = new Blob([nonBlockCode.toString() + "; nonBlockCode();"]);
+ }
+ ok(blob, "Blob has been created");
+
+ blobURL = URL.createObjectURL(blob);
+ ok(blobURL, "Blob URL has been created");
+
+ worker = new Worker(blobURL);
+ ok(worker, "Worker has been created");
+
+ await new Promise((resolve, reject) => {
+ worker.onmessage = function (e) {
+ if (e.data) {
+ resolve();
+ } else {
+ reject();
+ }
+ };
+
+ worker.onerror = function (e) {
+ reject();
+ };
+ });
+ },
+ async _ => {
+ function nonBlockCode() {
+ indexedDB.open("test", "1");
+ postMessage(true);
+ }
+
+ /* import-globals-from storageAccessAPIHelpers.js */
+ await hasStorageAccessInitially();
+
+ let blob = new Blob([nonBlockCode.toString() + "; nonBlockCode();"]);
+ ok(blob, "Blob has been created");
+
+ let blobURL = URL.createObjectURL(blob);
+ ok(blobURL, "Blob URL has been created");
+
+ let worker = new Worker(blobURL);
+ ok(worker, "Worker has been created");
+
+ await new Promise((resolve, reject) => {
+ worker.onmessage = function (e) {
+ if (e.data) {
+ resolve();
+ } else {
+ reject();
+ }
+ };
+
+ worker.onerror = function (e) {
+ reject();
+ };
+ });
+
+ /* import-globals-from storageAccessAPIHelpers.js */
+ await callRequestStorageAccess();
+
+ // For non-tracking windows, calling the API is a no-op
+
+ worker = new Worker(blobURL);
+ ok(worker, "Worker has been created");
+
+ await new Promise((resolve, reject) => {
+ worker.onmessage = function (e) {
+ if (e.data) {
+ resolve();
+ } else {
+ reject();
+ }
+ };
+
+ worker.onerror = function (e) {
+ reject();
+ };
+ });
+ },
+ async _ => {
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+ },
+ [["dom.indexedDB.hide_in_pbmode.enabled", false]],
+ false,
+ false
+);
diff --git a/toolkit/components/antitracking/test/browser/browser_blockingLocalStorage.js b/toolkit/components/antitracking/test/browser/browser_blockingLocalStorage.js
new file mode 100644
index 0000000000..1362fc7519
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_blockingLocalStorage.js
@@ -0,0 +1,94 @@
+requestLongerTimeout(4);
+
+AntiTracking.runTestInNormalAndPrivateMode(
+ "localStorage",
+ async _ => {
+ try {
+ localStorage.foo = 42;
+ ok(false, "LocalStorage cannot be used!");
+ } catch (e) {
+ ok(true, "LocalStorage cannot be used!");
+ is(e.name, "SecurityError", "We want a security error message.");
+ }
+ },
+ async _ => {
+ localStorage.foo = 42;
+ ok(true, "LocalStorage is allowed");
+ },
+ async _ => {
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+ }
+);
+
+AntiTracking.runTestInNormalAndPrivateMode(
+ "localStorage and Storage Access API",
+ async _ => {
+ /* import-globals-from storageAccessAPIHelpers.js */
+ await noStorageAccessInitially();
+
+ try {
+ localStorage.foo = 42;
+ ok(false, "LocalStorage cannot be used!");
+ } catch (e) {
+ ok(true, "LocalStorage cannot be used!");
+ is(e.name, "SecurityError", "We want a security error message.");
+ }
+
+ /* import-globals-from storageAccessAPIHelpers.js */
+ await callRequestStorageAccess();
+
+ let effectiveCookieBehavior = SpecialPowers.isContentWindowPrivate(window)
+ ? SpecialPowers.Services.prefs.getIntPref(
+ "network.cookie.cookieBehavior.pbmode"
+ )
+ : SpecialPowers.Services.prefs.getIntPref(
+ "network.cookie.cookieBehavior"
+ );
+
+ if (
+ [
+ SpecialPowers.Ci.nsICookieService.BEHAVIOR_REJECT,
+ SpecialPowers.Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN,
+ ].includes(effectiveCookieBehavior)
+ ) {
+ try {
+ localStorage.foo = 42;
+ ok(false, "LocalStorage cannot be used!");
+ } catch (e) {
+ ok(true, "LocalStorage cannot be used!");
+ is(e.name, "SecurityError", "We want a security error message.");
+ }
+ } else {
+ localStorage.foo = 42;
+ ok(true, "LocalStorage is allowed");
+ }
+ },
+ async _ => {
+ /* import-globals-from storageAccessAPIHelpers.js */
+ await hasStorageAccessInitially();
+
+ localStorage.foo = 42;
+ ok(true, "LocalStorage is allowed");
+
+ /* import-globals-from storageAccessAPIHelpers.js */
+ await callRequestStorageAccess();
+
+ // For non-tracking windows, calling the API is a no-op
+ localStorage.foo = 42;
+ ok(true, "LocalStorage is allowed");
+ },
+ async _ => {
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+ },
+ null,
+ false,
+ false
+);
diff --git a/toolkit/components/antitracking/test/browser/browser_blockingMessaging.js b/toolkit/components/antitracking/test/browser/browser_blockingMessaging.js
new file mode 100644
index 0000000000..34af2b76ce
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_blockingMessaging.js
@@ -0,0 +1,322 @@
+if (AppConstants.MOZ_CODE_COVERAGE) {
+ requestLongerTimeout(12);
+} else {
+ requestLongerTimeout(12);
+}
+
+AntiTracking.runTestInNormalAndPrivateMode(
+ "BroadcastChannel",
+ async _ => {
+ try {
+ new BroadcastChannel("hello");
+ ok(false, "BroadcastChannel cannot be used!");
+ } catch (e) {
+ ok(true, "BroadcastChannel cannot be used!");
+ is(e.name, "SecurityError", "We want a security error message.");
+ }
+ },
+ async _ => {
+ new BroadcastChannel("hello");
+ ok(true, "BroadcastChannel be used");
+ },
+ async _ => {
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+ }
+);
+
+AntiTracking.runTestInNormalAndPrivateMode(
+ "BroadcastChannel in workers",
+ async _ => {
+ function blockingCode() {
+ try {
+ new BroadcastChannel("hello");
+ postMessage(false);
+ } catch (e) {
+ postMessage(e.name == "SecurityError");
+ }
+ }
+
+ let blob = new Blob([blockingCode.toString() + "; blockingCode();"]);
+ ok(blob, "Blob has been created");
+
+ let blobURL = URL.createObjectURL(blob);
+ ok(blobURL, "Blob URL has been created");
+
+ let worker = new Worker(blobURL);
+ ok(worker, "Worker has been created");
+
+ await new Promise((resolve, reject) => {
+ worker.onmessage = function (e) {
+ if (e.data) {
+ resolve();
+ } else {
+ reject();
+ }
+ };
+
+ worker.onerror = function (e) {
+ reject();
+ };
+ });
+ },
+ async _ => {
+ function nonBlockingCode() {
+ new BroadcastChannel("hello");
+ postMessage(true);
+ }
+
+ let blob = new Blob([nonBlockingCode.toString() + "; nonBlockingCode();"]);
+ ok(blob, "Blob has been created");
+
+ let blobURL = URL.createObjectURL(blob);
+ ok(blobURL, "Blob URL has been created");
+
+ let worker = new Worker(blobURL);
+ ok(worker, "Worker has been created");
+
+ await new Promise((resolve, reject) => {
+ worker.onmessage = function (e) {
+ if (e.data) {
+ resolve();
+ } else {
+ reject();
+ }
+ };
+
+ worker.onerror = function (e) {
+ reject();
+ };
+ });
+ },
+ async _ => {
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+ }
+);
+
+AntiTracking.runTestInNormalAndPrivateMode(
+ "BroadcastChannel and Storage Access API",
+ async _ => {
+ /* import-globals-from storageAccessAPIHelpers.js */
+ await noStorageAccessInitially();
+
+ try {
+ new BroadcastChannel("hello");
+ ok(false, "BroadcastChannel cannot be used!");
+ } catch (e) {
+ ok(true, "BroadcastChannel cannot be used!");
+ is(e.name, "SecurityError", "We want a security error message.");
+ }
+
+ /* import-globals-from storageAccessAPIHelpers.js */
+ await callRequestStorageAccess();
+
+ let effectiveCookieBehavior = SpecialPowers.isContentWindowPrivate(window)
+ ? SpecialPowers.Services.prefs.getIntPref(
+ "network.cookie.cookieBehavior.pbmode"
+ )
+ : SpecialPowers.Services.prefs.getIntPref(
+ "network.cookie.cookieBehavior"
+ );
+
+ if (
+ [
+ SpecialPowers.Ci.nsICookieService.BEHAVIOR_REJECT,
+ SpecialPowers.Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN,
+ ].includes(effectiveCookieBehavior)
+ ) {
+ try {
+ new BroadcastChannel("hello");
+ ok(false, "BroadcastChannel cannot be used!");
+ } catch (e) {
+ ok(true, "BroadcastChannel cannot be used!");
+ is(e.name, "SecurityError", "We want a security error message.");
+ }
+ } else {
+ new BroadcastChannel("hello");
+ ok(true, "BroadcastChannel can be used");
+ }
+ },
+ async _ => {
+ /* import-globals-from storageAccessAPIHelpers.js */
+ await hasStorageAccessInitially();
+
+ new BroadcastChannel("hello");
+ ok(true, "BroadcastChanneli can be used");
+
+ /* import-globals-from storageAccessAPIHelpers.js */
+ await callRequestStorageAccess();
+
+ new BroadcastChannel("hello");
+ ok(true, "BroadcastChannel can be used");
+ },
+ async _ => {
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+ },
+ null,
+ false,
+ false
+);
+
+AntiTracking.runTestInNormalAndPrivateMode(
+ "BroadcastChannel in workers and Storage Access API",
+ async _ => {
+ function blockingCode() {
+ try {
+ new BroadcastChannel("hello");
+ postMessage(false);
+ } catch (e) {
+ postMessage(e.name == "SecurityError");
+ }
+ }
+ function nonBlockingCode() {
+ new BroadcastChannel("hello");
+ postMessage(true);
+ }
+
+ /* import-globals-from storageAccessAPIHelpers.js */
+ await noStorageAccessInitially();
+
+ let blob = new Blob([blockingCode.toString() + "; blockingCode();"]);
+ ok(blob, "Blob has been created");
+
+ let blobURL = URL.createObjectURL(blob);
+ ok(blobURL, "Blob URL has been created");
+
+ let worker = new Worker(blobURL);
+ ok(worker, "Worker has been created");
+
+ await new Promise((resolve, reject) => {
+ worker.onmessage = function (e) {
+ if (e.data) {
+ resolve();
+ } else {
+ reject();
+ }
+ };
+
+ worker.onerror = function (e) {
+ reject();
+ };
+ });
+
+ /* import-globals-from storageAccessAPIHelpers.js */
+ await callRequestStorageAccess();
+
+ let effectiveCookieBehavior = SpecialPowers.isContentWindowPrivate(window)
+ ? SpecialPowers.Services.prefs.getIntPref(
+ "network.cookie.cookieBehavior.pbmode"
+ )
+ : SpecialPowers.Services.prefs.getIntPref(
+ "network.cookie.cookieBehavior"
+ );
+
+ if (
+ [
+ SpecialPowers.Ci.nsICookieService.BEHAVIOR_REJECT,
+ SpecialPowers.Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN,
+ ].includes(effectiveCookieBehavior)
+ ) {
+ blob = new Blob([blockingCode.toString() + "; blockingCode();"]);
+ } else {
+ blob = new Blob([nonBlockingCode.toString() + "; nonBlockingCode();"]);
+ }
+
+ ok(blob, "Blob has been created");
+
+ blobURL = URL.createObjectURL(blob);
+ ok(blobURL, "Blob URL has been created");
+
+ worker = new Worker(blobURL);
+ ok(worker, "Worker has been created");
+
+ await new Promise((resolve, reject) => {
+ worker.onmessage = function (e) {
+ if (e.data) {
+ resolve();
+ } else {
+ reject();
+ }
+ };
+
+ worker.onerror = function (e) {
+ reject();
+ };
+ });
+ },
+ async _ => {
+ function nonBlockingCode() {
+ new BroadcastChannel("hello");
+ postMessage(true);
+ }
+
+ /* import-globals-from storageAccessAPIHelpers.js */
+ await hasStorageAccessInitially();
+
+ let blob = new Blob([nonBlockingCode.toString() + "; nonBlockingCode();"]);
+ ok(blob, "Blob has been created");
+
+ let blobURL = URL.createObjectURL(blob);
+ ok(blobURL, "Blob URL has been created");
+
+ let worker = new Worker(blobURL);
+ ok(worker, "Worker has been created");
+
+ await new Promise((resolve, reject) => {
+ worker.onmessage = function (e) {
+ if (e.data) {
+ resolve();
+ } else {
+ reject();
+ }
+ };
+
+ worker.onerror = function (e) {
+ reject();
+ };
+ });
+
+ /* import-globals-from storageAccessAPIHelpers.js */
+ await callRequestStorageAccess();
+
+ // For non-tracking windows, calling the API is a no-op
+
+ worker = new Worker(blobURL);
+ ok(worker, "Worker has been created");
+
+ await new Promise((resolve, reject) => {
+ worker.onmessage = function (e) {
+ if (e.data) {
+ resolve();
+ } else {
+ reject();
+ }
+ };
+
+ worker.onerror = function (e) {
+ reject();
+ };
+ });
+ },
+ async _ => {
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+ },
+ null,
+ false,
+ false
+);
diff --git a/toolkit/components/antitracking/test/browser/browser_blockingNoOpener.js b/toolkit/components/antitracking/test/browser/browser_blockingNoOpener.js
new file mode 100644
index 0000000000..ba75454e18
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_blockingNoOpener.js
@@ -0,0 +1,41 @@
+gFeatures = "noopener";
+
+AntiTracking.runTestInNormalAndPrivateMode(
+ "Blocking in the case of noopener windows",
+ async _ => {
+ try {
+ localStorage.foo = 42;
+ ok(false, "LocalStorage cannot be used!");
+ } catch (e) {
+ ok(true, "LocalStorage cannot be used!");
+ is(e.name, "SecurityError", "We want a security error message.");
+ }
+ },
+ async phase => {
+ switch (phase) {
+ case 1:
+ localStorage.foo = 42;
+ ok(true, "LocalStorage is allowed");
+ break;
+ case 2:
+ try {
+ localStorage.foo = 42;
+ ok(false, "LocalStorage cannot be used!");
+ } catch (e) {
+ ok(true, "LocalStorage cannot be used!");
+ is(e.name, "SecurityError", "We want a security error message.");
+ }
+ break;
+ }
+ },
+ async _ => {
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+ },
+ null,
+ true,
+ false
+);
diff --git a/toolkit/components/antitracking/test/browser/browser_blockingServiceWorkers.js b/toolkit/components/antitracking/test/browser/browser_blockingServiceWorkers.js
new file mode 100644
index 0000000000..c61f85d69f
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_blockingServiceWorkers.js
@@ -0,0 +1,30 @@
+AntiTracking.runTest(
+ "ServiceWorkers",
+ async _ => {
+ await navigator.serviceWorker
+ .register("empty.js")
+ .then(
+ _ => {
+ ok(false, "ServiceWorker cannot be used!");
+ },
+ _ => {
+ ok(true, "ServiceWorker cannot be used!");
+ }
+ )
+ .catch(e => ok(false, "Promise rejected: " + e));
+ },
+ null,
+ async _ => {
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+ },
+ [
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ["dom.ipc.processCount", 1],
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ ]
+);
diff --git a/toolkit/components/antitracking/test/browser/browser_blockingServiceWorkersStorageAccessAPI.js b/toolkit/components/antitracking/test/browser/browser_blockingServiceWorkersStorageAccessAPI.js
new file mode 100644
index 0000000000..a7676bf939
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_blockingServiceWorkersStorageAccessAPI.js
@@ -0,0 +1,133 @@
+/* import-globals-from antitracking_head.js */
+
+requestLongerTimeout(2);
+
+AntiTracking.runTest(
+ "ServiceWorkers and Storage Access API",
+ async _ => {
+ /* import-globals-from storageAccessAPIHelpers.js */
+ await noStorageAccessInitially();
+
+ await navigator.serviceWorker
+ .register("empty.js")
+ .then(
+ _ => {
+ ok(false, "ServiceWorker cannot be used!");
+ },
+ _ => {
+ ok(true, "ServiceWorker cannot be used!");
+ }
+ )
+ .catch(e => ok(false, "Promise rejected: " + e));
+
+ /* import-globals-from storageAccessAPIHelpers.js */
+ await callRequestStorageAccess();
+
+ let effectiveCookieBehavior = SpecialPowers.isContentWindowPrivate(window)
+ ? SpecialPowers.Services.prefs.getIntPref(
+ "network.cookie.cookieBehavior.pbmode"
+ )
+ : SpecialPowers.Services.prefs.getIntPref(
+ "network.cookie.cookieBehavior"
+ );
+
+ if (
+ [
+ SpecialPowers.Ci.nsICookieService.BEHAVIOR_REJECT,
+ SpecialPowers.Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN,
+ ].includes(effectiveCookieBehavior)
+ ) {
+ await navigator.serviceWorker
+ .register("empty.js")
+ .then(
+ _ => {
+ ok(false, "ServiceWorker cannot be used!");
+ },
+ _ => {
+ ok(true, "ServiceWorker cannot be used!");
+ }
+ )
+ .catch(e => ok(false, "Promise rejected: " + e));
+ } else {
+ await navigator.serviceWorker
+ .register("empty.js")
+ .then(
+ reg => {
+ ok(true, "ServiceWorker can be used!");
+ return reg;
+ },
+ _ => {
+ ok(false, "ServiceWorker cannot be used! " + _);
+ }
+ )
+ .then(
+ reg => reg.unregister(),
+ _ => {
+ ok(false, "unregister failed");
+ }
+ )
+ .catch(e => ok(false, "Promise rejected: " + e));
+ }
+ },
+ async _ => {
+ /* import-globals-from storageAccessAPIHelpers.js */
+ await hasStorageAccessInitially();
+
+ await navigator.serviceWorker
+ .register("empty.js")
+ .then(
+ reg => {
+ ok(true, "ServiceWorker can be used!");
+ return reg;
+ },
+ _ => {
+ ok(false, "ServiceWorker cannot be used!");
+ }
+ )
+ .then(
+ reg => reg.unregister(),
+ _ => {
+ ok(false, "unregister failed");
+ }
+ )
+ .catch(e => ok(false, "Promise rejected: " + e));
+
+ /* import-globals-from storageAccessAPIHelpers.js */
+ await callRequestStorageAccess();
+
+ // For non-tracking windows, calling the API is a no-op
+ await navigator.serviceWorker
+ .register("empty.js")
+ .then(
+ reg => {
+ ok(true, "ServiceWorker can be used!");
+ return reg;
+ },
+ _ => {
+ ok(false, "ServiceWorker cannot be used!");
+ }
+ )
+ .then(
+ reg => reg.unregister(),
+ _ => {
+ ok(false, "unregister failed");
+ }
+ )
+ .catch(e => ok(false, "Promise rejected: " + e));
+ },
+ async _ => {
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+ },
+ [
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ["dom.ipc.processCount", 1],
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ ],
+ false,
+ false
+);
diff --git a/toolkit/components/antitracking/test/browser/browser_blockingSessionStorage.js b/toolkit/components/antitracking/test/browser/browser_blockingSessionStorage.js
new file mode 100644
index 0000000000..25a926ed3f
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_blockingSessionStorage.js
@@ -0,0 +1,126 @@
+requestLongerTimeout(6);
+
+AntiTracking.runTestInNormalAndPrivateMode(
+ "sessionStorage",
+ async _ => {
+ let effectiveCookieBehavior = SpecialPowers.isContentWindowPrivate(window)
+ ? SpecialPowers.Services.prefs.getIntPref(
+ "network.cookie.cookieBehavior.pbmode"
+ )
+ : SpecialPowers.Services.prefs.getIntPref(
+ "network.cookie.cookieBehavior"
+ );
+
+ let shouldThrow = [
+ SpecialPowers.Ci.nsICookieService.BEHAVIOR_REJECT,
+ ].includes(effectiveCookieBehavior);
+
+ let hasThrown;
+ try {
+ sessionStorage.foo = 42;
+ hasThrown = false;
+ } catch (e) {
+ hasThrown = true;
+ is(e.name, "SecurityError", "We want a security error message.");
+ }
+
+ is(
+ hasThrown,
+ shouldThrow,
+ "SessionStorage show thrown only if cookieBehavior is REJECT"
+ );
+ },
+ async _ => {
+ sessionStorage.foo = 42;
+ ok(true, "SessionStorage is always allowed");
+ },
+ async _ => {
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+ },
+ [],
+ true,
+ true
+);
+
+AntiTracking.runTestInNormalAndPrivateMode(
+ "sessionStorage and Storage Access API",
+ async _ => {
+ /* import-globals-from storageAccessAPIHelpers.js */
+ await noStorageAccessInitially();
+
+ let effectiveCookieBehavior = SpecialPowers.isContentWindowPrivate(window)
+ ? SpecialPowers.Services.prefs.getIntPref(
+ "network.cookie.cookieBehavior.pbmode"
+ )
+ : SpecialPowers.Services.prefs.getIntPref(
+ "network.cookie.cookieBehavior"
+ );
+
+ let shouldThrow = [
+ SpecialPowers.Ci.nsICookieService.BEHAVIOR_REJECT,
+ ].includes(effectiveCookieBehavior);
+
+ let hasThrown;
+ try {
+ sessionStorage.foo = 42;
+ hasThrown = false;
+ } catch (e) {
+ hasThrown = true;
+ is(e.name, "SecurityError", "We want a security error message.");
+ }
+
+ is(
+ hasThrown,
+ shouldThrow,
+ "SessionStorage show thrown only if cookieBehavior is REJECT"
+ );
+
+ /* import-globals-from storageAccessAPIHelpers.js */
+ await callRequestStorageAccess();
+
+ try {
+ sessionStorage.foo = 42;
+ hasThrown = false;
+ } catch (e) {
+ hasThrown = true;
+ is(e.name, "SecurityError", "We want a security error message.");
+ }
+
+ is(
+ hasThrown,
+ shouldThrow,
+ "SessionStorage show thrown only if cookieBehavior is REJECT"
+ );
+ },
+ async _ => {
+ /* import-globals-from storageAccessAPIHelpers.js */
+ await hasStorageAccessInitially();
+
+ sessionStorage.foo = 42;
+ ok(true, "SessionStorage is always allowed");
+
+ /* import-globals-from storageAccessAPIHelpers.js */
+ await callRequestStorageAccess();
+
+ // For non-tracking windows, calling the API is a no-op
+ sessionStorage.foo = 42;
+ ok(
+ true,
+ "SessionStorage is allowed after calling the storage access API too"
+ );
+ },
+ async _ => {
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+ },
+ null,
+ false,
+ false
+);
diff --git a/toolkit/components/antitracking/test/browser/browser_blockingSharedWorkers.js b/toolkit/components/antitracking/test/browser/browser_blockingSharedWorkers.js
new file mode 100644
index 0000000000..0daffc4565
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_blockingSharedWorkers.js
@@ -0,0 +1,94 @@
+requestLongerTimeout(4);
+
+AntiTracking.runTestInNormalAndPrivateMode(
+ "SharedWorkers",
+ async _ => {
+ try {
+ new SharedWorker("a.js", "foo");
+ ok(false, "SharedWorker cannot be used!");
+ } catch (e) {
+ ok(true, "SharedWorker cannot be used!");
+ is(e.name, "SecurityError", "We want a security error message.");
+ }
+ },
+ async _ => {
+ new SharedWorker("a.js", "foo");
+ ok(true, "SharedWorker is allowed");
+ },
+ async _ => {
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+ }
+);
+
+AntiTracking.runTestInNormalAndPrivateMode(
+ "SharedWorkers and Storage Access API",
+ async _ => {
+ /* import-globals-from storageAccessAPIHelpers.js */
+ await noStorageAccessInitially();
+
+ try {
+ new SharedWorker("a.js", "foo");
+ ok(false, "SharedWorker cannot be used!");
+ } catch (e) {
+ ok(true, "SharedWorker cannot be used!");
+ is(e.name, "SecurityError", "We want a security error message.");
+ }
+
+ /* import-globals-from storageAccessAPIHelpers.js */
+ await callRequestStorageAccess();
+
+ let effectiveCookieBehavior = SpecialPowers.isContentWindowPrivate(window)
+ ? SpecialPowers.Services.prefs.getIntPref(
+ "network.cookie.cookieBehavior.pbmode"
+ )
+ : SpecialPowers.Services.prefs.getIntPref(
+ "network.cookie.cookieBehavior"
+ );
+
+ if (
+ [
+ SpecialPowers.Ci.nsICookieService.BEHAVIOR_REJECT,
+ SpecialPowers.Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN,
+ ].includes(effectiveCookieBehavior)
+ ) {
+ try {
+ new SharedWorker("a.js", "foo");
+ ok(false, "SharedWorker cannot be used!");
+ } catch (e) {
+ ok(true, "SharedWorker cannot be used!");
+ is(e.name, "SecurityError", "We want a security error message.");
+ }
+ } else {
+ new SharedWorker("a.js", "foo");
+ ok(true, "SharedWorker is allowed");
+ }
+ },
+ async _ => {
+ /* import-globals-from storageAccessAPIHelpers.js */
+ await hasStorageAccessInitially();
+
+ new SharedWorker("a.js", "foo");
+ ok(true, "SharedWorker is allowed");
+
+ /* import-globals-from storageAccessAPIHelpers.js */
+ await callRequestStorageAccess();
+
+ // For non-tracking windows, calling the API is a no-op
+ new SharedWorker("a.js", "bar");
+ ok(true, "SharedWorker is allowed");
+ },
+ async _ => {
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+ },
+ null,
+ false,
+ false
+);
diff --git a/toolkit/components/antitracking/test/browser/browser_contentBlockingAllowListPrincipal.js b/toolkit/components/antitracking/test/browser/browser_contentBlockingAllowListPrincipal.js
new file mode 100644
index 0000000000..a9e2ac6fce
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_contentBlockingAllowListPrincipal.js
@@ -0,0 +1,237 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_SANDBOX_URL =
+ "https://example.com/browser/toolkit/components/antitracking/test/browser/sandboxed.html";
+
+/**
+ * Tests the contentBlockingAllowListPrincipal.
+ * @param {Browser} browser - Browser to test.
+ * @param {("content"|"system")} type - Expected principal type.
+ * @param {String} [origin] - Expected origin of principal. Defaults to the
+ * origin of the browsers content principal.
+ */
+function checkAllowListPrincipal(
+ browser,
+ type,
+ origin = browser.contentPrincipal.origin
+) {
+ let principal =
+ browser.browsingContext.currentWindowGlobal
+ .contentBlockingAllowListPrincipal;
+ ok(principal, "Principal is set");
+
+ if (type == "content") {
+ ok(principal.isContentPrincipal, "Is content principal");
+
+ ok(
+ principal.schemeIs("https"),
+ "allowlist content principal must have https scheme"
+ );
+ } else if (type == "system") {
+ ok(principal.isSystemPrincipal, "Is system principal");
+ } else {
+ throw new Error("Unexpected principal type");
+ }
+
+ is(principal.origin, origin, "Correct origin");
+}
+
+/**
+ * Runs a given test in a normal window and in a private browsing window.
+ * @param {String} initialUrl - URL to load in the initial test tab.
+ * @param {Function} testCallback - Test function to run in both windows.
+ */
+async function runTestInNormalAndPrivateMode(initialUrl, testCallback) {
+ for (let i = 0; i < 2; i++) {
+ let isPrivateBrowsing = !!i;
+ info("Running test. Private browsing: " + !!i);
+ let win = await BrowserTestUtils.openNewBrowserWindow({
+ private: isPrivateBrowsing,
+ });
+ let tab = BrowserTestUtils.addTab(win.gBrowser, initialUrl);
+ let browser = tab.linkedBrowser;
+
+ await BrowserTestUtils.browserLoaded(browser);
+
+ await testCallback(browser, isPrivateBrowsing);
+
+ await BrowserTestUtils.closeWindow(win);
+ }
+}
+
+/**
+ * Creates an iframe in the passed browser and waits for it to load.
+ * @param {Browser} browser - Browser to create the frame in.
+ * @param {String} src - Frame source url.
+ * @param {String} id - Frame id.
+ * @param {String} [sandboxAttr] - Optional list of sandbox attributes to set
+ * for the iframe. Defaults to no sandbox.
+ * @returns {Promise} - Resolves once the frame has loaded.
+ */
+function createFrame(browser, src, id, sandboxAttr) {
+ return SpecialPowers.spawn(
+ browser,
+ [{ page: src, frameId: id, sandboxAttr }],
+ async function (obj) {
+ await new content.Promise(resolve => {
+ let frame = content.document.createElement("iframe");
+ frame.src = obj.page;
+ frame.id = obj.frameId;
+ if (obj.sandboxAttr) {
+ frame.setAttribute("sandbox", obj.sandboxAttr);
+ }
+ frame.addEventListener("load", resolve, { once: true });
+ content.document.body.appendChild(frame);
+ });
+ }
+ );
+}
+
+add_task(async setup => {
+ // Disable heuristics. We don't need them and if enabled the resulting
+ // telemetry can race with the telemetry in the next test.
+ // See Bug 1686836, Bug 1686894.
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["privacy.restrict3rdpartystorage.heuristic.redirect", false],
+ ["privacy.restrict3rdpartystorage.heuristic.recently_visited", false],
+ ["privacy.restrict3rdpartystorage.heuristic.window_open", false],
+ ["dom.security.https_first_pbm", false],
+ ],
+ });
+});
+
+/**
+ * Test that we get the correct allow list principal which matches the content
+ * principal for an https site.
+ */
+add_task(async test_contentPrincipalHTTPS => {
+ await runTestInNormalAndPrivateMode("https://example.com", browser => {
+ checkAllowListPrincipal(browser, "content");
+ });
+});
+
+/**
+ * Tests that the scheme of the allowlist principal is HTTPS, even though the
+ * site is loaded via HTTP.
+ */
+add_task(async test_contentPrincipalHTTP => {
+ await runTestInNormalAndPrivateMode(
+ "http://example.net",
+ (browser, isPrivateBrowsing) => {
+ checkAllowListPrincipal(
+ browser,
+ "content",
+ "https://example.net" +
+ (isPrivateBrowsing ? "^privateBrowsingId=1" : "")
+ );
+ }
+ );
+});
+
+/**
+ * Tests that the allow list principal is a system principal for the preferences
+ * about site.
+ */
+add_task(async test_systemPrincipal => {
+ await runTestInNormalAndPrivateMode("about:preferences", browser => {
+ checkAllowListPrincipal(browser, "system");
+ });
+});
+
+/**
+ * Tests that we get a valid content principal for top level sandboxed pages,
+ * and not the document principal which is a null principal.
+ */
+add_task(async test_TopLevelSandbox => {
+ await runTestInNormalAndPrivateMode(
+ TEST_SANDBOX_URL,
+ (browser, isPrivateBrowsing) => {
+ ok(
+ browser.contentPrincipal.isNullPrincipal,
+ "Top level sandboxed page should have null principal"
+ );
+ checkAllowListPrincipal(
+ browser,
+ "content",
+ "https://example.com" +
+ (isPrivateBrowsing ? "^privateBrowsingId=1" : "")
+ );
+ }
+ );
+});
+
+/**
+ * Tests that we get a valid content principal for a new tab opened via
+ * window.open.
+ */
+add_task(async test_windowOpen => {
+ await runTestInNormalAndPrivateMode("https://example.com", async browser => {
+ checkAllowListPrincipal(browser, "content");
+
+ let promiseTabOpened = BrowserTestUtils.waitForNewTab(
+ browser.ownerGlobal.gBrowser,
+ "https://example.org/",
+ true
+ );
+
+ // Call window.open from iframe.
+ await SpecialPowers.spawn(browser, [], async function () {
+ content.open("https://example.org/");
+ });
+
+ let tab = await promiseTabOpened;
+
+ checkAllowListPrincipal(tab.linkedBrowser, "content");
+
+ BrowserTestUtils.removeTab(tab);
+ });
+});
+
+/**
+ * Tests that we get a valid content principal for a new tab opened via
+ * window.open from a sandboxed iframe.
+ */
+add_task(async test_windowOpenFromSandboxedFrame => {
+ await runTestInNormalAndPrivateMode(
+ "https://example.com",
+ async (browser, isPrivateBrowsing) => {
+ checkAllowListPrincipal(browser, "content");
+
+ // Create sandboxed iframe, allow popups.
+ await createFrame(
+ browser,
+ "https://example.com",
+ "sandboxedIframe",
+ "allow-popups"
+ );
+ // Iframe BC is the only child of the test browser.
+ let [frameBrowsingContext] = browser.browsingContext.children;
+
+ let promiseTabOpened = BrowserTestUtils.waitForNewTab(
+ browser.ownerGlobal.gBrowser,
+ "https://example.org/",
+ true
+ );
+
+ // Call window.open from iframe.
+ await SpecialPowers.spawn(frameBrowsingContext, [], async function () {
+ content.open("https://example.org/");
+ });
+
+ let tab = await promiseTabOpened;
+
+ checkAllowListPrincipal(
+ tab.linkedBrowser,
+ "content",
+ "https://example.org" +
+ (isPrivateBrowsing ? "^privateBrowsingId=1" : "")
+ );
+
+ BrowserTestUtils.removeTab(tab);
+ }
+ );
+});
diff --git a/toolkit/components/antitracking/test/browser/browser_contentBlockingTelemetry.js b/toolkit/components/antitracking/test/browser/browser_contentBlockingTelemetry.js
new file mode 100644
index 0000000000..dc7bf0c80e
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_contentBlockingTelemetry.js
@@ -0,0 +1,404 @@
+/**
+ * Bug 1668199 - Testing the content blocking telemetry.
+ */
+
+"use strict";
+
+const { TelemetryTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TelemetryTestUtils.sys.mjs"
+);
+
+const LABEL_STORAGE_GRANTED = 0;
+const LABEL_STORAGE_ACCESS_API = 1;
+const LABEL_OPENER_AFTER_UI = 2;
+const LABEL_OPENER = 3;
+const LABEL_REDIRECT = 4;
+
+function clearTelemetry() {
+ Services.telemetry.getSnapshotForHistograms("main", true /* clear */);
+ Services.telemetry.getHistogramById("STORAGE_ACCESS_REMAINING_DAYS").clear();
+}
+
+const expectedExpiredDays = getExpectedExpiredDaysFromPref(
+ "privacy.restrict3rdpartystorage.expiration"
+);
+const expectedExpiredDaysRedirect = getExpectedExpiredDaysFromPref(
+ "privacy.restrict3rdpartystorage.expiration_redirect"
+);
+
+function getExpectedExpiredDaysFromPref(pref) {
+ let expiredSecond = Services.prefs.getIntPref(pref);
+
+ // This is unlikely to happen, but just in case.
+ if (expiredSecond <= 0) {
+ return 0;
+ }
+
+ // We need to subtract one second from the expect expired second because there
+ // will be a short delay between the time we add the permission and the time
+ // we record the telemetry. Subtracting one can help us to get the correct
+ // expected expired days.
+ //
+ // Note that 86400 is seconds in one day.
+ return Math.trunc((expiredSecond - 1) / 86400);
+}
+
+async function testTelemetry(
+ aProbeInParent,
+ aExpectedCnt,
+ aLabel,
+ aExpectedIdx
+) {
+ info("Trigger the 'idle-daily' to trigger the telemetry probe.");
+ // Synthesis a fake 'idle-daily' notification to the content blocking
+ // telemetry service.
+ let cbts = Cc["@mozilla.org/content-blocking-telemetry-service;1"].getService(
+ Ci.nsIObserver
+ );
+ cbts.observe(null, "idle-daily", null);
+
+ let storageAccessGrantedHistogram;
+
+ // Wait until the telemetry probe appears.
+ await BrowserTestUtils.waitForCondition(() => {
+ let histograms;
+ if (aProbeInParent) {
+ histograms = Services.telemetry.getSnapshotForHistograms(
+ "main",
+ false /* clear */
+ ).parent;
+ } else {
+ histograms = Services.telemetry.getSnapshotForHistograms(
+ "main",
+ false /* clear */
+ ).content;
+ }
+ storageAccessGrantedHistogram = histograms.STORAGE_ACCESS_GRANTED_COUNT;
+
+ return (
+ !!storageAccessGrantedHistogram &&
+ storageAccessGrantedHistogram.values[LABEL_STORAGE_GRANTED] ==
+ aExpectedCnt
+ );
+ });
+
+ is(
+ storageAccessGrantedHistogram.values[LABEL_STORAGE_GRANTED],
+ aExpectedCnt,
+ "There should be expected storage access granted count in telemetry."
+ );
+ is(
+ storageAccessGrantedHistogram.values[aLabel],
+ 1,
+ "There should be one reason count in telemetry."
+ );
+
+ let storageAccessRemainingDaysHistogram = Services.telemetry.getHistogramById(
+ "STORAGE_ACCESS_REMAINING_DAYS"
+ );
+
+ TelemetryTestUtils.assertHistogram(
+ storageAccessRemainingDaysHistogram,
+ aExpectedIdx,
+ 1
+ );
+
+ // Clear telemetry probes
+ clearTelemetry();
+}
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["network.cookie.cookieBehavior", BEHAVIOR_REJECT_TRACKER],
+ ["network.cookie.cookieBehavior.pbmode", BEHAVIOR_REJECT_TRACKER],
+ ["privacy.trackingprotection.annotate_channels", true],
+ [
+ "privacy.restrict3rdpartystorage.userInteractionRequiredForHosts",
+ "tracking.example.com,tracking.example.org",
+ ],
+ ["privacy.restrict3rdpartystorage.heuristic.redirect", true],
+ ["toolkit.telemetry.ipcBatchTimeout", 0],
+ ],
+ });
+
+ // Clear Telemetry probes before testing.
+ // There can be telemetry race conditions if the previous test generates
+ // content blocking telemetry.
+ // See Bug 1686836, Bug 1686894.
+ clearTelemetry();
+
+ await UrlClassifierTestUtils.addTestTrackers();
+
+ registerCleanupFunction(_ => {
+ Services.perms.removeAll();
+ });
+});
+
+add_task(async function testTelemetryForStorageAccessAPI() {
+ info("Starting testing if storage access API send telemetry probe ...");
+
+ // First, clear all permissions.
+ Services.perms.removeAll();
+
+ info("Creating a new tab");
+ let tab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE);
+ gBrowser.selectedTab = tab;
+
+ let browser = gBrowser.getBrowserForTab(tab);
+ await BrowserTestUtils.browserLoaded(browser);
+
+ info("Loading the tracking iframe and call the RequestStorageAccess.");
+ await SpecialPowers.spawn(
+ browser,
+ [
+ {
+ page: TEST_3RD_PARTY_PAGE_UI,
+ },
+ ],
+ async obj => {
+ let msg = {};
+ msg.callback = (async _ => {
+ /* import-globals-from storageAccessAPIHelpers.js */
+ await callRequestStorageAccess();
+ }).toString();
+
+ await new content.Promise(resolve => {
+ let ifr = content.document.createElement("iframe");
+ ifr.onload = function () {
+ info("Sending code to the 3rd party content");
+ ifr.contentWindow.postMessage(msg, "*");
+ };
+
+ content.addEventListener("message", function msg(event) {
+ if (event.data.type == "finish") {
+ content.removeEventListener("message", msg);
+ resolve();
+ return;
+ }
+
+ if (event.data.type == "ok") {
+ ok(event.data.what, event.data.msg);
+ return;
+ }
+
+ if (event.data.type == "info") {
+ info(event.data.msg);
+ return;
+ }
+
+ ok(false, "Unknown message");
+ });
+
+ content.document.body.appendChild(ifr);
+ ifr.src = obj.page;
+ });
+ }
+ );
+
+ info("Removing the tab");
+ BrowserTestUtils.removeTab(tab);
+
+ // The storage access permission will be expired in 29 days, so the expected
+ // index in the telemetry probe would be 29.
+ await testTelemetry(false, 1, LABEL_STORAGE_ACCESS_API, expectedExpiredDays);
+});
+
+add_task(async function testTelemetryForWindowOpenHeuristic() {
+ info("Starting testing if window open heuristic send telemetry probe ...");
+
+ // First, clear all permissions.
+ Services.perms.removeAll();
+
+ info("Creating a new tab");
+ let tab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE);
+ gBrowser.selectedTab = tab;
+
+ let browser = gBrowser.getBrowserForTab(tab);
+ await BrowserTestUtils.browserLoaded(browser);
+
+ info("Loading the tracking iframe and trigger the heuristic");
+ await SpecialPowers.spawn(
+ browser,
+ [
+ {
+ page: TEST_3RD_PARTY_PAGE_WO,
+ },
+ ],
+ async obj => {
+ let msg = {};
+ msg.blockingCallback = (async _ => {
+ /* import-globals-from storageAccessAPIHelpers.js */
+ await noStorageAccessInitially();
+ }).toString();
+
+ msg.nonBlockingCallback = (async _ => {
+ /* import-globals-from storageAccessAPIHelpers.js */
+ await hasStorageAccessInitially();
+ }).toString();
+
+ info("Checking if storage access is denied");
+ await new content.Promise(resolve => {
+ let ifr = content.document.createElement("iframe");
+ ifr.onload = function () {
+ info("Sending code to the 3rd party content");
+ ifr.contentWindow.postMessage(msg, "*");
+ };
+
+ content.addEventListener("message", function msg(event) {
+ if (event.data.type == "finish") {
+ content.removeEventListener("message", msg);
+ resolve();
+ return;
+ }
+
+ if (event.data.type == "ok") {
+ ok(event.data.what, event.data.msg);
+ return;
+ }
+
+ if (event.data.type == "info") {
+ info(event.data.msg);
+ return;
+ }
+
+ ok(false, "Unknown message");
+ });
+
+ content.document.body.appendChild(ifr);
+ ifr.src = obj.page;
+ });
+ }
+ );
+
+ info("Removing the tab");
+ BrowserTestUtils.removeTab(tab);
+
+ // The storage access permission will be expired in 29 days, so the expected
+ // index in the telemetry probe would be 29.
+ await testTelemetry(false, 1, LABEL_OPENER, expectedExpiredDays);
+});
+
+add_task(async function testTelemetryForUserInteractionHeuristic() {
+ info(
+ "Starting testing if UserInteraction heuristic send telemetry probe ..."
+ );
+
+ // First, clear all permissions.
+ Services.perms.removeAll();
+
+ info("Creating a new tab");
+ let tab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE);
+ gBrowser.selectedTab = tab;
+
+ let browser = gBrowser.getBrowserForTab(tab);
+ await BrowserTestUtils.browserLoaded(browser);
+
+ info("Interact with the tracker in top-level.");
+ await AntiTracking.interactWithTracker();
+
+ info("Loading the tracking iframe and trigger the heuristic");
+ await SpecialPowers.spawn(
+ browser,
+ [
+ {
+ page: TEST_3RD_PARTY_PAGE_UI,
+ popup: TEST_POPUP_PAGE,
+ },
+ ],
+ async obj => {
+ let msg = {};
+ msg.blockingCallback = (async _ => {
+ await noStorageAccessInitially();
+ }).toString();
+
+ msg.nonBlockingCallback = (async _ => {
+ /* import-globals-from storageAccessAPIHelpers.js */
+ await hasStorageAccessInitially();
+ }).toString();
+
+ let ifr = content.document.createElement("iframe");
+ let loading = new content.Promise(resolve => {
+ ifr.onload = resolve;
+ });
+ content.document.body.appendChild(ifr);
+ ifr.src = obj.page;
+ await loading;
+
+ info("Opening a window from the iframe.");
+ await SpecialPowers.spawn(ifr, [obj.popup], async popup => {
+ let windowClosed = new content.Promise(resolve => {
+ Services.ww.registerNotification(function notification(
+ aSubject,
+ aTopic,
+ aData
+ ) {
+ // We need to check the document URI here as well for the same
+ // reason above.
+ if (
+ aTopic == "domwindowclosed" &&
+ aSubject.document.documentURI ==
+ "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/3rdPartyOpenUI.html"
+ ) {
+ Services.ww.unregisterNotification(notification);
+ resolve();
+ }
+ });
+ });
+
+ content.open(popup);
+
+ info("Let's wait for the window to be closed");
+ await windowClosed;
+ });
+ }
+ );
+
+ info("Removing the tab");
+ BrowserTestUtils.removeTab(tab);
+
+ // The storage access permission will be expired in 29 days, so the expected
+ // index in the telemetry probe would be 29.
+ //
+ // Note that the expected count here is 2. It's because the opener heuristic
+ // will also be triggered when triggered UserInteraction Heuristic.
+ await testTelemetry(false, 2, LABEL_OPENER_AFTER_UI, expectedExpiredDays);
+});
+
+add_task(async function testTelemetryForRedirectHeuristic() {
+ info("Starting testing if redirect heuristic send telemetry probe ...");
+
+ const TEST_TRACKING_PAGE = TEST_3RD_PARTY_DOMAIN + TEST_PATH + "page.html";
+ const TEST_REDIRECT_PAGE =
+ TEST_3RD_PARTY_DOMAIN + TEST_PATH + "redirect.sjs?" + TEST_TOP_PAGE;
+
+ // First, clear all permissions.
+ Services.perms.removeAll();
+
+ info("Creating a new tab");
+ let tab = BrowserTestUtils.addTab(gBrowser, TEST_TRACKING_PAGE);
+ gBrowser.selectedTab = tab;
+
+ let browser = gBrowser.getBrowserForTab(tab);
+ await BrowserTestUtils.browserLoaded(browser);
+
+ info("Loading the tracking page and trigger the redirect.");
+ SpecialPowers.spawn(browser, [TEST_REDIRECT_PAGE], async url => {
+ content.document.userInteractionForTesting();
+
+ let link = content.document.createElement("a");
+ link.appendChild(content.document.createTextNode("click me!"));
+ link.href = url;
+ content.document.body.appendChild(link);
+ link.click();
+ });
+
+ await BrowserTestUtils.browserLoaded(browser, false, TEST_TOP_PAGE);
+
+ info("Removing the tab");
+ BrowserTestUtils.removeTab(tab);
+
+ // We would only grant the storage permission for 29 days for the redirect
+ // heuristic, so the expected index in the telemetry probe would be 29.
+ await testTelemetry(true, 1, LABEL_REDIRECT, expectedExpiredDaysRedirect);
+});
diff --git a/toolkit/components/antitracking/test/browser/browser_cookieBetweenTabs.js b/toolkit/components/antitracking/test/browser/browser_cookieBetweenTabs.js
new file mode 100644
index 0000000000..635f145d46
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_cookieBetweenTabs.js
@@ -0,0 +1,59 @@
+add_task(async function () {
+ await SpecialPowers.flushPrefEnv();
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.storage_access.enabled", true],
+ ["network.cookie.cookieBehavior", BEHAVIOR_REJECT],
+ ["network.cookie.cookieBehavior.pbmode", BEHAVIOR_REJECT],
+ ["privacy.trackingprotection.enabled", false],
+ ["privacy.trackingprotection.pbmode.enabled", false],
+ ["privacy.trackingprotection.annotate_channels", true],
+ ["dom.ipc.processCount", 4],
+ ],
+ });
+
+ info("First tab opened");
+ let tab = BrowserTestUtils.addTab(
+ gBrowser,
+ TEST_DOMAIN + TEST_PATH + "empty.html"
+ );
+ gBrowser.selectedTab = tab;
+
+ let browser = gBrowser.getBrowserForTab(tab);
+ await BrowserTestUtils.browserLoaded(browser);
+
+ info("Disabling content blocking for this page");
+ gProtectionsHandler.disableForCurrentPage();
+
+ await BrowserTestUtils.browserLoaded(browser);
+
+ await SpecialPowers.spawn(browser, [], async _ => {
+ is(content.document.cookie, "", "No cookie set");
+ content.document.cookie = "a=b";
+ is(content.document.cookie, "a=b", "Cookie set");
+ });
+
+ info("Second tab opened");
+ let tab2 = BrowserTestUtils.addTab(
+ gBrowser,
+ TEST_DOMAIN + TEST_PATH + "empty.html"
+ );
+ gBrowser.selectedTab = tab2;
+
+ let browser2 = gBrowser.getBrowserForTab(tab2);
+ await BrowserTestUtils.browserLoaded(browser2);
+
+ await SpecialPowers.spawn(browser2, [], async _ => {
+ is(content.document.cookie, "a=b", "Cookie set");
+ });
+
+ info("Removing tabs");
+ BrowserTestUtils.removeTab(tab);
+ BrowserTestUtils.removeTab(tab2);
+
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+});
diff --git a/toolkit/components/antitracking/test/browser/browser_denyPermissionForTracker.js b/toolkit/components/antitracking/test/browser/browser_denyPermissionForTracker.js
new file mode 100644
index 0000000000..cd19fa4466
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_denyPermissionForTracker.js
@@ -0,0 +1,64 @@
+// This test works by setting up an exception for the tracker domain, which
+// disables all the anti-tracking tests.
+
+add_task(async _ => {
+ PermissionTestUtils.add(
+ "https://tracking.example.org",
+ "cookie",
+ Services.perms.DENY_ACTION
+ );
+ PermissionTestUtils.add(
+ "https://tracking.example.com",
+ "cookie",
+ Services.perms.DENY_ACTION
+ );
+ // Grant interaction permission so we can directly call
+ // requestStorageAccess from the tracker.
+ PermissionTestUtils.add(
+ "https://tracking.example.org",
+ "storageAccessAPI",
+ Services.perms.ALLOW_ACTION
+ );
+
+ registerCleanupFunction(_ => {
+ Services.perms.removeAll();
+ });
+});
+
+AntiTracking._createTask({
+ name: "Test that we do honour a cookie permission for nested windows",
+ cookieBehavior: BEHAVIOR_REJECT_TRACKER,
+ blockingByContentBlockingRTUI: true,
+ allowList: false,
+ callback: async _ => {
+ document.cookie = "name=value";
+ ok(document.cookie == "", "All is blocked");
+
+ // requestStorageAccess should reject
+ SpecialPowers.wrap(document).notifyUserGestureActivation();
+ await document
+ .requestStorageAccess()
+ .then(() => {
+ ok(false, "Should not grant storage access");
+ })
+ .catch(() => {
+ ok(true, "Should not grant storage access");
+ });
+ SpecialPowers.wrap(document).clearUserGestureActivation();
+ },
+ extraPrefs: null,
+ expectedBlockingNotifications:
+ Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_BY_PERMISSION,
+ runInPrivateWindow: false,
+ iframeSandbox: null,
+ accessRemoval: null,
+ callbackAfterRemoval: null,
+});
+
+add_task(async _ => {
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+});
diff --git a/toolkit/components/antitracking/test/browser/browser_doublyNestedTracker.js b/toolkit/components/antitracking/test/browser/browser_doublyNestedTracker.js
new file mode 100644
index 0000000000..c15e3abd5f
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_doublyNestedTracker.js
@@ -0,0 +1,130 @@
+add_task(async function () {
+ info("Starting doubly nested tracker test");
+
+ await SpecialPowers.flushPrefEnv();
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [
+ "network.cookie.cookieBehavior",
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER,
+ ],
+ [
+ "network.cookie.cookieBehavior.pbmode",
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER,
+ ],
+ ["privacy.trackingprotection.enabled", false],
+ ["privacy.trackingprotection.pbmode.enabled", false],
+ ["privacy.trackingprotection.annotate_channels", true],
+ [
+ "privacy.restrict3rdpartystorage.userInteractionRequiredForHosts",
+ "tracking.example.com,tracking.example.org",
+ ],
+ // Bug 1617611: Fix all the tests broken by "cookies SameSite=lax by default"
+ ["network.cookie.sameSite.laxByDefault", false],
+ ],
+ });
+
+ await UrlClassifierTestUtils.addTestTrackers();
+
+ let tab = BrowserTestUtils.addTab(gBrowser, TEST_3RD_PARTY_PAGE);
+ gBrowser.selectedTab = tab;
+
+ let browser = gBrowser.getBrowserForTab(tab);
+ await BrowserTestUtils.browserLoaded(browser);
+
+ async function loadSubpage() {
+ async function runChecks() {
+ is(document.cookie, "", "No cookies for me");
+ document.cookie = "name=value";
+ is(document.cookie, "name=value", "I have the cookies!");
+ }
+
+ await new Promise(resolve => {
+ let ifr = document.createElement("iframe");
+ ifr.onload = _ => {
+ info("Sending code to the 3rd party content");
+ ifr.contentWindow.postMessage(runChecks.toString(), "*");
+ };
+
+ addEventListener("message", function msg(event) {
+ if (event.data.type == "finish") {
+ removeEventListener("message", msg);
+ resolve();
+ return;
+ }
+
+ if (event.data.type == "ok") {
+ ok(event.data.what, event.data.msg);
+ return;
+ }
+
+ if (event.data.type == "info") {
+ info(event.data.msg);
+ return;
+ }
+
+ ok(false, "Unknown message");
+ });
+
+ document.body.appendChild(ifr);
+ ifr.src =
+ "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/3rdParty.html";
+ });
+ }
+
+ // We need to use the same scheme in Fission test.
+ let testAnotherThirdPartyPage = SpecialPowers.useRemoteSubframes
+ ? TEST_ANOTHER_3RD_PARTY_PAGE_HTTPS
+ : TEST_ANOTHER_3RD_PARTY_PAGE;
+
+ await SpecialPowers.spawn(
+ browser,
+ [{ page: testAnotherThirdPartyPage, callback: loadSubpage.toString() }],
+ async function (obj) {
+ await new content.Promise(resolve => {
+ let ifr = content.document.createElement("iframe");
+ ifr.onload = _ => {
+ info("Sending code to the 3rd party content");
+ ifr.contentWindow.postMessage(obj.callback, "*");
+ };
+
+ content.addEventListener("message", function msg(event) {
+ if (event.data.type == "finish") {
+ content.removeEventListener("message", msg);
+ resolve();
+ return;
+ }
+
+ if (event.data.type == "ok") {
+ ok(event.data.what, event.data.msg);
+ return;
+ }
+
+ if (event.data.type == "info") {
+ info(event.data.msg);
+ return;
+ }
+
+ ok(false, "Unknown message");
+ });
+
+ content.document.body.appendChild(ifr);
+ ifr.src = obj.page;
+ });
+ }
+ );
+
+ BrowserTestUtils.removeTab(tab);
+
+ UrlClassifierTestUtils.cleanupTestTrackers();
+});
+
+add_task(async function () {
+ info("Cleaning up.");
+ SpecialPowers.clearUserPref("network.cookie.sameSite.laxByDefault");
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+});
diff --git a/toolkit/components/antitracking/test/browser/browser_emailtracking.js b/toolkit/components/antitracking/test/browser/browser_emailtracking.js
new file mode 100644
index 0000000000..90616aba6d
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_emailtracking.js
@@ -0,0 +1,182 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+"use strict";
+
+add_setup(async function () {
+ // Disable other tracking protection feature to avoid interfering with the
+ // current test. This also setup prefs for testing email tracking.
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["privacy.trackingprotection.enabled", false],
+ ["privacy.trackingprotection.pbmode.enabled", false],
+ ["privacy.trackingprotection.annotate_channels", false],
+ ["privacy.trackingprotection.cryptomining.enabled", false],
+ ["privacy.trackingprotection.emailtracking.enabled", true],
+ ["privacy.trackingprotection.fingerprinting.enabled", false],
+ ["privacy.trackingprotection.socialtracking.enabled", false],
+ [
+ "urlclassifier.features.emailtracking.blocklistTables",
+ "mochitest5-track-simple",
+ ],
+ ["urlclassifier.features.emailtracking.allowlistTables", ""],
+ [
+ "urlclassifier.features.emailtracking.datacollection.blocklistTables",
+ "mochitest5-track-simple",
+ ],
+ [
+ "urlclassifier.features.emailtracking.datacollection.allowlistTables",
+ "",
+ ],
+ ],
+ });
+
+ await UrlClassifierTestUtils.addTestTrackers();
+
+ registerCleanupFunction(_ => {
+ UrlClassifierTestUtils.cleanupTestTrackers();
+ });
+});
+
+function runTest(obj) {
+ add_task(async _ => {
+ info("Test: " + obj.testName);
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [
+ "privacy.trackingprotection.emailtracking.enabled",
+ obj.protectionEnabled,
+ ],
+ [
+ "privacy.trackingprotection.emailtracking.pbmode.enabled",
+ obj.protectionPrivateEnabled,
+ ],
+ ],
+ });
+
+ let win;
+
+ if (obj.testPrivate) {
+ win = await BrowserTestUtils.openNewBrowserWindow({ private: true });
+ } else {
+ win = window;
+ }
+
+ info("Creating a non-tracker top-level context");
+ let tab = BrowserTestUtils.addTab(win.gBrowser, TEST_TOP_PAGE);
+ let browser = tab.linkedBrowser;
+ await BrowserTestUtils.browserLoaded(browser);
+
+ info("The non-tracker page opens an email tracker iframe");
+ await SpecialPowers.spawn(
+ browser,
+ [
+ {
+ image: TEST_EMAIL_TRACKER_DOMAIN + TEST_PATH + "raptor.jpg",
+ script: TEST_EMAIL_TRACKER_DOMAIN + TEST_PATH + "empty.js",
+ loading: obj.loading,
+ },
+ ],
+ async obj => {
+ info("Image loading ...");
+ let loading = await new content.Promise(resolve => {
+ let image = new content.Image();
+ image.src = obj.image + "?" + Math.random();
+ image.onload = _ => resolve(true);
+ image.onerror = _ => resolve(false);
+ });
+
+ is(loading, obj.loading, "Image loading expected");
+
+ let script = content.document.createElement("script");
+ script.setAttribute("src", obj.script);
+
+ info("Script loading ...");
+ loading = await new content.Promise(resolve => {
+ script.onload = _ => resolve(true);
+ script.onerror = _ => resolve(false);
+ content.document.body.appendChild(script);
+ });
+
+ is(loading, obj.loading, "Script loading expected");
+ }
+ );
+
+ info("Checking content blocking log.");
+ let contentBlockingLog = JSON.parse(await browser.getContentBlockingLog());
+ let origins = Object.keys(contentBlockingLog);
+ is(origins.length, 1, "There should be one origin entry in the log.");
+ for (let origin of origins) {
+ is(
+ origin + "/",
+ TEST_EMAIL_TRACKER_DOMAIN,
+ "Correct tracker origin must be reported"
+ );
+ Assert.deepEqual(
+ contentBlockingLog[origin],
+ obj.expectedLogItems,
+ "Content blocking log should be as expected"
+ );
+ }
+
+ BrowserTestUtils.removeTab(tab);
+ if (obj.testPrivate) {
+ await BrowserTestUtils.closeWindow(win);
+ }
+ await SpecialPowers.popPrefEnv();
+ });
+}
+
+runTest({
+ testName:
+ "EmailTracking-dataCollection feature enabled but not considered for tracking detection.",
+ protectionEnabled: false,
+ protectionPrivateEnabled: false,
+ loading: true,
+ expectedLogItems: [
+ [
+ Ci.nsIWebProgressListener.STATE_LOADED_EMAILTRACKING_LEVEL_1_CONTENT,
+ true,
+ 2,
+ ],
+ ],
+});
+
+runTest({
+ testName: "Emailtracking-protection feature enabled.",
+ protectionEnabled: true,
+ protectionPrivateEnabled: true,
+ loading: false,
+ expectedLogItems: [
+ [Ci.nsIWebProgressListener.STATE_BLOCKED_EMAILTRACKING_CONTENT, true, 2],
+ ],
+});
+
+runTest({
+ testName:
+ "Emailtracking-protection feature enabled for private windows and doesn't block in normal windows",
+ protectionEnabled: false,
+ protectionPrivateEnabled: true,
+ loading: true,
+ expectedLogItems: [
+ [
+ Ci.nsIWebProgressListener.STATE_LOADED_EMAILTRACKING_LEVEL_1_CONTENT,
+ true,
+ 2,
+ ],
+ ],
+});
+
+runTest({
+ testName:
+ "Emailtracking-protection feature enabled for private windows and block in private windows",
+ testPrivate: true,
+ protectionEnabled: true,
+ protectionPrivateEnabled: true,
+ loading: false,
+ expectedLogItems: [
+ [Ci.nsIWebProgressListener.STATE_BLOCKED_EMAILTRACKING_CONTENT, true, 1],
+ ],
+});
diff --git a/toolkit/components/antitracking/test/browser/browser_existingCookiesForSubresources.js b/toolkit/components/antitracking/test/browser/browser_existingCookiesForSubresources.js
new file mode 100644
index 0000000000..35e9dfb169
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_existingCookiesForSubresources.js
@@ -0,0 +1,235 @@
+add_task(async function () {
+ info("Starting subResources test");
+
+ await SpecialPowers.flushPrefEnv();
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["privacy.trackingprotection.enabled", false],
+ ["privacy.trackingprotection.pbmode.enabled", false],
+ ["privacy.trackingprotection.annotate_channels", true],
+ [
+ "privacy.restrict3rdpartystorage.userInteractionRequiredForHosts",
+ "tracking.example.com,tracking.example.org",
+ ],
+ ],
+ });
+
+ await UrlClassifierTestUtils.addTestTrackers();
+
+ info("Creating a new tab");
+ let tab = BrowserTestUtils.addTab(gBrowser, TEST_3RD_PARTY_PAGE);
+ gBrowser.selectedTab = tab;
+
+ let browser = gBrowser.getBrowserForTab(tab);
+ await BrowserTestUtils.browserLoaded(browser);
+
+ info(
+ "Loading tracking scripts and tracking images before restricting 3rd party cookies"
+ );
+ await SpecialPowers.spawn(browser, [], async function () {
+ // Let's load the script twice here.
+ {
+ let src = content.document.createElement("script");
+ let p = new content.Promise(resolve => {
+ src.onload = resolve;
+ });
+ content.document.body.appendChild(src);
+ src.src =
+ "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/subResources.sjs?what=script";
+ await p;
+ }
+ {
+ let src = content.document.createElement("script");
+ let p = new content.Promise(resolve => {
+ src.onload = resolve;
+ });
+ content.document.body.appendChild(src);
+ src.src =
+ "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/subResources.sjs?what=script";
+ await p;
+ }
+
+ // Let's load an image twice here.
+ {
+ let img = content.document.createElement("img");
+ let p = new content.Promise(resolve => {
+ img.onload = resolve;
+ });
+ content.document.body.appendChild(img);
+ img.src =
+ "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/subResources.sjs?what=image";
+ await p;
+ }
+ {
+ let img = content.document.createElement("img");
+ let p = new content.Promise(resolve => {
+ img.onload = resolve;
+ });
+ content.document.body.appendChild(img);
+ img.src =
+ "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/subResources.sjs?what=image";
+ await p;
+ }
+ });
+
+ await fetch(
+ "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/subResources.sjs?result&what=image"
+ )
+ .then(r => r.text())
+ .then(text => {
+ is(text, "1", "Cookies received for images");
+ });
+
+ await fetch(
+ "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/subResources.sjs?result&what=script"
+ )
+ .then(r => r.text())
+ .then(text => {
+ is(text, "1", "Cookies received for scripts");
+ });
+
+ info("Removing the tab");
+ BrowserTestUtils.removeTab(tab);
+
+ Services.perms.removeAll();
+
+ // Now set up our prefs
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [
+ "network.cookie.cookieBehavior",
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER,
+ ],
+ [
+ "network.cookie.cookieBehavior.pbmode",
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER,
+ ],
+ [
+ "privacy.restrict3rdpartystorage.userInteractionRequiredForHosts",
+ "tracking.example.com,tracking.example.org",
+ ],
+ ],
+ });
+
+ info("Creating a new tab");
+ tab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE);
+ gBrowser.selectedTab = tab;
+
+ browser = gBrowser.getBrowserForTab(tab);
+ await BrowserTestUtils.browserLoaded(browser);
+
+ info("Creating a 3rd party content");
+ await SpecialPowers.spawn(
+ browser,
+ [{ page: TEST_3RD_PARTY_PAGE, callback: (async _ => {}).toString() }],
+ async function (obj) {
+ await new content.Promise(resolve => {
+ let ifr = content.document.createElement("iframe");
+ ifr.onload = function () {
+ info("Sending code to the 3rd party content");
+ ifr.contentWindow.postMessage(obj.callback, "*");
+ };
+
+ content.addEventListener("message", function msg(event) {
+ if (event.data.type == "finish") {
+ content.removeEventListener("message", msg);
+ resolve();
+ return;
+ }
+
+ if (event.data.type == "ok") {
+ ok(event.data.what, event.data.msg);
+ return;
+ }
+
+ if (event.data.type == "info") {
+ info(event.data.msg);
+ return;
+ }
+
+ ok(false, "Unknown message");
+ });
+
+ content.document.body.appendChild(ifr);
+ ifr.src = obj.page;
+ });
+ }
+ );
+
+ info("Loading tracking scripts and tracking images again");
+ await SpecialPowers.spawn(browser, [], async function () {
+ // Let's load the script twice here.
+ {
+ let src = content.document.createElement("script");
+ let p = new content.Promise(resolve => {
+ src.onload = resolve;
+ });
+ content.document.body.appendChild(src);
+ src.src =
+ "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/subResources.sjs?what=script";
+ await p;
+ }
+ {
+ let src = content.document.createElement("script");
+ let p = new content.Promise(resolve => {
+ src.onload = resolve;
+ });
+ content.document.body.appendChild(src);
+ src.src =
+ "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/subResources.sjs?what=script";
+ await p;
+ }
+
+ // Let's load an image twice here.
+ {
+ let img = content.document.createElement("img");
+ let p = new content.Promise(resolve => {
+ img.onload = resolve;
+ });
+ content.document.body.appendChild(img);
+ img.src =
+ "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/subResources.sjs?what=image";
+ await p;
+ }
+ {
+ let img = content.document.createElement("img");
+ let p = new content.Promise(resolve => {
+ img.onload = resolve;
+ });
+ content.document.body.appendChild(img);
+ img.src =
+ "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/subResources.sjs?what=image";
+ await p;
+ }
+ });
+
+ await fetch(
+ "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/subResources.sjs?result&what=image"
+ )
+ .then(r => r.text())
+ .then(text => {
+ is(text, "0", "No cookie received for images.");
+ });
+
+ await fetch(
+ "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/subResources.sjs?result&what=script"
+ )
+ .then(r => r.text())
+ .then(text => {
+ is(text, "0", "No cookie received received for scripts.");
+ });
+
+ info("Removing the tab");
+ BrowserTestUtils.removeTab(tab);
+
+ UrlClassifierTestUtils.cleanupTestTrackers();
+});
+
+add_task(async function () {
+ info("Cleaning up.");
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+});
diff --git a/toolkit/components/antitracking/test/browser/browser_fileUrl.js b/toolkit/components/antitracking/test/browser/browser_fileUrl.js
new file mode 100644
index 0000000000..509c143a9b
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_fileUrl.js
@@ -0,0 +1,41 @@
+/**
+ * Bug 1663192 - Testing for ensuring the top-level window in a fire url is
+ * treated as first-party.
+ */
+
+"use strict";
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["network.cookie.cookieBehavior", 1],
+ ["network.cookie.cookieBehavior.pbmode", 1],
+ ],
+ });
+});
+
+add_task(async function () {
+ let dir = getChromeDir(getResolvedURI(gTestPath));
+ dir.append("file_localStorage.html");
+ const uriString = Services.io.newFileURI(dir).spec;
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, uriString);
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [], function () {
+ let result = content.document.getElementById("result");
+
+ is(
+ result.textContent,
+ "PASS",
+ "The localStorage is accessible in top-level window"
+ );
+
+ let loadInfo = content.docShell.currentDocumentChannel.loadInfo;
+
+ ok(
+ !loadInfo.isThirdPartyContextToTopWindow,
+ "The top-level window shouldn't be third-party"
+ );
+ });
+
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/toolkit/components/antitracking/test/browser/browser_firstPartyCookieRejectionHonoursAllowList.js b/toolkit/components/antitracking/test/browser/browser_firstPartyCookieRejectionHonoursAllowList.js
new file mode 100644
index 0000000000..d3d06d2950
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_firstPartyCookieRejectionHonoursAllowList.js
@@ -0,0 +1,77 @@
+add_task(async function () {
+ info("Starting subResources test");
+
+ await SpecialPowers.flushPrefEnv();
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["network.cookie.cookieBehavior", Ci.nsICookieService.BEHAVIOR_REJECT],
+ [
+ "network.cookie.cookieBehavior.pbmode",
+ Ci.nsICookieService.BEHAVIOR_REJECT,
+ ],
+ ["privacy.trackingprotection.enabled", false],
+ ["privacy.trackingprotection.pbmode.enabled", false],
+ ["privacy.trackingprotection.annotate_channels", true],
+ ],
+ });
+
+ let tab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE);
+ gBrowser.selectedTab = tab;
+
+ let browser = gBrowser.getBrowserForTab(tab);
+ await BrowserTestUtils.browserLoaded(browser);
+
+ info("Disabling content blocking for this page");
+ gProtectionsHandler.disableForCurrentPage();
+
+ // The previous function reloads the browser, so wait for it to load again!
+ await BrowserTestUtils.browserLoaded(browser);
+
+ await SpecialPowers.spawn(browser, [], async function (obj) {
+ await new content.Promise(async resolve => {
+ let document = content.document;
+ let window = document.defaultView;
+
+ is(document.cookie, "", "No cookies for me");
+
+ await window
+ .fetch("server.sjs")
+ .then(r => r.text())
+ .then(text => {
+ is(text, "cookie-not-present", "We should not have cookies");
+ });
+
+ document.cookie = "name=value";
+ ok(document.cookie.includes("name=value"), "Some cookies for me");
+ ok(document.cookie.includes("foopy=1"), "Some cookies for me");
+
+ await window
+ .fetch("server.sjs")
+ .then(r => r.text())
+ .then(text => {
+ is(text, "cookie-present", "We should have cookies");
+ });
+
+ ok(document.cookie.length, "Some Cookies for me");
+
+ resolve();
+ });
+ });
+
+ info("Enabling content blocking for this page");
+ gProtectionsHandler.enableForCurrentPage();
+
+ // The previous function reloads the browser, so wait for it to load again!
+ await BrowserTestUtils.browserLoaded(browser);
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function () {
+ info("Cleaning up.");
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+});
diff --git a/toolkit/components/antitracking/test/browser/browser_fpiServiceWorkers_fingerprinting.js b/toolkit/components/antitracking/test/browser/browser_fpiServiceWorkers_fingerprinting.js
new file mode 100644
index 0000000000..fcdc83cde6
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_fpiServiceWorkers_fingerprinting.js
@@ -0,0 +1,90 @@
+/* import-globals-from storageAccessAPIHelpers.js */
+
+// This test ensures that a service worker for an exempted domain is exempted when it is
+// in the first party context, and not exempted when it is in a third party context.
+
+PartitionedStorageHelper.runTest(
+ "ServiceWorkers - Check that RFP correctly is exempted and not exempted when FPI is enabled",
+ async (win3rdParty, win1stParty, allowed) => {
+ // Quickly unset and reset these prefs so we can get the real navigator.hardwareConcurrency
+ // It can't be set enternally and then captured because this function gets stringified and then evaled in a new scope.
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["privacy.firstParty.isolate", false],
+ ["privacy.resistFingerprinting", false],
+ ],
+ });
+
+ var SPOOFED_HW_CONCURRENCY = 2;
+ var DEFAULT_HARDWARE_CONCURRENCY = navigator.hardwareConcurrency;
+
+ await SpecialPowers.popPrefEnv();
+
+ // Register service worker for the first-party window.
+ if (!win1stParty.sw) {
+ win1stParty.sw = await registerServiceWorker(
+ win1stParty,
+ "serviceWorker.js"
+ );
+ }
+
+ // Register service worker for the third-party window.
+ if (!win3rdParty.sw) {
+ win3rdParty.sw = await registerServiceWorker(
+ win3rdParty,
+ "serviceWorker.js"
+ );
+ }
+
+ // Check DOM cache from the first-party service worker.
+ let res = await sendAndWaitWorkerMessage(
+ win1stParty.sw,
+ win1stParty.navigator.serviceWorker,
+ { type: "GetHWConcurrency" }
+ );
+ console.info(
+ "First Party, got: " +
+ res.value +
+ " Expected: " +
+ DEFAULT_HARDWARE_CONCURRENCY
+ );
+ is(
+ res.value,
+ DEFAULT_HARDWARE_CONCURRENCY,
+ "As a first party, HW Concurrency should not be spoofed"
+ );
+
+ // Check DOM cache from the third-party service worker.
+ res = await sendAndWaitWorkerMessage(
+ win3rdParty.sw,
+ win3rdParty.navigator.serviceWorker,
+ { type: "GetHWConcurrency" }
+ );
+ console.info(
+ "Third Party, got: " + res.value + " Expected: " + SPOOFED_HW_CONCURRENCY
+ );
+ is(
+ res.value,
+ SPOOFED_HW_CONCURRENCY,
+ "As a third party, HW Concurrency should be spoofed"
+ );
+ },
+
+ async _ => {
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+ },
+
+ [
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ["dom.ipc.processCount", 1],
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ ["privacy.firstParty.isolate", true],
+ ["privacy.resistFingerprinting", true],
+ ["privacy.resistFingerprinting.exemptedDomains", "*.example.com"],
+ ]
+);
diff --git a/toolkit/components/antitracking/test/browser/browser_hasStorageAccess.js b/toolkit/components/antitracking/test/browser/browser_hasStorageAccess.js
new file mode 100644
index 0000000000..c51959fe78
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_hasStorageAccess.js
@@ -0,0 +1,196 @@
+// This test ensures HasStorageAccess API returns the right value under different
+// scenarios.
+
+var settings = [
+ // same-origin no-tracker
+ {
+ name: "Test whether same-origin non-tracker frame has storage access",
+ topPage: TEST_TOP_PAGE,
+ thirdPartyPage: TEST_DOMAIN + TEST_PATH + "3rdParty.html",
+ },
+ // 3rd-party no-tracker
+ {
+ name: "Test whether 3rd-party non-tracker frame has storage access",
+ topPage: TEST_TOP_PAGE,
+ thirdPartyPage: TEST_4TH_PARTY_PAGE,
+ },
+ // 3rd-party no-tracker with permission
+ {
+ name: "Test whether 3rd-party non-tracker frame has storage access when storage permission is granted before",
+ topPage: TEST_TOP_PAGE,
+ thirdPartyPage: TEST_4TH_PARTY_PAGE,
+ setup: () => {
+ let type = "3rdPartyStorage^http://not-tracking.example.com";
+ let permission = Services.perms.ALLOW_ACTION;
+ let expireType = Services.perms.EXPIRE_SESSION;
+ PermissionTestUtils.add(TEST_DOMAIN, type, permission, expireType, 0);
+
+ registerCleanupFunction(_ => {
+ Services.perms.removeAll();
+ });
+ },
+ },
+ // 3rd-party tracker
+ {
+ name: "Test whether 3rd-party tracker frame has storage access",
+ topPage: TEST_TOP_PAGE,
+ thirdPartyPage: TEST_3RD_PARTY_PAGE,
+ },
+ // 3rd-party tracker with permission
+ {
+ name: "Test whether 3rd-party tracker frame has storage access when storage access permission is granted before",
+ topPage: TEST_TOP_PAGE,
+ thirdPartyPage: TEST_3RD_PARTY_PAGE,
+ setup: () => {
+ let type = "3rdPartyStorage^https://tracking.example.org";
+ let permission = Services.perms.ALLOW_ACTION;
+ let expireType = Services.perms.EXPIRE_SESSION;
+ PermissionTestUtils.add(TEST_DOMAIN, type, permission, expireType, 0);
+
+ registerCleanupFunction(_ => {
+ Services.perms.removeAll();
+ });
+ },
+ },
+ // same-site 3rd-party tracker
+ {
+ name: "Test whether same-site 3rd-party tracker frame has storage access",
+ topPage: TEST_TOP_PAGE,
+ thirdPartyPage: TEST_ANOTHER_3RD_PARTY_PAGE,
+ },
+ // same-origin 3rd-party tracker
+ {
+ name: "Test whether same-origin 3rd-party tracker frame has storage access",
+ topPage: TEST_ANOTHER_3RD_PARTY_DOMAIN + TEST_PATH + "page.html",
+ thirdPartyPage: TEST_ANOTHER_3RD_PARTY_PAGE,
+ },
+];
+
+var testCases = [
+ {
+ behavior: BEHAVIOR_ACCEPT, // 0
+ hasStorageAccess: [
+ true /* same-origin non-tracker */,
+ true /* 3rd-party non-tracker */,
+ true /* 3rd-party non-tracker with permission */,
+ true /* 3rd-party tracker */,
+ true /* 3rd-party tracker with permission */,
+ true /* same-site tracker */,
+ true /* same-origin tracker */,
+ ],
+ },
+ {
+ behavior: BEHAVIOR_REJECT_FOREIGN, // 1
+ hasStorageAccess: [
+ true /* same-origin non-tracker */,
+ false /* 3rd-party non-tracker */,
+ SpecialPowers.Services.prefs.getBoolPref(
+ "network.cookie.rejectForeignWithExceptions.enabled"
+ ) /* 3rd-party tracker with permission */,
+ false /* 3rd-party tracker */,
+ SpecialPowers.Services.prefs.getBoolPref(
+ "network.cookie.rejectForeignWithExceptions.enabled"
+ ) /* 3rd-party non-tracker with permission */,
+ true /* same-site tracker */,
+ true /* same-origin tracker */,
+ ],
+ },
+ {
+ behavior: BEHAVIOR_REJECT, // 2
+ hasStorageAccess: [
+ false /* same-origin non-tracker */,
+ false /* 3rd-party non-tracker */,
+ false /* 3rd-party non-tracker with permission */,
+ false /* 3rd-party tracker */,
+ false /* 3rd-party tracker with permission */,
+ false /* same-site tracker */,
+ false /* same-origin tracker */,
+ ],
+ },
+ {
+ behavior: BEHAVIOR_LIMIT_FOREIGN, // 3
+ hasStorageAccess: [
+ true /* same-origin non-tracker */,
+ false /* 3rd-party non-tracker */,
+ false /* 3rd-party non-tracker with permission */,
+ false /* 3rd-party tracker */,
+ false /* 3rd-party tracker with permission */,
+ true /* same-site tracker */,
+ true /* same-origin tracker */,
+ ],
+ },
+ {
+ behavior: BEHAVIOR_REJECT_TRACKER, // 4
+ hasStorageAccess: [
+ true /* same-origin non-tracker */,
+ true /* 3rd-party non-tracker */,
+ true /* 3rd-party non-tracker with permission */,
+ false /* 3rd-party tracker */,
+ true /* 3rd-party tracker with permission */,
+ true /* same-site tracker */,
+ true /* same-origin tracker */,
+ ],
+ },
+ {
+ behavior: BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, // 5
+ hasStorageAccess: [
+ true /* same-origin non-tracker */,
+ false /* 3rd-party non-tracker */,
+ true /* 3rd-party non-tracker with permission */,
+ false /* 3rd-party tracker */,
+ true /* 3rd-party tracker with permission */,
+ true /* same-site tracker */,
+ true /* same-origin tracker */,
+ ],
+ },
+];
+
+(function () {
+ settings.forEach(setting => {
+ if (setting.setup) {
+ add_task(async _ => {
+ setting.setup();
+ });
+ }
+
+ testCases.forEach(test => {
+ let callback = test.hasStorageAccess[settings.indexOf(setting)]
+ ? async _ => {
+ /* import-globals-from storageAccessAPIHelpers.js */
+ await hasStorageAccessInitially();
+ }
+ : async _ => {
+ /* import-globals-from storageAccessAPIHelpers.js */
+ await noStorageAccessInitially();
+ };
+
+ AntiTracking._createTask({
+ name: setting.name,
+ cookieBehavior: test.behavior,
+ allowList: false,
+ callback,
+ extraPrefs: [
+ [
+ "privacy.partition.always_partition_third_party_non_cookie_storage",
+ false,
+ ],
+ ],
+ expectedBlockingNotifications: 0,
+ runInPrivateWindow: false,
+ iframeSandbox: null,
+ accessRemoval: null,
+ callbackAfterRemoval: null,
+ topPage: setting.topPage,
+ thirdPartyPage: setting.thirdPartyPage,
+ });
+ });
+
+ add_task(async _ => {
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+ });
+ });
+})();
diff --git a/toolkit/components/antitracking/test/browser/browser_hasStorageAccess_alwaysPartition.js b/toolkit/components/antitracking/test/browser/browser_hasStorageAccess_alwaysPartition.js
new file mode 100644
index 0000000000..d89b0b1103
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_hasStorageAccess_alwaysPartition.js
@@ -0,0 +1,209 @@
+// This test ensures HasStorageAccess API returns the right value under different
+// scenarios.
+
+var settings = [
+ // same-origin no-tracker
+ {
+ name: "Test whether same-origin non-tracker frame has storage access",
+ topPage: TEST_TOP_PAGE,
+ thirdPartyPage: TEST_DOMAIN + TEST_PATH + "3rdParty.html",
+ },
+ // 3rd-party no-tracker
+ {
+ name: "Test whether 3rd-party non-tracker frame has storage access",
+ topPage: TEST_TOP_PAGE,
+ thirdPartyPage: TEST_4TH_PARTY_PAGE,
+ },
+ // 3rd-party no-tracker with permission
+ {
+ name: "Test whether 3rd-party non-tracker frame has storage access when storage permission is granted before",
+ topPage: TEST_TOP_PAGE,
+ thirdPartyPage: TEST_4TH_PARTY_PAGE,
+ setup: () => {
+ let type = "3rdPartyStorage^http://not-tracking.example.com";
+ let permission = Services.perms.ALLOW_ACTION;
+ let expireType = Services.perms.EXPIRE_SESSION;
+ PermissionTestUtils.add(TEST_DOMAIN, type, permission, expireType, 0);
+
+ registerCleanupFunction(_ => {
+ Services.perms.removeAll();
+ });
+ },
+ },
+ // 3rd-party tracker
+ {
+ name: "Test whether 3rd-party tracker frame has storage access",
+ topPage: TEST_TOP_PAGE,
+ thirdPartyPage: TEST_3RD_PARTY_PAGE,
+ },
+ // 3rd-party tracker with permission
+ {
+ name: "Test whether 3rd-party tracker frame has storage access when storage access permission is granted before",
+ topPage: TEST_TOP_PAGE,
+ thirdPartyPage: TEST_3RD_PARTY_PAGE,
+ setup: () => {
+ let type = "3rdPartyStorage^https://tracking.example.org";
+ let permission = Services.perms.ALLOW_ACTION;
+ let expireType = Services.perms.EXPIRE_SESSION;
+ PermissionTestUtils.add(TEST_DOMAIN, type, permission, expireType, 0);
+
+ registerCleanupFunction(_ => {
+ Services.perms.removeAll();
+ });
+ },
+ },
+ // same-site 3rd-party tracker
+ {
+ name: "Test whether same-site 3rd-party tracker frame has storage access",
+ topPage: TEST_TOP_PAGE,
+ thirdPartyPage: TEST_ANOTHER_3RD_PARTY_PAGE,
+ },
+ // same-origin 3rd-party tracker
+ {
+ name: "Test whether same-origin 3rd-party tracker frame has storage access",
+ topPage: TEST_ANOTHER_3RD_PARTY_DOMAIN + TEST_PATH + "page.html",
+ thirdPartyPage: TEST_ANOTHER_3RD_PARTY_PAGE,
+ },
+];
+
+const allBlocked = Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_ALL;
+const foreignBlocked = Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_FOREIGN;
+const trackerBlocked = Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER;
+
+var testCases = [
+ {
+ behavior: BEHAVIOR_ACCEPT, // 0
+ cases: [
+ [true] /* same-origin non-tracker */,
+ [true] /* 3rd-party non-tracker */,
+ [true] /* 3rd-party non-tracker with permission */,
+ [true] /* 3rd-party tracker */,
+ [true] /* 3rd-party tracker with permission */,
+ [true] /* same-site tracker */,
+ [true] /* same-origin tracker */,
+ ],
+ },
+ {
+ behavior: BEHAVIOR_REJECT_FOREIGN, // 1
+ cases: [
+ [true] /* same-origin non-tracker */,
+ [false, foreignBlocked] /* 3rd-party non-tracker */,
+ [
+ SpecialPowers.Services.prefs.getBoolPref(
+ "network.cookie.rejectForeignWithExceptions.enabled"
+ ),
+ foreignBlocked,
+ ] /* 3rd-party tracker with permission */,
+ [false, foreignBlocked] /* 3rd-party tracker */,
+ [
+ SpecialPowers.Services.prefs.getBoolPref(
+ "network.cookie.rejectForeignWithExceptions.enabled"
+ ),
+ foreignBlocked,
+ ] /* 3rd-party non-tracker with permission */,
+ [true] /* same-site tracker */,
+ [true] /* same-origin tracker */,
+ ],
+ },
+ {
+ behavior: BEHAVIOR_REJECT, // 2
+ cases: [
+ [false, allBlocked] /* same-origin non-tracker */,
+ [false, allBlocked] /* 3rd-party non-tracker */,
+ [false, allBlocked] /* 3rd-party non-tracker with permission */,
+ [false, allBlocked] /* 3rd-party tracker */,
+ [false, allBlocked] /* 3rd-party tracker with permission */,
+ [false, allBlocked] /* same-site tracker */,
+ [false, allBlocked] /* same-origin tracker */,
+ ],
+ },
+ {
+ behavior: BEHAVIOR_LIMIT_FOREIGN, // 3
+ cases: [
+ [true] /* same-origin non-tracker */,
+ [false, foreignBlocked] /* 3rd-party non-tracker */,
+ [false, foreignBlocked] /* 3rd-party non-tracker with permission */,
+ [false, foreignBlocked] /* 3rd-party tracker */,
+ [false, foreignBlocked] /* 3rd-party tracker with permission */,
+ [true] /* same-site tracker */,
+ [true] /* same-origin tracker */,
+ ],
+ },
+ {
+ behavior: BEHAVIOR_REJECT_TRACKER, // 4
+ cases: [
+ [true] /* same-origin non-tracker */,
+ [true] /* 3rd-party non-tracker */,
+ [true] /* 3rd-party non-tracker with permission */,
+ [false, trackerBlocked] /* 3rd-party tracker */,
+ [true] /* 3rd-party tracker with permission */,
+ [true] /* same-site tracker */,
+ [true] /* same-origin tracker */,
+ ],
+ },
+ {
+ behavior: BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, // 5
+ cases: [
+ [true] /* same-origin non-tracker */,
+ [false] /* 3rd-party non-tracker */,
+ [true] /* 3rd-party non-tracker with permission */,
+ [false, trackerBlocked] /* 3rd-party tracker */,
+ [true] /* 3rd-party tracker with permission */,
+ [true] /* same-site tracker */,
+ [true] /* same-origin tracker */,
+ ],
+ },
+];
+
+(function () {
+ settings.forEach(setting => {
+ ok(true, JSON.stringify(setting));
+ if (setting.setup) {
+ add_task(async _ => {
+ setting.setup();
+ });
+ }
+
+ testCases.forEach(test => {
+ let [hasStorageAccess, expectedBlockingNotifications] =
+ test.cases[settings.indexOf(setting)];
+ let callback = hasStorageAccess
+ ? async _ => {
+ /* import-globals-from storageAccessAPIHelpers.js */
+ await hasStorageAccessInitially();
+ }
+ : async _ => {
+ /* import-globals-from storageAccessAPIHelpers.js */
+ await noStorageAccessInitially();
+ };
+
+ AntiTracking._createTask({
+ name: setting.name,
+ cookieBehavior: test.behavior,
+ allowList: false,
+ callback,
+ extraPrefs: [
+ [
+ "privacy.partition.always_partition_third_party_non_cookie_storage",
+ true,
+ ],
+ ],
+ expectedBlockingNotifications,
+ runInPrivateWindow: false,
+ iframeSandbox: null,
+ accessRemoval: null,
+ callbackAfterRemoval: null,
+ topPage: setting.topPage,
+ thirdPartyPage: setting.thirdPartyPage,
+ });
+ });
+
+ add_task(async _ => {
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+ });
+ });
+})();
diff --git a/toolkit/components/antitracking/test/browser/browser_iframe_document_open.js b/toolkit/components/antitracking/test/browser/browser_iframe_document_open.js
new file mode 100644
index 0000000000..73876ee1a5
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_iframe_document_open.js
@@ -0,0 +1,86 @@
+//
+// Bug 1725996 - Test if the cookie set in a document which is created by
+// document.open() in an about:blank iframe has a correct
+// partitionKey
+//
+
+const TEST_PAGE = TEST_DOMAIN + TEST_PATH + "file_iframe_document_open.html";
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [
+ "network.cookie.cookieBehavior",
+ BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN,
+ ],
+ [
+ "network.cookie.cookieBehavior.pbmode",
+ BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN,
+ ],
+ ["privacy.dynamic_firstparty.use_site", true],
+ ],
+ });
+});
+
+add_task(async function test_firstParty_iframe() {
+ // Clear all cookies before test.
+ Services.cookies.removeAll();
+
+ // Open the test page which creates an iframe and then calls document.write()
+ // to write a cookie in the iframe.
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_PAGE);
+
+ // Wait until the cookie appears.
+ await TestUtils.waitForCondition(_ => Services.cookies.cookies.length);
+
+ // Check the partitionKey in the cookie.
+ let cookie = Services.cookies.cookies[0];
+ is(
+ cookie.originAttributes.partitionKey,
+ "",
+ "The partitionKey should remain empty for first-party iframe."
+ );
+
+ // Clean up.
+ Services.cookies.removeAll();
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function test_thirdParty_iframe() {
+ // Clear all cookies before test.
+ Services.cookies.removeAll();
+
+ // Open a tab with a different domain with the test page.
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ TEST_TOP_PAGE_2
+ );
+
+ // Open the test page within a third-party context.
+ await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [TEST_PAGE],
+ async function (page) {
+ let ifr = content.document.createElement("iframe");
+ let loading = ContentTaskUtils.waitForEvent(ifr, "load");
+ ifr.src = page;
+ content.document.body.appendChild(ifr);
+ await loading;
+ }
+ );
+
+ // Wait until the cookie appears.
+ await TestUtils.waitForCondition(_ => Services.cookies.cookies.length);
+
+ // Check the partitionKey in the cookie.
+ let cookie = Services.cookies.cookies[0];
+ is(
+ cookie.originAttributes.partitionKey,
+ "(http,xn--exmple-cua.test)",
+ "The partitionKey should exist for third-party iframe."
+ );
+
+ // Clean up.
+ Services.cookies.removeAll();
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/toolkit/components/antitracking/test/browser/browser_imageCache4.js b/toolkit/components/antitracking/test/browser/browser_imageCache4.js
new file mode 100644
index 0000000000..8fcc298cf0
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_imageCache4.js
@@ -0,0 +1,13 @@
+let cookieBehavior = BEHAVIOR_REJECT_TRACKER;
+let blockingByAllowList = false;
+let expectedBlockingNotifications =
+ Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER;
+
+let rootDir = getRootDirectory(gTestPath);
+let jar = getJar(rootDir);
+if (jar) {
+ let tmpdir = extractJarToTmp(jar);
+ rootDir = "file://" + tmpdir.path + "/";
+}
+/* import-globals-from imageCacheWorker.js */
+Services.scriptloader.loadSubScript(rootDir + "imageCacheWorker.js", this);
diff --git a/toolkit/components/antitracking/test/browser/browser_imageCache8.js b/toolkit/components/antitracking/test/browser/browser_imageCache8.js
new file mode 100644
index 0000000000..b57bf1dca5
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_imageCache8.js
@@ -0,0 +1,13 @@
+let cookieBehavior = BEHAVIOR_REJECT_TRACKER;
+let blockingByAllowList = true;
+let expectedBlockingNotifications =
+ Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER;
+
+let rootDir = getRootDirectory(gTestPath);
+let jar = getJar(rootDir);
+if (jar) {
+ let tmpdir = extractJarToTmp(jar);
+ rootDir = "file://" + tmpdir.path + "/";
+}
+/* import-globals-from imageCacheWorker.js */
+Services.scriptloader.loadSubScript(rootDir + "imageCacheWorker.js", this);
diff --git a/toolkit/components/antitracking/test/browser/browser_localStorageEvents.js b/toolkit/components/antitracking/test/browser/browser_localStorageEvents.js
new file mode 100644
index 0000000000..b46cc60b91
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_localStorageEvents.js
@@ -0,0 +1,186 @@
+add_task(async function () {
+ info("Starting subResources test");
+
+ await SpecialPowers.flushPrefEnv();
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.storage_access.enabled", true],
+ [
+ "network.cookie.cookieBehavior",
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER,
+ ],
+ [
+ "network.cookie.cookieBehavior.pbmode",
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER,
+ ],
+ ["privacy.trackingprotection.enabled", false],
+ ["privacy.trackingprotection.pbmode.enabled", false],
+ ["privacy.trackingprotection.annotate_channels", true],
+ ],
+ });
+
+ await UrlClassifierTestUtils.addTestTrackers();
+});
+
+add_task(async function testLocalStorageEventPropagation() {
+ info("Creating a new tab");
+ let tab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE);
+ gBrowser.selectedTab = tab;
+
+ let browser = gBrowser.getBrowserForTab(tab);
+ await BrowserTestUtils.browserLoaded(browser);
+
+ info("Loading tracking scripts");
+ await SpecialPowers.spawn(
+ browser,
+ [
+ {
+ page: TEST_3RD_PARTY_DOMAIN + TEST_PATH + "localStorage.html",
+ },
+ ],
+ async obj => {
+ info("Creating tracker iframe");
+
+ let ifr = content.document.createElement("iframe");
+ ifr.src = obj.page;
+
+ await new content.Promise(resolve => {
+ ifr.onload = function () {
+ resolve();
+ };
+ content.document.body.appendChild(ifr);
+ });
+
+ info("LocalStorage should be blocked.");
+ await new content.Promise(resolve => {
+ content.addEventListener(
+ "message",
+ e => {
+ if (e.data.type == "test") {
+ is(e.data.status, false, "LocalStorage blocked");
+ } else {
+ ok(false, "Unknown message");
+ }
+ resolve();
+ },
+ { once: true }
+ );
+ ifr.contentWindow.postMessage("test", "*");
+ });
+
+ info("Let's open the popup");
+ await new content.Promise(resolve => {
+ content.addEventListener(
+ "message",
+ e => {
+ if (e.data.type == "test") {
+ is(e.data.status, true, "LocalStorage unblocked");
+ } else {
+ ok(false, "Unknown message");
+ }
+ resolve();
+ },
+ { once: true }
+ );
+ ifr.contentWindow.postMessage("open", "*");
+ });
+ }
+ );
+
+ info("Removing the tab");
+ BrowserTestUtils.removeTab(tab);
+
+ info("Cleaning up.");
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+});
+
+add_task(async function testBlockedLocalStorageEventPropagation() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [
+ "privacy.restrict3rdpartystorage.userInteractionRequiredForHosts",
+ "tracking.example.com,tracking.example.org",
+ ],
+ ],
+ });
+
+ info("Creating a new tab");
+ let tab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE);
+ gBrowser.selectedTab = tab;
+
+ let browser = gBrowser.getBrowserForTab(tab);
+ await BrowserTestUtils.browserLoaded(browser);
+
+ info("Loading tracking scripts");
+ await SpecialPowers.spawn(
+ browser,
+ [
+ {
+ page: TEST_3RD_PARTY_DOMAIN + TEST_PATH + "localStorage.html",
+ },
+ ],
+ async obj => {
+ info("Creating tracker iframe");
+
+ let ifr = content.document.createElement("iframe");
+ ifr.src = obj.page;
+
+ await new content.Promise(resolve => {
+ ifr.onload = function () {
+ resolve();
+ };
+ content.document.body.appendChild(ifr);
+ });
+
+ info("LocalStorage should be blocked.");
+ await new content.Promise(resolve => {
+ content.addEventListener(
+ "message",
+ e => {
+ if (e.data.type == "test") {
+ is(e.data.status, false, "LocalStorage blocked");
+ } else {
+ ok(false, "Unknown message");
+ }
+ resolve();
+ },
+ { once: true }
+ );
+ ifr.contentWindow.postMessage("test", "*");
+ });
+
+ info("Let's open the popup");
+ await new content.Promise(resolve => {
+ content.addEventListener(
+ "message",
+ e => {
+ if (e.data.type == "test") {
+ is(e.data.status, false, "LocalStorage still blocked");
+ } else {
+ ok(false, "Unknown message");
+ }
+ resolve();
+ },
+ { once: true }
+ );
+ ifr.contentWindow.postMessage("open and test", "*");
+ });
+ }
+ );
+
+ info("Removing the tab");
+ BrowserTestUtils.removeTab(tab);
+
+ UrlClassifierTestUtils.cleanupTestTrackers();
+
+ info("Cleaning up.");
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+});
diff --git a/toolkit/components/antitracking/test/browser/browser_onBeforeRequestNotificationForTrackingResources.js b/toolkit/components/antitracking/test/browser/browser_onBeforeRequestNotificationForTrackingResources.js
new file mode 100644
index 0000000000..847bfd7dc9
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_onBeforeRequestNotificationForTrackingResources.js
@@ -0,0 +1,96 @@
+/**
+ * This test ensures that onBeforeRequest is dispatched for webRequest loads that
+ * are blocked by tracking protection. It sets up a page with a third-party script
+ * resource on it that is blocked by TP, and sets up an onBeforeRequest listener
+ * which waits to be notified about that resource. The test would time out if the
+ * onBeforeRequest listener isn't called dispatched before the load is canceled.
+ */
+
+let extension;
+add_task(async function () {
+ extension = ExtensionTestUtils.loadExtension({
+ manifest: { permissions: ["webRequest", "webRequestBlocking", "*://*/*"] },
+ async background() {
+ let gExpectedResourcesSeen = 0;
+ function onBeforeRequest(details) {
+ let spec = details.url;
+ browser.test.log("Observed channel for " + spec);
+ // We would use TEST_3RD_PARTY_DOMAIN_TP here, but the variable is inaccessible
+ // since it is defined in head.js!
+ if (!spec.startsWith("https://tracking.example.com/")) {
+ return undefined;
+ }
+ if (spec.endsWith("empty.js")) {
+ browser.test.succeed("Correct resource observed");
+ ++gExpectedResourcesSeen;
+ } else if (spec.endsWith("empty.js?redirect")) {
+ return { redirectUrl: spec.replace("empty.js?redirect", "head.js") };
+ } else if (spec.endsWith("head.js")) {
+ ++gExpectedResourcesSeen;
+ }
+ if (gExpectedResourcesSeen == 2) {
+ browser.webRequest.onBeforeRequest.removeListener(onBeforeRequest);
+ browser.test.sendMessage("finish");
+ }
+ return undefined;
+ }
+
+ browser.webRequest.onBeforeRequest.addListener(
+ onBeforeRequest,
+ { urls: ["*://*/*"] },
+ ["blocking"]
+ );
+ browser.test.sendMessage("ready");
+ },
+ });
+ await extension.startup();
+ await extension.awaitMessage("ready");
+});
+
+add_task(async function () {
+ info("Starting subResources test");
+
+ await SpecialPowers.flushPrefEnv();
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["privacy.trackingprotection.enabled", true],
+ // the test doesn't open a private window, so we don't care about this pref's value
+ ["privacy.trackingprotection.pbmode.enabled", false],
+ // tracking annotations aren't needed in this test, only TP is needed
+ ["privacy.trackingprotection.annotate_channels", false],
+ [
+ "privacy.restrict3rdpartystorage.userInteractionRequiredForHosts",
+ "tracking.example.com,tracking.example.org",
+ ],
+ ["privacy.trackingprotection.testing.report_blocked_node", true],
+ ],
+ });
+
+ await UrlClassifierTestUtils.addTestTrackers();
+
+ let promise = extension.awaitMessage("finish");
+
+ info("Creating a new tab");
+ let tab = BrowserTestUtils.addTab(gBrowser, TEST_EMBEDDER_PAGE);
+ gBrowser.selectedTab = tab;
+
+ let browser = gBrowser.getBrowserForTab(tab);
+ await BrowserTestUtils.browserLoaded(browser);
+
+ await promise;
+
+ info("Verify the number of tracking nodes found");
+ await SpecialPowers.spawn(browser, [{ expected: 3 }], async function (obj) {
+ is(
+ content.document.blockedNodeByClassifierCount,
+ obj.expected,
+ "Expected tracking nodes found"
+ );
+ });
+
+ info("Removing the tab");
+ BrowserTestUtils.removeTab(tab);
+
+ UrlClassifierTestUtils.cleanupTestTrackers();
+ await extension.unload();
+});
diff --git a/toolkit/components/antitracking/test/browser/browser_onModifyRequestNotificationForTrackingResources.js b/toolkit/components/antitracking/test/browser/browser_onModifyRequestNotificationForTrackingResources.js
new file mode 100644
index 0000000000..0674b87136
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_onModifyRequestNotificationForTrackingResources.js
@@ -0,0 +1,96 @@
+/* eslint-disable mozilla/no-arbitrary-setTimeout */
+/**
+ * This test ensures that http-on-modify-request is dispatched for channels that
+ * are blocked by tracking protection. It sets up a page with a third-party script
+ * resource on it that is blocked by TP, and sets up an http-on-modify-request
+ * observer which waits to be notified about that resource. The test would time out
+ * if the http-on-modify-request notification isn't dispatched before the channel is
+ * canceled.
+ */
+
+let gExpectedResourcesSeen = 0;
+async function onModifyRequest() {
+ return new Promise((resolve, reject) => {
+ Services.obs.addObserver(function observer(subject, topic, data) {
+ let httpChannel = subject.QueryInterface(Ci.nsIHttpChannel);
+ let spec = httpChannel.URI.spec;
+ info("Observed channel for " + spec);
+ if (httpChannel.URI.prePath + "/" != TEST_3RD_PARTY_DOMAIN_TP) {
+ return;
+ }
+ if (spec.endsWith("empty.js")) {
+ ok(true, "Correct resource observed");
+ ++gExpectedResourcesSeen;
+ } else if (spec.endsWith("empty.js?redirect")) {
+ httpChannel.redirectTo(
+ Services.io.newURI(spec.replace("empty.js?redirect", "head.js"))
+ );
+ } else if (spec.endsWith("empty.js?redirect2")) {
+ httpChannel.suspend();
+ setTimeout(() => {
+ httpChannel.redirectTo(
+ Services.io.newURI(spec.replace("empty.js?redirect2", "head.js"))
+ );
+ httpChannel.resume();
+ }, 100);
+ } else if (spec.endsWith("head.js")) {
+ ++gExpectedResourcesSeen;
+ }
+ if (gExpectedResourcesSeen == 3) {
+ Services.obs.removeObserver(observer, "http-on-modify-request");
+ resolve();
+ }
+ }, "http-on-modify-request");
+ });
+}
+
+add_task(async function () {
+ info("Starting subResources test");
+
+ await SpecialPowers.flushPrefEnv();
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["privacy.trackingprotection.enabled", true],
+ // the test doesn't open a private window, so we don't care about this pref's value
+ ["privacy.trackingprotection.pbmode.enabled", false],
+ // tracking annotations aren't needed in this test, only TP is needed
+ ["privacy.trackingprotection.annotate_channels", false],
+ [
+ "privacy.restrict3rdpartystorage.userInteractionRequiredForHosts",
+ "tracking.example.com,tracking.example.org",
+ ],
+ ["privacy.trackingprotection.testing.report_blocked_node", true],
+ ],
+ });
+
+ await UrlClassifierTestUtils.addTestTrackers();
+
+ let promise = onModifyRequest();
+
+ info("Creating a new tab");
+ let tab = BrowserTestUtils.addTab(gBrowser, TEST_EMBEDDER_PAGE);
+ gBrowser.selectedTab = tab;
+
+ let browser = gBrowser.getBrowserForTab(tab);
+ await BrowserTestUtils.browserLoaded(browser);
+
+ await promise;
+
+ info("Verify the number of tracking nodes found");
+ await SpecialPowers.spawn(
+ browser,
+ [{ expected: gExpectedResourcesSeen }],
+ async function (obj) {
+ is(
+ content.document.blockedNodeByClassifierCount,
+ obj.expected,
+ "Expected tracking nodes found"
+ );
+ }
+ );
+
+ info("Removing the tab");
+ BrowserTestUtils.removeTab(tab);
+
+ UrlClassifierTestUtils.cleanupTestTrackers();
+});
diff --git a/toolkit/components/antitracking/test/browser/browser_partitionedClearSiteDataHeader.js b/toolkit/components/antitracking/test/browser/browser_partitionedClearSiteDataHeader.js
new file mode 100644
index 0000000000..7c79ecbe32
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_partitionedClearSiteDataHeader.js
@@ -0,0 +1,593 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+"use strict";
+
+/**
+ * Tests that when receiving the "clear-site-data" header - with dFPI enabled -
+ * we clear storage under the correct partition.
+ */
+
+const { SiteDataTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/SiteDataTestUtils.sys.mjs"
+);
+
+const HOST_A = "example.com";
+const HOST_B = "example.org";
+const ORIGIN_A = `https://${HOST_A}`;
+const ORIGIN_B = `https://${HOST_B}`;
+const CLEAR_SITE_DATA_PATH = `/${TEST_PATH}clearSiteData.sjs`;
+const CLEAR_SITE_DATA_URL_ORIGIN_B = ORIGIN_B + CLEAR_SITE_DATA_PATH;
+const CLEAR_SITE_DATA_URL_ORIGIN_A = ORIGIN_A + CLEAR_SITE_DATA_PATH;
+const THIRD_PARTY_FRAME_ID_ORIGIN_B = "thirdPartyFrame";
+const THIRD_PARTY_FRAME_ID_ORIGIN_A = "thirdPartyFrame2";
+const STORAGE_KEY = "testKey";
+
+// Skip localStorage tests when using legacy localStorage. The legacy
+// localStorage implementation does not support clearing data by principal. See
+// Bug 1688221, Bug 1688665.
+const skipLocalStorageTests = Services.prefs.getBoolPref(
+ "dom.storage.enable_unsupported_legacy_implementation"
+);
+
+/**
+ * Creates an iframe in the passed browser and waits for it to load.
+ * @param {Browser} browser - Browser to create the frame in.
+ * @param {String} src - Frame source url.
+ * @param {String} id - Frame id.
+ * @param {boolean} sandbox - Whether the frame should be sandboxed.
+ * @returns {Promise} - Resolves once the frame has loaded.
+ */
+function createFrame(browser, src, id, sandbox) {
+ return SpecialPowers.spawn(
+ browser,
+ [{ page: src, frameId: id, sandbox }],
+ async function (obj) {
+ await new content.Promise(resolve => {
+ let frame = content.document.createElement("iframe");
+ if (obj.sandbox) {
+ frame.setAttribute("sandbox", "allow-scripts");
+ }
+ frame.src = obj.page;
+ frame.id = obj.frameId;
+ frame.addEventListener("load", resolve, { once: true });
+ content.document.body.appendChild(frame);
+ });
+ }
+ );
+}
+
+/**
+ * Creates a new tab, loads a url and creates an iframe.
+ * Callers need to clean up the tab before the test ends.
+ * @param {String} firstPartyUrl - Url to load in tab.
+ * @param {String} thirdPartyUrl - Url to load in frame.
+ * @param {String} frameId - Id of iframe element.
+ * @param {boolean} sandbox - Whether the frame should be sandboxed.
+ * @returns {Promise} - Resolves with the tab and the frame BrowsingContext once
+ * the tab and the frame have loaded.
+ */
+async function createTabWithFrame(
+ firstPartyUrl,
+ thirdPartyUrl,
+ frameId,
+ sandbox
+) {
+ // Create tab and wait for it to be loaded.
+ let tab = BrowserTestUtils.addTab(gBrowser, firstPartyUrl);
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+
+ // Create cross origin iframe.
+ await createFrame(tab.linkedBrowser, thirdPartyUrl, frameId, sandbox);
+
+ // Return BrowsingContext of created iframe.
+ return { tab, frameBC: tab.linkedBrowser.browsingContext.children[0] };
+}
+
+/**
+ * Test wrapper for the ClearSiteData tests.
+ * Loads ORIGIN_A and ORIGIN_B in two tabs and inserts a cross origin pointing
+ * to the other iframe each.
+ * Both frames ORIGIN_B under ORIGIN_A and ORIGIN_A under ORIGIN_B will be
+ * storage partitioned.
+ * Depending on the clearDataContext variable we then either navigate ORIGIN_A
+ * (as top level) or ORIGIN_B (as third party frame) to the clear-site-data
+ * endpoint.
+ * @param {function} cbPreClear - Called after initial setup, once top levels
+ * and frames have been loaded.
+ * @param {function} cbPostClear - Called after data has been cleared via the
+ * "Clear-Site-Data" header.
+ * @param {("firstParty"|"thirdPartyPartitioned")} clearDataContext - Whether to
+ * navigate to the path that sets the "Clear-Site-Data" header with the first or
+ * third party.
+ * @param {boolean} [sandboxFrame] - Whether the frames should be sandboxed. No
+ * sandbox by default.
+ */
+async function runClearSiteDataTest(
+ cbPreClear,
+ cbPostClear,
+ clearDataContext,
+ sandboxFrame = false
+) {
+ // Create a tabs for origin A and B with cross origins frames B and A
+ let [
+ { frameBC: frameContextB, tab: tabA },
+ { frameBC: frameContextA, tab: tabB },
+ ] = await Promise.all([
+ createTabWithFrame(
+ ORIGIN_A,
+ ORIGIN_B,
+ THIRD_PARTY_FRAME_ID_ORIGIN_B,
+ sandboxFrame
+ ),
+ createTabWithFrame(
+ ORIGIN_B,
+ ORIGIN_A,
+ THIRD_PARTY_FRAME_ID_ORIGIN_A,
+ sandboxFrame
+ ),
+ ]);
+
+ let browserA = tabA.linkedBrowser;
+ let contextA = browserA.browsingContext;
+ let browserB = tabB.linkedBrowser;
+ let contextB = browserB.browsingContext;
+
+ // Run test callback before clear-site-data
+ if (cbPreClear) {
+ await cbPreClear(contextA, contextB, frameContextB, frameContextA);
+ }
+
+ // Navigate to path with clear-site-data header
+ // Depending on the clearDataContext variable we either do this with the
+ // top browser or the third party storage partitioned frame (B embedded in A).
+ info(`Opening path with clear-site-data-header for ${clearDataContext}`);
+ if (clearDataContext == "firstParty") {
+ // Open in new tab so we keep our current test tab intact. The
+ // post-clear-callback might need it.
+ await BrowserTestUtils.withNewTab(CLEAR_SITE_DATA_URL_ORIGIN_A, () => {});
+ } else if (clearDataContext == "thirdPartyPartitioned") {
+ // Navigate frame to path with clear-site-data header
+ await SpecialPowers.spawn(
+ browserA,
+ [
+ {
+ page: CLEAR_SITE_DATA_URL_ORIGIN_B,
+ frameId: THIRD_PARTY_FRAME_ID_ORIGIN_B,
+ },
+ ],
+ async function (obj) {
+ await new content.Promise(resolve => {
+ let frame = content.document.getElementById(obj.frameId);
+ frame.addEventListener("load", resolve, { once: true });
+ frame.src = obj.page;
+ });
+ }
+ );
+ } else {
+ ok(false, "Invalid context requested for clear-site-data");
+ }
+
+ if (cbPostClear) {
+ await cbPostClear(contextA, contextB, frameContextB, frameContextA);
+ }
+
+ info("Cleaning up.");
+ BrowserTestUtils.removeTab(tabA);
+ BrowserTestUtils.removeTab(tabB);
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, resolve);
+ });
+}
+
+/**
+ * Create an origin with partitionKey.
+ * @param {String} originNoSuffix - Origin without origin attributes.
+ * @param {String} [firstParty] - First party to create partitionKey.
+ * @returns {String} Origin with suffix. If not passed this will return the
+ * umodified origin.
+ */
+function getOrigin(originNoSuffix, firstParty) {
+ let origin = originNoSuffix;
+ if (firstParty) {
+ let [scheme, host] = firstParty.split("://");
+ origin += `^partitionKey=(${scheme},${host})`;
+ }
+ return origin;
+}
+
+/**
+ * Sets a storage item for an origin.
+ * @param {("cookie"|"localStorage")} storageType - Which storage type to use.
+ * @param {String} originNoSuffix - Context to set storage item in.
+ * @param {String} [firstParty] - Optional first party domain to partition
+ * under.
+ * @param {String} key - Key of the entry.
+ * @param {String} value - Value of the entry.
+ */
+function setStorageEntry(storageType, originNoSuffix, firstParty, key, value) {
+ if (storageType != "cookie" && storageType != "localStorage") {
+ ok(false, "Invalid storageType passed");
+ return;
+ }
+
+ let origin = getOrigin(originNoSuffix, firstParty);
+
+ if (storageType == "cookie") {
+ SiteDataTestUtils.addToCookies({ origin, name: key, value });
+ return;
+ }
+ // localStorage
+ SiteDataTestUtils.addToLocalStorage(origin, key, value);
+}
+
+/**
+ * Tests whether a host sets a cookie.
+ * For the purpose of this test we assume that there is either one or no cookie
+ * set.
+ * This performs cookie lookups directly via the cookie service.
+ * @param {boolean} hasCookie - Whether we expect to see a cookie.
+ * @param {String} originNoSuffix - Origin the cookie is stored for.
+ * @param {String|null} firstParty - Whether to test for a partitioned cookie.
+ * If set this will be used to construct the partitionKey.
+ * @param {String} [key] - Expected key / name of the cookie.
+ * @param {String} [value] - Expected value of the cookie.
+ */
+function testHasCookie(hasCookie, originNoSuffix, firstParty, key, value) {
+ let origin = getOrigin(originNoSuffix, firstParty);
+
+ let label = `${originNoSuffix}${
+ firstParty ? ` (partitioned under ${firstParty})` : ""
+ }`;
+
+ if (!hasCookie) {
+ ok(
+ !SiteDataTestUtils.hasCookies(origin),
+ `Cookie for ${label} is not set for key ${key}`
+ );
+ return;
+ }
+
+ ok(
+ SiteDataTestUtils.hasCookies(origin, [{ key, value }]),
+ `Cookie for ${label} is set ${key}=${value}`
+ );
+}
+
+/**
+ * Tests whether a context has a localStorage entry.
+ * @param {boolean} hasEntry - Whether we expect to see an entry.
+ * @param {String} originNoSuffix - Origin to test localStorage for.
+ * @param {String} [firstParty] - First party context to test under.
+ * @param {String} key - key of the localStorage item.
+ * @param {String} [expectedValue] - Expected value of the item.
+ */
+function testHasLocalStorageEntry(
+ hasEntry,
+ originNoSuffix,
+ firstParty,
+ key,
+ expectedValue
+) {
+ if (key == null) {
+ ok(false, "localStorage key is mandatory");
+ return;
+ }
+ let label = `${originNoSuffix}${
+ firstParty ? ` (partitioned under ${firstParty})` : ""
+ }`;
+ let origin = getOrigin(originNoSuffix, firstParty);
+ if (hasEntry) {
+ let hasEntry = SiteDataTestUtils.hasLocalStorage(origin, [
+ { key, value: expectedValue },
+ ]);
+ ok(
+ hasEntry,
+ `localStorage for ${label} has expected value ${key}=${expectedValue}`
+ );
+ } else {
+ let hasEntry = SiteDataTestUtils.hasLocalStorage(origin);
+ ok(!hasEntry, `localStorage for ${label} is not set for key ${key}`);
+ }
+}
+
+/**
+ * Sets the initial storage entries used by the storage tests in this file.
+ * 1. first party ( A )
+ * 2. first party ( B )
+ * 3. third party partitioned ( B under A)
+ * 4. third party partitioned ( A under B)
+ * The entry values reflect which context they are set for.
+ * @param {("cookie"|"localStorage")} storageType - Storage type to initialize.
+ */
+async function setupInitialStorageState(storageType) {
+ if (storageType != "cookie" && storageType != "localStorage") {
+ ok(false, "Invalid storageType passed");
+ return;
+ }
+
+ // Set a first party entry
+ setStorageEntry(storageType, ORIGIN_A, null, STORAGE_KEY, "firstParty");
+
+ // Set a storage entry in the storage partitioned third party frame
+ setStorageEntry(
+ storageType,
+ ORIGIN_B,
+ ORIGIN_A,
+ STORAGE_KEY,
+ "thirdPartyPartitioned"
+ );
+
+ // Set a storage entry in the non storage partitioned third party page
+ setStorageEntry(storageType, ORIGIN_B, null, STORAGE_KEY, "thirdParty");
+
+ // Set a storage entry in the second storage partitioned third party frame.
+ setStorageEntry(
+ storageType,
+ ORIGIN_A,
+ ORIGIN_B,
+ STORAGE_KEY,
+ "thirdPartyPartitioned2"
+ );
+
+ info("Test that storage entries are set for all contexts");
+
+ if (storageType == "cookie") {
+ testHasCookie(true, ORIGIN_A, null, STORAGE_KEY, "firstParty");
+ testHasCookie(true, ORIGIN_B, null, STORAGE_KEY, "thirdParty");
+ testHasCookie(
+ true,
+ ORIGIN_B,
+ ORIGIN_A,
+ STORAGE_KEY,
+ "thirdPartyPartitioned"
+ );
+ testHasCookie(
+ true,
+ ORIGIN_A,
+ ORIGIN_B,
+ STORAGE_KEY,
+ "thirdPartyPartitioned2"
+ );
+ return;
+ }
+
+ testHasLocalStorageEntry(true, ORIGIN_A, null, STORAGE_KEY, "firstParty");
+ testHasLocalStorageEntry(true, ORIGIN_B, null, STORAGE_KEY, "thirdParty");
+ testHasLocalStorageEntry(
+ true,
+ ORIGIN_B,
+ ORIGIN_A,
+ STORAGE_KEY,
+ "thirdPartyPartitioned"
+ );
+ testHasLocalStorageEntry(
+ true,
+ ORIGIN_A,
+ ORIGIN_B,
+ STORAGE_KEY,
+ "thirdPartyPartitioned2"
+ );
+}
+
+add_setup(async function () {
+ info("Starting ClearSiteData test");
+
+ await SpecialPowers.flushPrefEnv();
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.storage_access.enabled", true],
+ [
+ "network.cookie.cookieBehavior",
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN,
+ ],
+ [
+ "network.cookie.cookieBehavior.pbmode",
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN,
+ ],
+ ["privacy.trackingprotection.enabled", false],
+ ["privacy.trackingprotection.pbmode.enabled", false],
+ // Needed for SiteDataTestUtils#hasLocalStorage
+ ["dom.storage.client_validation", false],
+ ],
+ });
+});
+
+/**
+ * Test clearing partitioned cookies via clear-site-data header
+ * (Cleared via the cookie service).
+ */
+
+/**
+ * Tests that when a storage partitioned third party frame loads a site with
+ * "Clear-Site-Data", the cookies are cleared for only that partitioned frame.
+ */
+add_task(async function cookieClearThirdParty() {
+ await runClearSiteDataTest(
+ // Pre Clear-Site-Data
+ () => setupInitialStorageState("cookie"),
+ // Post Clear-Site-Data
+ () => {
+ info("Testing: First party cookie has not changed");
+ testHasCookie(true, ORIGIN_A, null, STORAGE_KEY, "firstParty");
+ info("Testing: Unpartitioned cookie has not changed");
+ testHasCookie(true, ORIGIN_B, null, STORAGE_KEY, "thirdParty");
+ info("Testing: Partitioned cookie for HOST_B (HOST_A) has been cleared");
+ testHasCookie(false, ORIGIN_B, ORIGIN_A);
+ info("Testing: Partitioned cookie for HOST_A (HOST_B) has not changed");
+ testHasCookie(
+ true,
+ ORIGIN_A,
+ ORIGIN_B,
+ STORAGE_KEY,
+ "thirdPartyPartitioned2"
+ );
+ },
+ // Send clear-site-data header in partitioned third party context.
+ "thirdPartyPartitioned"
+ );
+});
+
+/**
+ * Tests that when a sandboxed storage partitioned third party frame loads a
+ * site with "Clear-Site-Data", no cookies are cleared and we don't crash.
+ * Crash details in Bug 1686938.
+ */
+add_task(async function cookieClearThirdPartySandbox() {
+ await runClearSiteDataTest(
+ // Pre Clear-Site-Data
+ () => setupInitialStorageState("cookie"),
+ // Post Clear-Site-Data
+ () => {
+ info("Testing: First party cookie has not changed");
+ testHasCookie(true, ORIGIN_A, null, STORAGE_KEY, "firstParty");
+ info("Testing: Unpartitioned cookie has not changed");
+ testHasCookie(true, ORIGIN_B, null, STORAGE_KEY, "thirdParty");
+ info("Testing: Partitioned cookie for HOST_B (HOST_A) has not changed");
+ testHasCookie(
+ true,
+ ORIGIN_B,
+ ORIGIN_A,
+ STORAGE_KEY,
+ "thirdPartyPartitioned"
+ );
+ info("Testing: Partitioned cookie for HOST_A (HOST_B) has not changed");
+ testHasCookie(
+ true,
+ ORIGIN_A,
+ ORIGIN_B,
+ STORAGE_KEY,
+ "thirdPartyPartitioned2"
+ );
+ },
+ // Send clear-site-data header in partitioned third party context.
+ "thirdPartyPartitioned",
+ true
+ );
+});
+
+/**
+ * Tests that when the we load a path with "Clear-Site-Data" at top level, the
+ * cookies are cleared only in the first party context.
+ */
+add_task(async function cookieClearFirstParty() {
+ await runClearSiteDataTest(
+ // Pre Clear-Site-Data
+ () => setupInitialStorageState("cookie"),
+ // Post Clear-Site-Data
+ () => {
+ info("Testing: First party cookie has been cleared");
+ testHasCookie(false, ORIGIN_A, null);
+ info("Testing: Unpartitioned cookie has not changed");
+ testHasCookie(true, ORIGIN_B, null, STORAGE_KEY, "thirdParty");
+ info("Testing: Partitioned cookie for HOST_B (HOST_A) has not changed");
+ testHasCookie(
+ true,
+ ORIGIN_B,
+ ORIGIN_A,
+ STORAGE_KEY,
+ "thirdPartyPartitioned"
+ );
+ info("Testing: Partitioned cookie for HOST_A (HOST_B) has not changed");
+ testHasCookie(
+ true,
+ ORIGIN_A,
+ ORIGIN_B,
+ STORAGE_KEY,
+ "thirdPartyPartitioned2"
+ );
+ },
+ // Send clear-site-data header in first party context.
+ "firstParty"
+ );
+});
+
+/**
+ * Test clearing partitioned localStorage via clear-site-data header
+ * (Cleared via the quota manager).
+ */
+
+/**
+ * Tests that when a storage partitioned third party frame loads a site with
+ * "Clear-Site-Data", localStorage is cleared for only that partitioned frame.
+ */
+add_task(async function localStorageClearThirdParty() {
+ // Bug 1688221, Bug 1688665.
+ if (skipLocalStorageTests) {
+ info("Skipping test");
+ return;
+ }
+ await runClearSiteDataTest(
+ // Pre Clear-Site-Data
+ () => setupInitialStorageState("localStorage"),
+ // Post Clear-Site-Data
+ async () => {
+ info("Testing: First party localStorage has not changed");
+ testHasLocalStorageEntry(true, ORIGIN_A, null, STORAGE_KEY, "firstParty");
+ info("Testing: Unpartitioned localStorage has not changed");
+ testHasLocalStorageEntry(true, ORIGIN_B, null, STORAGE_KEY, "thirdParty");
+ info(
+ "Testing: Partitioned localStorage for HOST_B (HOST_A) has been cleared"
+ );
+ testHasLocalStorageEntry(false, ORIGIN_B, ORIGIN_A, STORAGE_KEY);
+ info(
+ "Testing: Partitioned localStorage for HOST_A (HOST_B) has not changed"
+ );
+ testHasLocalStorageEntry(
+ true,
+ ORIGIN_A,
+ ORIGIN_B,
+ STORAGE_KEY,
+ "thirdPartyPartitioned2"
+ );
+ },
+ // Send clear-site-data header in partitioned third party context.
+ "thirdPartyPartitioned"
+ );
+});
+
+/**
+ * Tests that when the we load a path with "Clear-Site-Data" at top level,
+ * localStorage is cleared only in the first party context.
+ */
+add_task(async function localStorageClearFirstParty() {
+ // Bug 1688221, Bug 1688665.
+ if (skipLocalStorageTests) {
+ info("Skipping test");
+ return;
+ }
+ await runClearSiteDataTest(
+ // Pre Clear-Site-Data
+ () => setupInitialStorageState("localStorage"),
+ // Post Clear-Site-Data
+ () => {
+ info("Testing: First party localStorage has been cleared");
+ testHasLocalStorageEntry(false, ORIGIN_A, null, STORAGE_KEY);
+ info("Testing: Unpartitioned thirdParty localStorage has not changed");
+ testHasLocalStorageEntry(true, ORIGIN_B, null, STORAGE_KEY, "thirdParty");
+ info(
+ "Testing: Partitioned localStorage for HOST_B (HOST_A) has not changed"
+ );
+ testHasLocalStorageEntry(
+ true,
+ ORIGIN_B,
+ ORIGIN_A,
+ STORAGE_KEY,
+ "thirdPartyPartitioned"
+ );
+ info(
+ "Testing: Partitioned localStorage for HOST_A (HOST_B) has not changed"
+ );
+ testHasLocalStorageEntry(
+ true,
+ ORIGIN_A,
+ ORIGIN_B,
+ STORAGE_KEY,
+ "thirdPartyPartitioned2"
+ );
+ },
+ // Send clear-site-data header in first party context.
+ "firstParty"
+ );
+});
diff --git a/toolkit/components/antitracking/test/browser/browser_partitionedConsoleMessage.js b/toolkit/components/antitracking/test/browser/browser_partitionedConsoleMessage.js
new file mode 100644
index 0000000000..ce74076825
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_partitionedConsoleMessage.js
@@ -0,0 +1,80 @@
+/* vim: set ts=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/. */
+
+/*
+ * Bug 1759496 - A test to verify if the console message of partitioned storage
+ * was sent correctly.
+ */
+
+"use strict";
+
+add_setup(async function () {
+ await setCookieBehaviorPref(
+ BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN,
+ false
+ );
+});
+
+add_task(async function runTest() {
+ info("Creating the tab");
+ let tab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE);
+ gBrowser.selectedTab = tab;
+
+ let browser = tab.linkedBrowser;
+ await BrowserTestUtils.browserLoaded(browser);
+
+ let consolePromise = new Promise(resolve => {
+ let consoleListener = {
+ observe(msg) {
+ if (
+ msg
+ .QueryInterface(Ci.nsIScriptError)
+ .category.startsWith("cookiePartitioned")
+ ) {
+ Services.console.unregisterListener(consoleListener);
+ resolve(msg.QueryInterface(Ci.nsIScriptError).errorMessage);
+ }
+ },
+ };
+
+ Services.console.registerListener(consoleListener);
+ });
+
+ info("Creating the third-party iframe");
+ let ifrBC = await SpecialPowers.spawn(
+ browser,
+ [TEST_TOP_PAGE_7],
+ async page => {
+ let ifr = content.document.createElement("iframe");
+
+ let loading = ContentTaskUtils.waitForEvent(ifr, "load");
+ content.document.body.appendChild(ifr);
+ ifr.src = page;
+ await loading;
+
+ return ifr.browsingContext;
+ }
+ );
+
+ info("Write cookie to the third-party iframe to ensure the console message");
+ await SpecialPowers.spawn(ifrBC, [], async _ => {
+ content.document.cookie = "foo";
+ });
+
+ let msg = await consolePromise;
+
+ ok(
+ msg.startsWith("Partitioned cookie or storage access was provided to"),
+ "The partitioned console message was sent correctly"
+ );
+
+ info("Clean up");
+ BrowserTestUtils.removeTab(tab);
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+});
diff --git a/toolkit/components/antitracking/test/browser/browser_partitionedCookies.js b/toolkit/components/antitracking/test/browser/browser_partitionedCookies.js
new file mode 100644
index 0000000000..d2d1e87dd4
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_partitionedCookies.js
@@ -0,0 +1,137 @@
+PartitionedStorageHelper.runTestInNormalAndPrivateMode(
+ "HTTP Cookies",
+ async (win3rdParty, win1stParty, allowed) => {
+ await win3rdParty.fetch("cookies.sjs?3rd").then(r => r.text());
+ await win3rdParty
+ .fetch("cookies.sjs")
+ .then(r => r.text())
+ .then(text => {
+ is(text, "cookie:foopy=3rd", "3rd party cookie set");
+ });
+
+ await win1stParty.fetch("cookies.sjs?first").then(r => r.text());
+ await win1stParty
+ .fetch("cookies.sjs")
+ .then(r => r.text())
+ .then(text => {
+ is(text, "cookie:foopy=first", "First party cookie set");
+ });
+
+ await win3rdParty
+ .fetch("cookies.sjs")
+ .then(r => r.text())
+ .then(text => {
+ if (allowed) {
+ is(
+ text,
+ "cookie:foopy=first",
+ "3rd party has the first party cookie set"
+ );
+ } else {
+ is(
+ text,
+ "cookie:foopy=3rd",
+ "3rd party has not the first party cookie set"
+ );
+ }
+ });
+ },
+
+ async _ => {
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+ }
+);
+
+PartitionedStorageHelper.runTestInNormalAndPrivateMode(
+ "DOM Cookies",
+ async (win3rdParty, win1stParty, allowed) => {
+ win3rdParty.document.cookie = "foo=3rd";
+ is(win3rdParty.document.cookie, "foo=3rd", "3rd party cookie set");
+
+ win1stParty.document.cookie = "foo=first";
+ is(win1stParty.document.cookie, "foo=first", "First party cookie set");
+
+ if (allowed) {
+ is(
+ win3rdParty.document.cookie,
+ "foo=first",
+ "3rd party has the first party cookie set"
+ );
+ } else {
+ is(
+ win3rdParty.document.cookie,
+ "foo=3rd",
+ "3rd party has not the first party cookie set"
+ );
+ }
+ },
+
+ async _ => {
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+ }
+);
+
+PartitionedStorageHelper.runPartitioningTestInNormalAndPrivateMode(
+ "Partitioned tabs - DOM Cookies",
+ "cookies",
+
+ // getDataCallback
+ async win => {
+ return win.document.cookie;
+ },
+
+ // addDataCallback
+ async (win, value) => {
+ win.document.cookie = value;
+ return true;
+ },
+
+ // cleanup
+ async _ => {
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+ },
+ true
+);
+
+PartitionedStorageHelper.runPartitioningTestInNormalAndPrivateMode(
+ "Partitioned tabs - Network Cookies",
+ "cookies",
+
+ // getDataCallback
+ async win => {
+ return win
+ .fetch("cookies.sjs")
+ .then(r => r.text())
+ .then(text => {
+ return text.substring("cookie:foopy=".length);
+ });
+ },
+
+ // addDataCallback
+ async (win, value) => {
+ await win.fetch("cookies.sjs?" + value).then(r => r.text());
+ return true;
+ },
+
+ // cleanup
+ async _ => {
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+ },
+ true
+);
diff --git a/toolkit/components/antitracking/test/browser/browser_partitionedDOMCache.js b/toolkit/components/antitracking/test/browser/browser_partitionedDOMCache.js
new file mode 100644
index 0000000000..93fc71b11e
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_partitionedDOMCache.js
@@ -0,0 +1,110 @@
+const APS_PREF =
+ "privacy.partition.always_partition_third_party_non_cookie_storage";
+
+PartitionedStorageHelper.runTest(
+ "DOMCache",
+ async (win3rdParty, win1stParty, allowed) => {
+ await win3rdParty.caches.open("wow").then(
+ _ => {
+ ok(allowed, "DOM Cache cannot be used!");
+ },
+ _ => {
+ ok(!allowed, "DOM Cache cannot be used!");
+ }
+ );
+
+ await win1stParty.caches.open("wow").then(
+ _ => {
+ ok(true, "DOM Cache should be available");
+ },
+ _ => {
+ ok(false, "DOM Cache should be available");
+ }
+ );
+ },
+
+ async _ => {
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+ },
+
+ [[APS_PREF, false]],
+
+ { runInSecureContext: true }
+);
+
+PartitionedStorageHelper.runTest(
+ "DOMCache",
+ async (win3rdParty, win1stParty, allowed) => {
+ await win1stParty.caches.open("wow").then(
+ async cache => {
+ ok(true, "DOM Cache should be available");
+ await cache.add("/");
+ },
+ _ => {
+ ok(false, "DOM Cache should be available");
+ }
+ );
+
+ await win3rdParty.caches.open("wow").then(
+ async cache => {
+ ok(true, "DOM Cache can be used!");
+ is(undefined, await cache.match("/"), "DOM Cache is partitioned");
+ },
+ _ => {
+ ok(false, "DOM Cache cannot be used!");
+ }
+ );
+ },
+
+ async _ => {
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+ },
+
+ [[APS_PREF, true]],
+
+ { runInSecureContext: true }
+);
+
+// Test that DOM cache is not available in PBM.
+PartitionedStorageHelper.runTest(
+ "DOMCache",
+ async (win3rdParty, win1stParty, allowed) => {
+ await win1stParty.caches.open("wow").then(
+ async cache => {
+ ok(false, "DOM Cache should not be available in PBM");
+ },
+ _ => {
+ ok(true, "DOM Cache should not be available in PBM");
+ }
+ );
+
+ await win3rdParty.caches.open("wow").then(
+ async cache => {
+ ok(false, "DOM Cache should not be available in PBM");
+ },
+ _ => {
+ ok(true, "DOM Cache should not be available in PBM");
+ }
+ );
+ },
+
+ async _ => {
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+ },
+
+ [],
+
+ { runInSecureContext: true, runInPrivateWindow: true }
+);
diff --git a/toolkit/components/antitracking/test/browser/browser_partitionedIndexedDB.js b/toolkit/components/antitracking/test/browser/browser_partitionedIndexedDB.js
new file mode 100644
index 0000000000..70dd0b03db
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_partitionedIndexedDB.js
@@ -0,0 +1,95 @@
+PartitionedStorageHelper.runTest(
+ "IndexedDB",
+ async (win3rdParty, win1stParty, allowed) => {
+ await new Promise(resolve => {
+ let a = win1stParty.indexedDB.open("test", 1);
+ ok(!!a, "IDB should not be blocked in 1st party contexts");
+
+ a.onsuccess = e => {
+ let db = e.target.result;
+ is(db.objectStoreNames.length, 1, "We have 1 objectStore");
+ is(db.objectStoreNames[0], "foobar", "We have 'foobar' objectStore");
+ resolve();
+ };
+
+ a.onupgradeneeded = e => {
+ let db = e.target.result;
+ is(db.objectStoreNames.length, 0, "We have 0 objectStores");
+ db.createObjectStore("foobar", { keyPath: "test" });
+ };
+ });
+
+ await new Promise(resolve => {
+ let a = win3rdParty.indexedDB.open("test", 1);
+ ok(!!a, "IDB should not be blocked in 3rd party contexts");
+
+ a.onsuccess = e => {
+ let db = e.target.result;
+
+ is(db.objectStoreNames.length, 0, "We have 0 objectStore");
+ resolve();
+ };
+ });
+ },
+
+ async _ => {
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+ }
+);
+
+PartitionedStorageHelper.runPartitioningTest(
+ "Partitioned tabs - IndexedDB",
+ "indexeddb",
+
+ // getDataCallback
+ async win => {
+ return new Promise(resolve => {
+ let a = win.indexedDB.open("test", 1);
+
+ a.onupgradeneeded = e => {
+ let db = e.target.result;
+ db.createObjectStore("foobar", { keyPath: "id" });
+ };
+
+ a.onsuccess = e => {
+ let db = e.target.result;
+ db.transaction("foobar").objectStore("foobar").get(1).onsuccess =
+ ee => {
+ resolve(
+ ee.target.result === undefined ? "" : ee.target.result.value
+ );
+ };
+ };
+ });
+ },
+
+ // addDataCallback
+ async (win, value) => {
+ return new Promise(resolve => {
+ let a = win.indexedDB.open("test", 1);
+
+ a.onsuccess = e => {
+ let db = e.target.result;
+ db
+ .transaction("foobar", "readwrite")
+ .objectStore("foobar")
+ .put({ id: 1, value }).onsuccess = _ => {
+ resolve(true);
+ };
+ };
+ });
+ },
+
+ // cleanup
+ async _ => {
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+ }
+);
diff --git a/toolkit/components/antitracking/test/browser/browser_partitionedLocalStorage.js b/toolkit/components/antitracking/test/browser/browser_partitionedLocalStorage.js
new file mode 100644
index 0000000000..fe45970132
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_partitionedLocalStorage.js
@@ -0,0 +1,115 @@
+AntiTracking.runTestInNormalAndPrivateMode(
+ "localStorage and Storage Access API",
+ async _ => {
+ /* import-globals-from storageAccessAPIHelpers.js */
+ await noStorageAccessInitially();
+
+ let effectiveCookieBehavior = SpecialPowers.isContentWindowPrivate(window)
+ ? SpecialPowers.Services.prefs.getIntPref(
+ "network.cookie.cookieBehavior.pbmode"
+ )
+ : SpecialPowers.Services.prefs.getIntPref(
+ "network.cookie.cookieBehavior"
+ );
+
+ let shouldThrow = [
+ SpecialPowers.Ci.nsICookieService.BEHAVIOR_REJECT,
+ SpecialPowers.Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN,
+ ].includes(effectiveCookieBehavior);
+
+ let hasThrown;
+ try {
+ localStorage.foo = 42;
+ ok(true, "LocalStorage is allowed");
+ is(localStorage.foo, "42", "The value matches");
+ hasThrown = false;
+ } catch (e) {
+ is(e.name, "SecurityError", "We want a security error message.");
+ hasThrown = true;
+ }
+
+ is(hasThrown, shouldThrow, "LocalStorage has been exposed correctly");
+
+ let prevLocalStorage;
+ if (!shouldThrow) {
+ prevLocalStorage = localStorage;
+ }
+
+ /* import-globals-from storageAccessAPIHelpers.js */
+ await callRequestStorageAccess();
+
+ if (shouldThrow) {
+ try {
+ is(localStorage.foo, undefined, "Undefined value after.");
+ ok(false, "localStorage should not be available");
+ } catch (e) {
+ ok(true, "localStorage should not be available");
+ }
+ } else {
+ ok(localStorage != prevLocalStorage, "We have a new localStorage");
+ is(localStorage.foo, undefined, "Undefined value after.");
+
+ localStorage.foo = 42;
+ ok(true, "LocalStorage is still allowed");
+ is(localStorage.foo, "42", "The value matches");
+ }
+ },
+ async _ => {
+ /* import-globals-from storageAccessAPIHelpers.js */
+ await hasStorageAccessInitially();
+
+ localStorage.foo = 42;
+ ok(true, "LocalStorage is allowed");
+ is(localStorage.foo, "42", "The value matches");
+
+ var prevLocalStorage = localStorage;
+
+ /* import-globals-from storageAccessAPIHelpers.js */
+ await callRequestStorageAccess();
+
+ // For non-tracking windows, calling the API is a no-op
+ ok(localStorage == prevLocalStorage, "We have a new localStorage");
+ is(localStorage.foo, "42", "The value matches");
+ ok(true, "LocalStorage is allowed");
+ },
+ async _ => {
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+ },
+ [
+ [
+ "privacy.restrict3rdpartystorage.partitionedHosts",
+ "tracking.example.org,tracking.example.com",
+ ],
+ ],
+ false,
+ false
+);
+
+PartitionedStorageHelper.runPartitioningTestInNormalAndPrivateMode(
+ "Partitioned tabs - localStorage",
+ "localstorage",
+
+ // getDataCallback
+ async win => {
+ return "foo" in win.localStorage ? win.localStorage.foo : "";
+ },
+
+ // addDataCallback
+ async (win, value) => {
+ win.localStorage.foo = value;
+ return true;
+ },
+
+ // cleanup
+ async _ => {
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+ }
+);
diff --git a/toolkit/components/antitracking/test/browser/browser_partitionedLocalStorage_events.js b/toolkit/components/antitracking/test/browser/browser_partitionedLocalStorage_events.js
new file mode 100644
index 0000000000..14ca62d062
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_partitionedLocalStorage_events.js
@@ -0,0 +1,1014 @@
+function log(test) {
+ if ("iteration" in test) {
+ info(
+ `Running test with prefValue: ${test.prefValue} (Test #${
+ test.iteration + 1
+ })`
+ );
+ test.iteration++;
+ } else {
+ test.iteration = 0;
+ log(test);
+ }
+}
+
+function runAllTests(prefValue) {
+ const storagePrincipalTest =
+ prefValue == Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER;
+ const dynamicFPITest =
+ prefValue ==
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN;
+
+ const test = { dynamicFPITest, prefValue };
+
+ let thirdPartyDomain;
+ if (storagePrincipalTest) {
+ thirdPartyDomain = TEST_3RD_PARTY_DOMAIN;
+ }
+ if (dynamicFPITest) {
+ thirdPartyDomain = TEST_4TH_PARTY_DOMAIN;
+ }
+ ok(thirdPartyDomain, "Sanity check");
+
+ // A same origin (and same-process via setting "dom.ipc.processCount" to 1)
+ // top-level window with access to real localStorage does not share storage
+ // with an ePartitionOrDeny iframe that should have PartitionedLocalStorage and
+ // no storage events are received in either direction. (Same-process in order
+ // to avoid having to worry about any e10s propagation issues.)
+ add_task(async _ => {
+ log(test);
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.ipc.processCount", 1],
+ ["network.cookie.cookieBehavior", prefValue],
+ ["network.cookie.cookieBehavior.pbmode", prefValue],
+ ["privacy.trackingprotection.enabled", false],
+ ["privacy.trackingprotection.pbmode.enabled", false],
+ ["privacy.trackingprotection.annotate_channels", true],
+ [
+ "privacy.restrict3rdpartystorage.partitionedHosts",
+ "tracking.example.org,not-tracking.example.com",
+ ],
+ ],
+ });
+
+ await UrlClassifierTestUtils.addTestTrackers();
+
+ info("Creating a non-tracker top-level context");
+ let normalTab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE);
+ let normalBrowser = gBrowser.getBrowserForTab(normalTab);
+ await BrowserTestUtils.browserLoaded(normalBrowser);
+
+ info("Creating a tracker top-level context");
+ let trackerTab = BrowserTestUtils.addTab(
+ gBrowser,
+ thirdPartyDomain + TEST_PATH + "page.html"
+ );
+ let trackerBrowser = gBrowser.getBrowserForTab(trackerTab);
+ await BrowserTestUtils.browserLoaded(trackerBrowser);
+
+ info("The non-tracker page opens a tracker iframe");
+ await SpecialPowers.spawn(
+ normalBrowser,
+ [
+ {
+ page: thirdPartyDomain + TEST_PATH + "localStorageEvents.html",
+ },
+ ],
+ async obj => {
+ let ifr = content.document.createElement("iframe");
+ ifr.setAttribute("id", "ifr");
+ ifr.setAttribute("src", obj.page);
+
+ info("Iframe loading...");
+ await new content.Promise(resolve => {
+ ifr.onload = resolve;
+ content.document.body.appendChild(ifr);
+ });
+
+ info("Setting localStorage value...");
+ ifr.contentWindow.postMessage("setValue", "*");
+
+ info("Getting the value...");
+ let value = await new Promise(resolve => {
+ content.addEventListener(
+ "message",
+ e => {
+ resolve(e.data);
+ },
+ { once: true }
+ );
+ ifr.contentWindow.postMessage("getValue", "*");
+ });
+
+ ok(
+ value.startsWith("tracker-"),
+ "The value is correctly set by the tracker"
+ );
+ }
+ );
+
+ info("The tracker page should not have received events");
+ await SpecialPowers.spawn(trackerBrowser, [], async _ => {
+ is(content.localStorage.foo, undefined, "Undefined value!");
+ content.localStorage.foo = "normal-" + Math.random();
+ });
+
+ info("Let's see if non-tracker page has received events");
+ await SpecialPowers.spawn(normalBrowser, [], async _ => {
+ let ifr = content.document.getElementById("ifr");
+
+ info("Getting the value...");
+ let value = await new Promise(resolve => {
+ content.addEventListener(
+ "message",
+ e => {
+ resolve(e.data);
+ },
+ { once: true }
+ );
+ ifr.contentWindow.postMessage("getValue", "*");
+ });
+
+ ok(
+ value.startsWith("tracker-"),
+ "The value is correctly set by the tracker"
+ );
+
+ info("Getting the events...");
+ let events = await new Promise(resolve => {
+ content.addEventListener(
+ "message",
+ e => {
+ resolve(e.data);
+ },
+ { once: true }
+ );
+ ifr.contentWindow.postMessage("getEvents", "*");
+ });
+
+ is(events, 0, "No events");
+ });
+
+ BrowserTestUtils.removeTab(trackerTab);
+ BrowserTestUtils.removeTab(normalTab);
+
+ UrlClassifierTestUtils.cleanupTestTrackers();
+ });
+
+ // Two ePartitionOrDeny iframes in the same tab in the same origin see the
+ // same localStorage values but no storage events are received from each other
+ // if dFPI is disabled.
+ add_task(async _ => {
+ log(test);
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.ipc.processCount", 1],
+ ["network.cookie.cookieBehavior", prefValue],
+ ["network.cookie.cookieBehavior.pbmode", prefValue],
+ ["privacy.trackingprotection.enabled", false],
+ ["privacy.trackingprotection.pbmode.enabled", false],
+ ["privacy.trackingprotection.annotate_channels", true],
+ [
+ "privacy.restrict3rdpartystorage.partitionedHosts",
+ "tracking.example.org,not-tracking.example.com",
+ ],
+ ],
+ });
+
+ await UrlClassifierTestUtils.addTestTrackers();
+
+ info("Creating a non-tracker top-level context");
+ let normalTab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE);
+ let normalBrowser = gBrowser.getBrowserForTab(normalTab);
+ await BrowserTestUtils.browserLoaded(normalBrowser);
+
+ info("The non-tracker page opens a tracker iframe");
+ await SpecialPowers.spawn(
+ normalBrowser,
+ [
+ {
+ page: thirdPartyDomain + TEST_PATH + "localStorageEvents.html",
+ dynamicFPITest: test.dynamicFPITest,
+ },
+ ],
+ async obj => {
+ let ifr1 = content.document.createElement("iframe");
+ ifr1.setAttribute("id", "ifr1");
+ ifr1.setAttribute("src", obj.page);
+
+ info("Iframe 1 loading...");
+ await new content.Promise(resolve => {
+ ifr1.onload = resolve;
+ content.document.body.appendChild(ifr1);
+ });
+
+ let ifr2 = content.document.createElement("iframe");
+ ifr2.setAttribute("id", "ifr2");
+ ifr2.setAttribute("src", obj.page);
+
+ info("Iframe 2 loading...");
+ await new content.Promise(resolve => {
+ ifr2.onload = resolve;
+ content.document.body.appendChild(ifr2);
+ });
+
+ info("Setting localStorage value in ifr1...");
+ ifr1.contentWindow.postMessage("setValue", "*");
+
+ info("Getting the value from ifr1...");
+ let value = await new Promise(resolve => {
+ content.addEventListener(
+ "message",
+ e => {
+ resolve(e.data);
+ },
+ { once: true }
+ );
+ ifr1.contentWindow.postMessage("getValue", "*");
+ });
+
+ ok(value.startsWith("tracker-"), "The value is correctly set in ifr1");
+
+ info("Getting the value from ifr2...");
+ value = await new Promise(resolve => {
+ content.addEventListener(
+ "message",
+ e => {
+ resolve(e.data);
+ },
+ { once: true }
+ );
+ ifr2.contentWindow.postMessage("getValue", "*");
+ });
+
+ if (obj.dynamicFPITest) {
+ ok(
+ value.startsWith("tracker-"),
+ "The value is correctly set in ifr2"
+ );
+ } else {
+ is(value, null, "The value is not set in ifr2");
+ }
+
+ info("Getting the events received by ifr2...");
+ let events = await new Promise(resolve => {
+ content.addEventListener(
+ "message",
+ e => {
+ resolve(e.data);
+ },
+ { once: true }
+ );
+ ifr2.contentWindow.postMessage("getEvents", "*");
+ });
+
+ if (obj.dynamicFPITest) {
+ is(events, 1, "one event");
+ } else {
+ is(events, 0, "No events");
+ }
+ }
+ );
+
+ BrowserTestUtils.removeTab(normalTab);
+
+ UrlClassifierTestUtils.cleanupTestTrackers();
+ });
+
+ // Same as the previous test but with a cookie behavior of BEHAVIOR_ACCEPT
+ // instead of BEHAVIOR_REJECT_TRACKER so the iframes get real, persistent
+ // localStorage instead of partitioned localStorage.
+ add_task(async _ => {
+ log(test);
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.ipc.processCount", 1],
+ ["network.cookie.cookieBehavior", Ci.nsICookieService.BEHAVIOR_ACCEPT],
+ [
+ "network.cookie.cookieBehavior.pbmode",
+ Ci.nsICookieService.BEHAVIOR_ACCEPT,
+ ],
+ ["privacy.trackingprotection.enabled", false],
+ ["privacy.trackingprotection.pbmode.enabled", false],
+ ["privacy.trackingprotection.annotate_channels", true],
+ [
+ "privacy.restrict3rdpartystorage.partitionedHosts",
+ "tracking.example.org,not-tracking.example.com",
+ ],
+ ],
+ });
+
+ await UrlClassifierTestUtils.addTestTrackers();
+
+ info("Creating a non-tracker top-level context");
+ let normalTab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE);
+ let normalBrowser = gBrowser.getBrowserForTab(normalTab);
+ await BrowserTestUtils.browserLoaded(normalBrowser);
+
+ info("The non-tracker page opens a tracker iframe");
+ await SpecialPowers.spawn(
+ normalBrowser,
+ [
+ {
+ page: thirdPartyDomain + TEST_PATH + "localStorageEvents.html",
+ },
+ ],
+ async obj => {
+ let ifr1 = content.document.createElement("iframe");
+ ifr1.setAttribute("id", "ifr1");
+ ifr1.setAttribute("src", obj.page);
+
+ info("Iframe 1 loading...");
+ await new content.Promise(resolve => {
+ ifr1.onload = resolve;
+ content.document.body.appendChild(ifr1);
+ });
+
+ let ifr2 = content.document.createElement("iframe");
+ ifr2.setAttribute("id", "ifr2");
+ ifr2.setAttribute("src", obj.page);
+
+ info("Iframe 2 loading...");
+ await new content.Promise(resolve => {
+ ifr2.onload = resolve;
+ content.document.body.appendChild(ifr2);
+ });
+
+ info("Setting localStorage value in ifr1...");
+ ifr1.contentWindow.postMessage("setValue", "*");
+
+ info("Getting the value from ifr1...");
+ let value1 = await new Promise(resolve => {
+ content.addEventListener(
+ "message",
+ e => {
+ resolve(e.data);
+ },
+ { once: true }
+ );
+ ifr1.contentWindow.postMessage("getValue", "*");
+ });
+
+ ok(value1.startsWith("tracker-"), "The value is correctly set in ifr1");
+
+ info("Getting the value from ifr2...");
+ let value2 = await new Promise(resolve => {
+ content.addEventListener(
+ "message",
+ e => {
+ resolve(e.data);
+ },
+ { once: true }
+ );
+ ifr2.contentWindow.postMessage("getValue", "*");
+ });
+
+ is(value2, value1, "The values match");
+
+ info("Getting the events received by ifr2...");
+ let events = await new Promise(resolve => {
+ content.addEventListener(
+ "message",
+ e => {
+ resolve(e.data);
+ },
+ { once: true }
+ );
+ ifr2.contentWindow.postMessage("getEvents", "*");
+ });
+
+ is(events, 1, "One event");
+ }
+ );
+
+ BrowserTestUtils.removeTab(normalTab);
+
+ UrlClassifierTestUtils.cleanupTestTrackers();
+ });
+
+ // An ePartitionOrDeny iframe navigated between two distinct pages on the same
+ // origin does not see the values stored by the previous iframe.
+ add_task(async _ => {
+ log(test);
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.ipc.processCount", 1],
+ ["network.cookie.cookieBehavior", prefValue],
+ ["network.cookie.cookieBehavior.pbmode", prefValue],
+ ["privacy.trackingprotection.enabled", false],
+ ["privacy.trackingprotection.pbmode.enabled", false],
+ ["privacy.trackingprotection.annotate_channels", true],
+ [
+ "privacy.restrict3rdpartystorage.partitionedHosts",
+ "tracking.example.org,not-tracking.example.com",
+ ],
+ ],
+ });
+
+ await UrlClassifierTestUtils.addTestTrackers();
+
+ info("Creating a non-tracker top-level context");
+ let normalTab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE);
+ let normalBrowser = gBrowser.getBrowserForTab(normalTab);
+ await BrowserTestUtils.browserLoaded(normalBrowser);
+
+ info("The non-tracker page opens a tracker iframe");
+ await SpecialPowers.spawn(
+ normalBrowser,
+ [
+ {
+ page: thirdPartyDomain + TEST_PATH + "localStorageEvents.html",
+ dynamicFPITest: test.dynamicFPITest,
+ },
+ ],
+ async obj => {
+ let ifr = content.document.createElement("iframe");
+ ifr.setAttribute("id", "ifr");
+ ifr.setAttribute("src", obj.page);
+
+ info("Iframe loading...");
+ await new content.Promise(resolve => {
+ ifr.onload = resolve;
+ content.document.body.appendChild(ifr);
+ });
+
+ info("Setting localStorage value in ifr...");
+ ifr.contentWindow.postMessage("setValue", "*");
+
+ info("Getting the value from ifr...");
+ let value = await new Promise(resolve => {
+ content.addEventListener(
+ "message",
+ e => {
+ resolve(e.data);
+ },
+ { once: true }
+ );
+ ifr.contentWindow.postMessage("getValue", "*");
+ });
+
+ ok(value.startsWith("tracker-"), "The value is correctly set in ifr");
+
+ info("Navigate...");
+ await new content.Promise(resolve => {
+ ifr.onload = resolve;
+ ifr.setAttribute("src", obj.page + "?" + Math.random());
+ });
+
+ info("Getting the value from ifr...");
+ let value2 = await new Promise(resolve => {
+ content.addEventListener(
+ "message",
+ e => {
+ resolve(e.data);
+ },
+ { once: true }
+ );
+ ifr.contentWindow.postMessage("getValue", "*");
+ });
+
+ if (obj.dynamicFPITest) {
+ is(value, value2, "The value is received");
+ } else {
+ is(value2, null, "The value is undefined");
+ }
+ }
+ );
+
+ BrowserTestUtils.removeTab(normalTab);
+
+ UrlClassifierTestUtils.cleanupTestTrackers();
+ });
+
+ // Like the previous test, but accepting trackers
+ add_task(async _ => {
+ log(test);
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.ipc.processCount", 1],
+ ["network.cookie.cookieBehavior", Ci.nsICookieService.BEHAVIOR_ACCEPT],
+ [
+ "network.cookie.cookieBehavior.pbmode",
+ Ci.nsICookieService.BEHAVIOR_ACCEPT,
+ ],
+ ["privacy.trackingprotection.enabled", false],
+ ["privacy.trackingprotection.pbmode.enabled", false],
+ ["privacy.trackingprotection.annotate_channels", true],
+ [
+ "privacy.restrict3rdpartystorage.partitionedHosts",
+ "tracking.example.org,not-tracking.example.com",
+ ],
+ ],
+ });
+
+ await UrlClassifierTestUtils.addTestTrackers();
+
+ info("Creating a non-tracker top-level context");
+ let normalTab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE);
+ let normalBrowser = gBrowser.getBrowserForTab(normalTab);
+ await BrowserTestUtils.browserLoaded(normalBrowser);
+
+ info("The non-tracker page opens a tracker iframe");
+ await SpecialPowers.spawn(
+ normalBrowser,
+ [
+ {
+ page: thirdPartyDomain + TEST_PATH + "localStorageEvents.html",
+ },
+ ],
+ async obj => {
+ let ifr = content.document.createElement("iframe");
+ ifr.setAttribute("id", "ifr");
+ ifr.setAttribute("src", obj.page);
+
+ info("Iframe loading...");
+ await new content.Promise(resolve => {
+ ifr.onload = resolve;
+ content.document.body.appendChild(ifr);
+ });
+
+ info("Setting localStorage value in ifr...");
+ ifr.contentWindow.postMessage("setValue", "*");
+
+ info("Getting the value from ifr...");
+ let value = await new Promise(resolve => {
+ content.addEventListener(
+ "message",
+ e => {
+ resolve(e.data);
+ },
+ { once: true }
+ );
+ ifr.contentWindow.postMessage("getValue", "*");
+ });
+
+ ok(value.startsWith("tracker-"), "The value is correctly set in ifr");
+
+ info("Navigate...");
+ await new content.Promise(resolve => {
+ ifr.onload = resolve;
+ ifr.setAttribute("src", obj.page + "?" + Math.random());
+ });
+
+ info("Getting the value from ifr...");
+ let value2 = await new Promise(resolve => {
+ content.addEventListener(
+ "message",
+ e => {
+ resolve(e.data);
+ },
+ { once: true }
+ );
+ ifr.contentWindow.postMessage("getValue", "*");
+ });
+
+ is(value, value2, "The value is received");
+ }
+ );
+
+ BrowserTestUtils.removeTab(normalTab);
+
+ UrlClassifierTestUtils.cleanupTestTrackers();
+ });
+
+ // An ePartitionOrDeny iframe on the same origin that is navigated to itself
+ // via window.location.reload() or equivalent does not see the values stored
+ // by its previous self.
+ add_task(async _ => {
+ log(test);
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.ipc.processCount", 1],
+ ["network.cookie.cookieBehavior", prefValue],
+ ["network.cookie.cookieBehavior.pbmode", prefValue],
+ ["privacy.trackingprotection.enabled", false],
+ ["privacy.trackingprotection.pbmode.enabled", false],
+ ["privacy.trackingprotection.annotate_channels", true],
+ [
+ "privacy.restrict3rdpartystorage.partitionedHosts",
+ "tracking.example.org,not-tracking.example.com",
+ ],
+ ],
+ });
+
+ await UrlClassifierTestUtils.addTestTrackers();
+
+ info("Creating a non-tracker top-level context");
+ let normalTab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE);
+ let normalBrowser = gBrowser.getBrowserForTab(normalTab);
+ await BrowserTestUtils.browserLoaded(normalBrowser);
+
+ info("The non-tracker page opens a tracker iframe");
+ await SpecialPowers.spawn(
+ normalBrowser,
+ [
+ {
+ page: thirdPartyDomain + TEST_PATH + "localStorageEvents.html",
+ dynamicFPITest: test.dynamicFPITest,
+ },
+ ],
+ async obj => {
+ let ifr = content.document.createElement("iframe");
+ ifr.setAttribute("id", "ifr");
+ ifr.setAttribute("src", obj.page);
+
+ info("Iframe loading...");
+ await new content.Promise(resolve => {
+ ifr.onload = resolve;
+ content.document.body.appendChild(ifr);
+ });
+
+ info("Setting localStorage value in ifr...");
+ ifr.contentWindow.postMessage("setValue", "*");
+
+ info("Getting the value from ifr...");
+ let value = await new Promise(resolve => {
+ content.addEventListener(
+ "message",
+ e => {
+ resolve(e.data);
+ },
+ { once: true }
+ );
+ ifr.contentWindow.postMessage("getValue", "*");
+ });
+
+ ok(value.startsWith("tracker-"), "The value is correctly set in ifr");
+
+ info("Reload...");
+ await new content.Promise(resolve => {
+ ifr.onload = resolve;
+ ifr.contentWindow.postMessage("reload", "*");
+ });
+
+ info("Getting the value from ifr...");
+ let value2 = await new Promise(resolve => {
+ content.addEventListener(
+ "message",
+ e => {
+ resolve(e.data);
+ },
+ { once: true }
+ );
+ ifr.contentWindow.postMessage("getValue", "*");
+ });
+
+ if (obj.dynamicFPITest) {
+ is(value, value2, "The value is equal");
+ } else {
+ is(value2, null, "The value is undefined");
+ }
+ }
+ );
+
+ BrowserTestUtils.removeTab(normalTab);
+
+ UrlClassifierTestUtils.cleanupTestTrackers();
+ });
+
+ // Like the previous test, but accepting trackers
+ add_task(async _ => {
+ log(test);
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.ipc.processCount", 1],
+ ["network.cookie.cookieBehavior", Ci.nsICookieService.BEHAVIOR_ACCEPT],
+ [
+ "network.cookie.cookieBehavior.pbmode",
+ Ci.nsICookieService.BEHAVIOR_ACCEPT,
+ ],
+ ["privacy.trackingprotection.enabled", false],
+ ["privacy.trackingprotection.pbmode.enabled", false],
+ ["privacy.trackingprotection.annotate_channels", true],
+ [
+ "privacy.restrict3rdpartystorage.partitionedHosts",
+ "tracking.example.org,not-tracking.example.com",
+ ],
+ ],
+ });
+
+ await UrlClassifierTestUtils.addTestTrackers();
+
+ info("Creating a non-tracker top-level context");
+ let normalTab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE);
+ let normalBrowser = gBrowser.getBrowserForTab(normalTab);
+ await BrowserTestUtils.browserLoaded(normalBrowser);
+
+ info("The non-tracker page opens a tracker iframe");
+ await SpecialPowers.spawn(
+ normalBrowser,
+ [
+ {
+ page: thirdPartyDomain + TEST_PATH + "localStorageEvents.html",
+ },
+ ],
+ async obj => {
+ let ifr = content.document.createElement("iframe");
+ ifr.setAttribute("id", "ifr");
+ ifr.setAttribute("src", obj.page);
+
+ info("Iframe loading...");
+ await new content.Promise(resolve => {
+ ifr.onload = resolve;
+ content.document.body.appendChild(ifr);
+ });
+
+ info("Setting localStorage value in ifr...");
+ ifr.contentWindow.postMessage("setValue", "*");
+
+ info("Getting the value from ifr...");
+ let value = await new Promise(resolve => {
+ content.addEventListener(
+ "message",
+ e => {
+ resolve(e.data);
+ },
+ { once: true }
+ );
+ ifr.contentWindow.postMessage("getValue", "*");
+ });
+
+ ok(value.startsWith("tracker-"), "The value is correctly set in ifr");
+
+ info("Reload...");
+ await new content.Promise(resolve => {
+ ifr.onload = resolve;
+ ifr.contentWindow.postMessage("reload", "*");
+ });
+
+ info("Getting the value from ifr...");
+ let value2 = await new Promise(resolve => {
+ content.addEventListener(
+ "message",
+ e => {
+ resolve(e.data);
+ },
+ { once: true }
+ );
+ ifr.contentWindow.postMessage("getValue", "*");
+ });
+
+ is(value, value2, "The value is equal");
+ }
+ );
+
+ BrowserTestUtils.removeTab(normalTab);
+
+ UrlClassifierTestUtils.cleanupTestTrackers();
+ });
+
+ // An ePartitionOrDeny iframe on different top-level domain tabs
+ add_task(async _ => {
+ log(test);
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.ipc.processCount", 1],
+ ["network.cookie.cookieBehavior", prefValue],
+ ["network.cookie.cookieBehavior.pbmode", prefValue],
+ ["privacy.firstparty.isolate", false],
+ ["privacy.trackingprotection.enabled", false],
+ ["privacy.trackingprotection.pbmode.enabled", false],
+ ["privacy.trackingprotection.annotate_channels", true],
+ [
+ "privacy.restrict3rdpartystorage.partitionedHosts",
+ "tracking.example.org,not-tracking.example.com",
+ ],
+ ],
+ });
+
+ await UrlClassifierTestUtils.addTestTrackers();
+
+ info("Creating a non-tracker top-level context");
+ let normalTab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE);
+ let normalBrowser = gBrowser.getBrowserForTab(normalTab);
+ await BrowserTestUtils.browserLoaded(normalBrowser);
+
+ info("The non-tracker page opens a tracker iframe");
+ let result1 = await SpecialPowers.spawn(
+ normalBrowser,
+ [
+ {
+ page: thirdPartyDomain + TEST_PATH + "localStorageEvents.html",
+ },
+ ],
+ async obj => {
+ let ifr = content.document.createElement("iframe");
+ ifr.setAttribute("id", "ifr");
+ ifr.setAttribute("src", obj.page);
+
+ info("Iframe loading...");
+ await new content.Promise(resolve => {
+ ifr.onload = resolve;
+ content.document.body.appendChild(ifr);
+ });
+
+ info("Setting localStorage value in ifr...");
+ ifr.contentWindow.postMessage("setValue", "*");
+
+ info("Getting the value from ifr...");
+ let value = await new Promise(resolve => {
+ content.addEventListener(
+ "message",
+ e => {
+ resolve(e.data);
+ },
+ { once: true }
+ );
+ ifr.contentWindow.postMessage("getValue", "*");
+ });
+
+ ok(value.startsWith("tracker-"), "The value is correctly set in ifr");
+ return value;
+ }
+ );
+ ok(result1.startsWith("tracker-"), "The value is correctly set in tab1");
+
+ info("Creating a non-tracker top-level context");
+ let normalTab2 = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE_2);
+ let normalBrowser2 = gBrowser.getBrowserForTab(normalTab2);
+ await BrowserTestUtils.browserLoaded(normalBrowser2);
+
+ info("The non-tracker page opens a tracker iframe");
+ let result2 = await SpecialPowers.spawn(
+ normalBrowser2,
+ [
+ {
+ page: thirdPartyDomain + TEST_PATH + "localStorageEvents.html",
+ },
+ ],
+ async obj => {
+ let ifr = content.document.createElement("iframe");
+ ifr.setAttribute("id", "ifr");
+ ifr.setAttribute("src", obj.page);
+
+ info("Iframe loading...");
+ await new content.Promise(resolve => {
+ ifr.onload = resolve;
+ content.document.body.appendChild(ifr);
+ });
+
+ info("Getting the value from ifr...");
+ let value = await new Promise(resolve => {
+ content.addEventListener(
+ "message",
+ e => {
+ resolve(e.data);
+ },
+ { once: true }
+ );
+ ifr.contentWindow.postMessage("getValue", "*");
+ });
+ return value;
+ }
+ );
+
+ ok(!result2, "The value is null");
+
+ BrowserTestUtils.removeTab(normalTab);
+ BrowserTestUtils.removeTab(normalTab2);
+
+ UrlClassifierTestUtils.cleanupTestTrackers();
+ });
+
+ // Like the previous test, but accepting trackers
+ add_task(async _ => {
+ log(test);
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.ipc.processCount", 1],
+ ["network.cookie.cookieBehavior", Ci.nsICookieService.BEHAVIOR_ACCEPT],
+ [
+ "network.cookie.cookieBehavior.pbmode",
+ Ci.nsICookieService.BEHAVIOR_ACCEPT,
+ ],
+ ["privacy.firstparty.isolate", false],
+ ["privacy.trackingprotection.enabled", false],
+ ["privacy.trackingprotection.pbmode.enabled", false],
+ ["privacy.trackingprotection.annotate_channels", true],
+ [
+ "privacy.restrict3rdpartystorage.partitionedHosts",
+ "tracking.example.org,not-tracking.example.com",
+ ],
+ ],
+ });
+
+ await UrlClassifierTestUtils.addTestTrackers();
+
+ info("Creating a non-tracker top-level context");
+ let normalTab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE);
+ let normalBrowser = gBrowser.getBrowserForTab(normalTab);
+ await BrowserTestUtils.browserLoaded(normalBrowser);
+
+ info("The non-tracker page opens a tracker iframe");
+ let result1 = await SpecialPowers.spawn(
+ normalBrowser,
+ [
+ {
+ page: thirdPartyDomain + TEST_PATH + "localStorageEvents.html",
+ },
+ ],
+ async obj => {
+ let ifr = content.document.createElement("iframe");
+ ifr.setAttribute("id", "ifr");
+ ifr.setAttribute("src", obj.page);
+
+ info("Iframe loading...");
+ await new content.Promise(resolve => {
+ ifr.onload = resolve;
+ content.document.body.appendChild(ifr);
+ });
+
+ info("Setting localStorage value in ifr...");
+ ifr.contentWindow.postMessage("setValue", "*");
+
+ info("Getting the value from ifr...");
+ let value = await new Promise(resolve => {
+ content.addEventListener(
+ "message",
+ e => {
+ resolve(e.data);
+ },
+ { once: true }
+ );
+ ifr.contentWindow.postMessage("getValue", "*");
+ });
+
+ ok(value.startsWith("tracker-"), "The value is correctly set in ifr");
+ return value;
+ }
+ );
+ ok(result1.startsWith("tracker-"), "The value is correctly set in tab1");
+
+ info("Creating a non-tracker top-level context");
+ let normalTab2 = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE_2);
+ let normalBrowser2 = gBrowser.getBrowserForTab(normalTab2);
+ await BrowserTestUtils.browserLoaded(normalBrowser2);
+
+ info("The non-tracker page opens a tracker iframe");
+ let result2 = await SpecialPowers.spawn(
+ normalBrowser2,
+ [
+ {
+ page: thirdPartyDomain + TEST_PATH + "localStorageEvents.html",
+ },
+ ],
+ async obj => {
+ let ifr = content.document.createElement("iframe");
+ ifr.setAttribute("id", "ifr");
+ ifr.setAttribute("src", obj.page);
+
+ info("Iframe loading...");
+ await new content.Promise(resolve => {
+ ifr.onload = resolve;
+ content.document.body.appendChild(ifr);
+ });
+
+ info("Getting the value from ifr...");
+ let value = await new Promise(resolve => {
+ content.addEventListener(
+ "message",
+ e => {
+ resolve(e.data);
+ },
+ { once: true }
+ );
+ ifr.contentWindow.postMessage("getValue", "*");
+ });
+ return value;
+ }
+ );
+
+ is(result1, result2, "The value is undefined");
+
+ BrowserTestUtils.removeTab(normalTab);
+ BrowserTestUtils.removeTab(normalTab2);
+
+ UrlClassifierTestUtils.cleanupTestTrackers();
+ });
+
+ // Cleanup data.
+ add_task(async _ => {
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+ });
+}
+
+for (let pref of [
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER,
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN,
+]) {
+ runAllTests(pref);
+}
diff --git a/toolkit/components/antitracking/test/browser/browser_partitionedMessaging.js b/toolkit/components/antitracking/test/browser/browser_partitionedMessaging.js
new file mode 100644
index 0000000000..683b1cc874
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_partitionedMessaging.js
@@ -0,0 +1,20 @@
+PartitionedStorageHelper.runTestInNormalAndPrivateMode(
+ "BroadcastChannel",
+ async (win3rdParty, win1stParty, allowed) => {
+ let a = new win3rdParty.BroadcastChannel("hello");
+ ok(!!a, "BroadcastChannel should be created by 3rd party iframe");
+
+ let b = new win1stParty.BroadcastChannel("hello");
+ ok(!!b, "BroadcastChannel should be created by 1st party iframe");
+
+ // BroadcastChannel uses the incument global, this means that its CTOR will
+ // always use the 3rd party iframe's window as global.
+ },
+ async _ => {
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+ }
+);
diff --git a/toolkit/components/antitracking/test/browser/browser_partitionedServiceWorkers.js b/toolkit/components/antitracking/test/browser/browser_partitionedServiceWorkers.js
new file mode 100644
index 0000000000..f45caba43a
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_partitionedServiceWorkers.js
@@ -0,0 +1,625 @@
+/* import-globals-from storageAccessAPIHelpers.js */
+
+PartitionedStorageHelper.runTest(
+ "ServiceWorkers - disable partitioning",
+ async (win3rdParty, win1stParty, allowed) => {
+ // Partitioned serviceWorkers are disabled in third-party context.
+ await win3rdParty.navigator.serviceWorker.register("empty.js").then(
+ _ => {
+ ok(allowed, "Success: ServiceWorker cannot be used!");
+ },
+ _ => {
+ ok(!allowed, "Failed: ServiceWorker cannot be used!");
+ }
+ );
+
+ await win1stParty.navigator.serviceWorker.register("empty.js").then(
+ _ => {
+ ok(true, "Success: ServiceWorker should be available!");
+ },
+ _ => {
+ ok(false, "Failed: ServiceWorker should be available!");
+ }
+ );
+ },
+
+ async _ => {
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+ },
+
+ [
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ["dom.ipc.processCount", 1],
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ ["privacy.partition.serviceWorkers", false],
+ ]
+);
+
+PartitionedStorageHelper.runTest(
+ "ServiceWorkers - enable partitioning",
+ async (win3rdParty, win1stParty, allowed) => {
+ // Partitioned serviceWorkers are enabled in third-party context.
+ await win3rdParty.navigator.serviceWorker.register("empty.js").then(
+ _ => {
+ ok(
+ true,
+ "Success: ServiceWorker should be available in third parties."
+ );
+ },
+ _ => {
+ ok(
+ false,
+ "Failed: ServiceWorker should be available in third parties."
+ );
+ }
+ );
+
+ await win1stParty.navigator.serviceWorker.register("empty.js").then(
+ _ => {
+ ok(true, "Success: ServiceWorker should be available!");
+ },
+ _ => {
+ ok(false, "Failed: ServiceWorker should be available!");
+ }
+ );
+ },
+
+ async _ => {
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+ },
+
+ [
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ["dom.ipc.processCount", 1],
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ ["privacy.partition.serviceWorkers", true],
+ ]
+);
+
+PartitionedStorageHelper.runTest(
+ "ServiceWorkers - MatchAll",
+ async (win3rdParty, win1stParty, allowed) => {
+ if (!win1stParty.sw) {
+ win1stParty.sw = await registerServiceWorker(win1stParty, "matchAll.js");
+ }
+
+ let msgPromise = new Promise(resolve => {
+ win1stParty.navigator.serviceWorker.addEventListener("message", msg => {
+ resolve(msg.data);
+ });
+ });
+
+ win1stParty.sw.postMessage(win3rdParty.location.href);
+ let msg = await msgPromise;
+
+ // The service worker will always be partitioned. So, the first party window
+ // won't have control on the third-party window.
+ is(
+ false,
+ msg,
+ "We won't have the 3rd party window controlled regardless of StorageAccess."
+ );
+ },
+
+ async _ => {
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+ },
+
+ [
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ["dom.ipc.processCount", 1],
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ ["privacy.partition.serviceWorkers", true],
+ ]
+);
+
+PartitionedStorageHelper.runTest(
+ "ServiceWorkers - Partition ScriptContext",
+ async (win3rdParty, win1stParty, allowed) => {
+ // Register service worker for the first-party window.
+ if (!win1stParty.sw) {
+ win1stParty.sw = await registerServiceWorker(
+ win1stParty,
+ "serviceWorker.js"
+ );
+ }
+
+ // Register service worker for the third-party window.
+ if (!win3rdParty.sw) {
+ win3rdParty.sw = await registerServiceWorker(
+ win3rdParty,
+ "serviceWorker.js"
+ );
+ }
+
+ // Set a script value to first-party service worker.
+ let res = await sendAndWaitWorkerMessage(
+ win1stParty.sw,
+ win1stParty.navigator.serviceWorker,
+ {
+ type: "SetScriptValue",
+ value: "1stParty",
+ }
+ );
+ ok(res.result, "OK", "Set script value to first-party service worker.");
+
+ // Set a script value to third-party service worker.
+ res = await sendAndWaitWorkerMessage(
+ win3rdParty.sw,
+ win3rdParty.navigator.serviceWorker,
+ {
+ type: "SetScriptValue",
+ value: "3rdParty",
+ }
+ );
+ ok(res.result, "OK", "Set script value to third-party service worker.");
+
+ // Get and check script value from the first-party service worker.
+ res = await sendAndWaitWorkerMessage(
+ win1stParty.sw,
+ win1stParty.navigator.serviceWorker,
+ { type: "GetScriptValue" }
+ );
+ is(
+ res.value,
+ "1stParty",
+ "The script value in first party window is correct"
+ );
+
+ // Get and check script value from the third-party service worker.
+ res = await sendAndWaitWorkerMessage(
+ win3rdParty.sw,
+ win3rdParty.navigator.serviceWorker,
+ { type: "GetScriptValue" }
+ );
+
+ is(
+ res.value,
+ "3rdParty",
+ "The script value in third party window is correct"
+ );
+ },
+
+ async _ => {
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+ },
+
+ [
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ["dom.ipc.processCount", 1],
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ ["privacy.partition.serviceWorkers", true],
+ ]
+);
+
+PartitionedStorageHelper.runTest(
+ "ServiceWorkers - Partition DOM Cache",
+ async (win3rdParty, win1stParty, allowed) => {
+ // Register service worker for the first-party window.
+ if (!win1stParty.sw) {
+ win1stParty.sw = await registerServiceWorker(
+ win1stParty,
+ "serviceWorker.js"
+ );
+ }
+
+ // Register service worker for the third-party window.
+ if (!win3rdParty.sw) {
+ win3rdParty.sw = await registerServiceWorker(
+ win3rdParty,
+ "serviceWorker.js"
+ );
+ }
+
+ // Set DOM cache to first-party service worker.
+ let res = await sendAndWaitWorkerMessage(
+ win1stParty.sw,
+ win1stParty.navigator.serviceWorker,
+ {
+ type: "SetCache",
+ value: "1stParty",
+ }
+ );
+ ok(res.result, "OK", "Set cache to first-party service worker.");
+
+ // Set DOM cache to third-party service worker.
+ res = await sendAndWaitWorkerMessage(
+ win3rdParty.sw,
+ win3rdParty.navigator.serviceWorker,
+ {
+ type: "SetCache",
+ value: "3rdParty",
+ }
+ );
+ ok(res.result, "OK", "Set cache to third-party service worker.");
+
+ // Check DOM cache from the first-party service worker.
+ res = await sendAndWaitWorkerMessage(
+ win1stParty.sw,
+ win1stParty.navigator.serviceWorker,
+ { type: "HasCache", value: "1stParty" }
+ );
+ ok(
+ res.value,
+ "The '1stParty' cache storage should exist for the first-party window."
+ );
+ res = await sendAndWaitWorkerMessage(
+ win1stParty.sw,
+ win1stParty.navigator.serviceWorker,
+ { type: "HasCache", value: "3rdParty" }
+ );
+ ok(
+ !res.value,
+ "The '3rdParty' cache storage should not exist for the first-party window."
+ );
+
+ // Check DOM cache from the third-party service worker.
+ res = await sendAndWaitWorkerMessage(
+ win3rdParty.sw,
+ win3rdParty.navigator.serviceWorker,
+ { type: "HasCache", value: "1stParty" }
+ );
+
+ ok(
+ !res.value,
+ "The '1stParty' cache storage should not exist for the third-party window."
+ );
+
+ res = await sendAndWaitWorkerMessage(
+ win3rdParty.sw,
+ win3rdParty.navigator.serviceWorker,
+ { type: "HasCache", value: "3rdParty" }
+ );
+
+ ok(
+ res.value,
+ "The '3rdParty' cache storage should exist for the third-party window."
+ );
+ },
+
+ async _ => {
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+ },
+
+ [
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ["dom.ipc.processCount", 1],
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ ["privacy.partition.serviceWorkers", true],
+ ]
+);
+
+PartitionedStorageHelper.runTest(
+ "ServiceWorkers - Partition IndexedDB",
+ async (win3rdParty, win1stParty, allowed) => {
+ // Register service worker for the first-party window.
+ if (!win1stParty.sw) {
+ win1stParty.sw = await registerServiceWorker(
+ win1stParty,
+ "serviceWorker.js"
+ );
+ }
+
+ // Register service worker for the third-party window.
+ if (!win3rdParty.sw) {
+ win3rdParty.sw = await registerServiceWorker(
+ win3rdParty,
+ "serviceWorker.js"
+ );
+ }
+
+ // Set indexedDB value to first-party service worker.
+ let res = await sendAndWaitWorkerMessage(
+ win1stParty.sw,
+ win1stParty.navigator.serviceWorker,
+ {
+ type: "SetIndexedDB",
+ value: "1stParty",
+ }
+ );
+ ok(res.result, "OK", "Set cache to first-party service worker.");
+
+ // Set indexedDB value to third-party service worker.
+ res = await sendAndWaitWorkerMessage(
+ win3rdParty.sw,
+ win3rdParty.navigator.serviceWorker,
+ {
+ type: "SetIndexedDB",
+ value: "3rdParty",
+ }
+ );
+ ok(res.result, "OK", "Set cache to third-party service worker.");
+
+ // Get and check indexedDB value from the first-party service worker.
+ res = await sendAndWaitWorkerMessage(
+ win1stParty.sw,
+ win1stParty.navigator.serviceWorker,
+ { type: "GetIndexedDB" }
+ );
+ is(
+ res.value,
+ "1stParty",
+ "The indexedDB value in first party window is correct"
+ );
+
+ // Get and check indexedDB from the third-party service worker.
+ res = await sendAndWaitWorkerMessage(
+ win3rdParty.sw,
+ win3rdParty.navigator.serviceWorker,
+ { type: "GetIndexedDB" }
+ );
+
+ is(
+ res.value,
+ "3rdParty",
+ "The indexedDB value in third party window is correct"
+ );
+ },
+
+ async _ => {
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+ },
+
+ [
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ["dom.ipc.processCount", 1],
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ ["privacy.partition.serviceWorkers", true],
+ ]
+);
+
+PartitionedStorageHelper.runTest(
+ "ServiceWorkers - Partition Intercept",
+ async (win3rdParty, win1stParty, allowed) => {
+ // Register service worker for the first-party window.
+ if (!win1stParty.sw) {
+ win1stParty.sw = await registerServiceWorker(
+ win1stParty,
+ "serviceWorker.js"
+ );
+ }
+
+ // Register service worker for the third-party window.
+ if (!win3rdParty.sw) {
+ win3rdParty.sw = await registerServiceWorker(
+ win3rdParty,
+ "serviceWorker.js"
+ );
+ }
+
+ // Fetch a resource in the first-party window.
+ await win1stParty.fetch("empty.js");
+
+ // Check that only the first-party service worker gets fetch event.
+ let res = await sendAndWaitWorkerMessage(
+ win1stParty.sw,
+ win1stParty.navigator.serviceWorker,
+ { type: "GetFetchURL" }
+ );
+ is(
+ res.value,
+ "http://not-tracking.example.com/browser/toolkit/components/antitracking/test/browser/empty.js",
+ "The first-party service worker received fetch event."
+ );
+ res = await sendAndWaitWorkerMessage(
+ win3rdParty.sw,
+ win3rdParty.navigator.serviceWorker,
+ { type: "GetFetchURL" }
+ );
+ is(
+ res.value,
+ "",
+ "The third-party service worker received no fetch event."
+ );
+
+ // Fetch a resource in the third-party window.
+ await win3rdParty.fetch("empty.js");
+
+ // Check if the fetch event only happens in third-party service worker.
+ res = await sendAndWaitWorkerMessage(
+ win1stParty.sw,
+ win1stParty.navigator.serviceWorker,
+ { type: "GetFetchURL" }
+ );
+ is(
+ res.value,
+ "",
+ "The first-party service worker received no fetch event."
+ );
+ res = await sendAndWaitWorkerMessage(
+ win3rdParty.sw,
+ win3rdParty.navigator.serviceWorker,
+ { type: "GetFetchURL" }
+ );
+ is(
+ res.value,
+ "http://not-tracking.example.com/browser/toolkit/components/antitracking/test/browser/empty.js",
+ "The third-party service worker received fetch event."
+ );
+ },
+
+ async _ => {
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+ },
+
+ [
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ["dom.ipc.processCount", 1],
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ ["privacy.partition.serviceWorkers", true],
+ ]
+);
+
+// Bug1743236 - Verify the content process won't crash if we create a dedicated
+// worker in a service worker controlled third-party page with Storage Access.
+PartitionedStorageHelper.runTest(
+ "ServiceWorkers - Create Dedicated Worker",
+ async (win3rdParty, win1stParty, allowed) => {
+ // We only do this test when the storage access is granted.
+ if (!allowed) {
+ return;
+ }
+
+ // Register service worker for the first-party window.
+ if (!win1stParty.sw) {
+ win1stParty.sw = await registerServiceWorker(
+ win1stParty,
+ "serviceWorker.js"
+ );
+ }
+
+ // Register service worker for the third-party window.
+ if (!win3rdParty.sw) {
+ win3rdParty.sw = await registerServiceWorker(
+ win3rdParty,
+ "serviceWorker.js"
+ );
+ }
+
+ // Create a dedicated worker in first-party window.
+ let firstPartyWorker = new win1stParty.Worker("dedicatedWorker.js");
+
+ // Post a message to the dedicated worker and wait until the message circles
+ // back.
+ await new Promise(resolve => {
+ firstPartyWorker.addEventListener("message", msg => {
+ if (msg.data == "1stParty") {
+ resolve();
+ }
+ });
+
+ firstPartyWorker.postMessage("1stParty");
+ });
+
+ // Create a dedicated worker in third-party window.
+ let thirdPartyWorker = new win3rdParty.Worker("dedicatedWorker.js");
+
+ // Post a message to the dedicated worker and wait until the message circles
+ // back.
+ await new Promise(resolve => {
+ thirdPartyWorker.addEventListener("message", msg => {
+ if (msg.data == "3rdParty") {
+ resolve();
+ }
+ });
+
+ thirdPartyWorker.postMessage("3rdParty");
+ });
+
+ firstPartyWorker.terminate();
+ thirdPartyWorker.terminate();
+ },
+
+ async _ => {
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+ },
+
+ [
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ["dom.ipc.processCount", 1],
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ ["privacy.partition.serviceWorkers", true],
+ ]
+);
+
+// Bug1768193 - Verify the parent process won't crash if we create a shared
+// worker in a service worker controlled third-party page with Storage Access.
+PartitionedStorageHelper.runTest(
+ "ServiceWorkers - Create Shared Worker",
+ async (win3rdParty, win1stParty, allowed) => {
+ // We only do this test when the storage access is granted.
+ if (!allowed) {
+ return;
+ }
+
+ // Register service worker for the first-party window.
+ if (!win1stParty.sw) {
+ win1stParty.sw = await registerServiceWorker(
+ win1stParty,
+ "serviceWorker.js"
+ );
+ }
+
+ // Register service worker for the third-party window.
+ if (!win3rdParty.sw) {
+ win3rdParty.sw = await registerServiceWorker(
+ win3rdParty,
+ "serviceWorker.js"
+ );
+ }
+
+ // Create a shared worker in third-party window.
+ let thirdPartyWorker = new win3rdParty.SharedWorker("sharedWorker.js");
+
+ // Post a message to the dedicated worker and wait until the message circles
+ // back.
+ await new Promise(resolve => {
+ thirdPartyWorker.port.onmessage = msg => {
+ resolve();
+ };
+ thirdPartyWorker.onerror = _ => {
+ ok(false, "We should not be here");
+ resolve();
+ };
+ thirdPartyWorker.port.postMessage("count");
+ });
+
+ thirdPartyWorker.port.postMessage("close");
+ },
+
+ async _ => {
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+ },
+
+ [
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ["dom.ipc.processCount", 1],
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ ["privacy.partition.serviceWorkers", true],
+ ]
+);
diff --git a/toolkit/components/antitracking/test/browser/browser_partitionedSharedWorkers.js b/toolkit/components/antitracking/test/browser/browser_partitionedSharedWorkers.js
new file mode 100644
index 0000000000..337d36b6e4
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_partitionedSharedWorkers.js
@@ -0,0 +1,74 @@
+PartitionedStorageHelper.runTestInNormalAndPrivateMode(
+ "SharedWorkers",
+ async (win3rdParty, win1stParty, allowed) => {
+ // This test fails if run with an HTTPS 3rd-party URL because the shared worker
+ // which would start from the window opened from 3rdPartyStorage.html will become
+ // secure context and per step 11.4.3 of
+ // https://html.spec.whatwg.org/multipage/workers.html#dom-sharedworker attempting
+ // to run the SharedWorker constructor would emit an error event.
+ is(
+ win3rdParty.location.protocol,
+ "http:",
+ "Our 3rd party URL shouldn't be HTTPS"
+ );
+
+ let sh1 = new win1stParty.SharedWorker("sharedWorker.js");
+ await new Promise(resolve => {
+ sh1.port.onmessage = e => {
+ is(e.data, 1, "We expected 1 connection");
+ resolve();
+ };
+ sh1.port.postMessage("count");
+ });
+
+ let sh3 = new win3rdParty.SharedWorker("sharedWorker.js");
+ await new Promise(resolve => {
+ sh3.port.onmessage = e => {
+ is(e.data, 1, `We expected 1 connection for 3rd party SharedWorker`);
+ resolve();
+ };
+ sh3.onerror = _ => {
+ ok(false, "We should not be here");
+ resolve();
+ };
+ sh3.port.postMessage("count");
+ });
+
+ sh1.port.postMessage("close");
+ sh3.port.postMessage("close");
+ },
+
+ async _ => {
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+ }
+);
+
+PartitionedStorageHelper.runPartitioningTestInNormalAndPrivateMode(
+ "Partitioned tabs - SharedWorker",
+ "sharedworker",
+
+ // getDataCallback
+ async win => {
+ win.sh = new win.SharedWorker("partitionedSharedWorker.js");
+ return new Promise(resolve => {
+ win.sh.port.onmessage = e => {
+ resolve(e.data);
+ };
+ win.sh.port.postMessage({ what: "get" });
+ });
+ },
+
+ // addDataCallback
+ async (win, value) => {
+ win.sh = new win.SharedWorker("partitionedSharedWorker.js");
+ win.sh.port.postMessage({ what: "put", value });
+ return true;
+ },
+
+ // cleanup
+ async _ => {}
+);
diff --git a/toolkit/components/antitracking/test/browser/browser_permissionInNormalWindows.js b/toolkit/components/antitracking/test/browser/browser_permissionInNormalWindows.js
new file mode 100644
index 0000000000..a414e7363b
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_permissionInNormalWindows.js
@@ -0,0 +1,106 @@
+AntiTracking.runTest(
+ "Test whether we receive any persistent permissions in normal windows",
+ // Blocking callback
+ async _ => {
+ // Nothing to do here!
+ },
+
+ // Non blocking callback
+ async _ => {
+ try {
+ // We load the test script in the parent process to check permissions.
+ let chromeScript = SpecialPowers.loadChromeScript(_ => {
+ /* eslint-env mozilla/chrome-script */
+ addMessageListener("go", _ => {
+ function ok(what, msg) {
+ sendAsyncMessage("ok", { what: !!what, msg });
+ }
+
+ function is(a, b, msg) {
+ ok(a === b, msg);
+ }
+
+ // We should use the principal of the TEST_DOMAIN since the storage
+ // permission is saved under it.
+ let principal =
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(
+ "http://example.net/"
+ );
+
+ for (let perm of Services.perms.getAllForPrincipal(principal)) {
+ // Ignore permissions other than storage access
+ if (!perm.type.startsWith("3rdPartyStorage^")) {
+ continue;
+ }
+ is(
+ perm.expireType,
+ Services.perms.EXPIRE_TIME,
+ "Permission must expire at a specific time"
+ );
+ ok(perm.expireTime > 0, "Permission must have a expiry time");
+ }
+
+ sendAsyncMessage("done");
+ });
+ });
+
+ chromeScript.addMessageListener("ok", obj => {
+ ok(obj.what, obj.msg);
+ });
+
+ await new Promise(resolve => {
+ chromeScript.addMessageListener("done", _ => {
+ chromeScript.destroy();
+ resolve();
+ });
+
+ chromeScript.sendAsyncMessage("go");
+ });
+
+ // We check the permission in tracking processes for non-Fission mode. In
+ // Fission mode, the permission won't be synced to the tracking process,
+ // so we don't check it.
+ if (!SpecialPowers.useRemoteSubframes) {
+ let Services = SpecialPowers.Services;
+ let principal =
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(
+ "http://example.net/"
+ );
+
+ for (let perm of Services.perms.getAllForPrincipal(principal)) {
+ // Ignore permissions other than storage access
+ if (!perm.type.startsWith("3rdPartyStorage^")) {
+ continue;
+ }
+ is(
+ perm.expireType,
+ Services.perms.EXPIRE_TIME,
+ "Permission must expire at a specific time"
+ );
+ ok(perm.expireTime > 0, "Permission must have a expiry time");
+ }
+ }
+ } catch (e) {
+ alert(e);
+ }
+ },
+
+ // Cleanup callback
+ async _ => {
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+ },
+ [
+ [
+ "privacy.partition.always_partition_third_party_non_cookie_storage",
+ false,
+ ],
+ ], // extra prefs
+ true, // run the window.open() test
+ true, // run the user interaction test
+ 0, // don't expect blocking notifications
+ false
+); // run in normal windows
diff --git a/toolkit/components/antitracking/test/browser/browser_permissionInNormalWindows_alwaysPartition.js b/toolkit/components/antitracking/test/browser/browser_permissionInNormalWindows_alwaysPartition.js
new file mode 100644
index 0000000000..8a53782ba2
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_permissionInNormalWindows_alwaysPartition.js
@@ -0,0 +1,109 @@
+AntiTracking.runTest(
+ "Test whether we receive any persistent permissions in normal windows",
+ // Blocking callback
+ async _ => {
+ // Nothing to do here!
+ },
+
+ // Non blocking callback
+ async _ => {
+ try {
+ // We load the test script in the parent process to check permissions.
+ let chromeScript = SpecialPowers.loadChromeScript(_ => {
+ /* eslint-env mozilla/chrome-script */
+ addMessageListener("go", _ => {
+ const { Services } = ChromeUtils.import(
+ "resource://gre/modules/Services.jsm"
+ );
+
+ function ok(what, msg) {
+ sendAsyncMessage("ok", { what: !!what, msg });
+ }
+
+ function is(a, b, msg) {
+ ok(a === b, msg);
+ }
+
+ // We should use the principal of the TEST_DOMAIN since the storage
+ // permission is saved under it.
+ let principal =
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(
+ "http://example.net/"
+ );
+
+ for (let perm of Services.perms.getAllForPrincipal(principal)) {
+ // Ignore permissions other than storage access
+ if (!perm.type.startsWith("3rdPartyStorage^")) {
+ continue;
+ }
+ is(
+ perm.expireType,
+ Services.perms.EXPIRE_TIME,
+ "Permission must expire at a specific time"
+ );
+ ok(perm.expireTime > 0, "Permission must have a expiry time");
+ }
+
+ sendAsyncMessage("done");
+ });
+ });
+
+ chromeScript.addMessageListener("ok", obj => {
+ ok(obj.what, obj.msg);
+ });
+
+ await new Promise(resolve => {
+ chromeScript.addMessageListener("done", _ => {
+ chromeScript.destroy();
+ resolve();
+ });
+
+ chromeScript.sendAsyncMessage("go");
+ });
+
+ // We check the permission in tracking processes for non-Fission mode. In
+ // Fission mode, the permission won't be synced to the tracking process,
+ // so we don't check it.
+ if (!SpecialPowers.useRemoteSubframes) {
+ let Services = SpecialPowers.Services;
+ let principal =
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(
+ "http://example.net/"
+ );
+
+ for (let perm of Services.perms.getAllForPrincipal(principal)) {
+ // Ignore permissions other than storage access
+ if (!perm.type.startsWith("3rdPartyStorage^")) {
+ continue;
+ }
+ is(
+ perm.expireType,
+ Services.perms.EXPIRE_TIME,
+ "Permission must expire at a specific time"
+ );
+ ok(perm.expireTime > 0, "Permission must have a expiry time");
+ }
+ }
+ } catch (e) {
+ alert(e);
+ }
+ },
+
+ // Cleanup callback
+ async _ => {
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+ },
+ [["privacy.partition.always_partition_third_party_non_cookie_storage", true]], // extra prefs
+ true, // run the window.open() test
+ true, // run the user interaction test
+ [
+ // expected blocking notifications
+ Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER,
+ Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_ALL,
+ ],
+ false
+); // run in normal windows
diff --git a/toolkit/components/antitracking/test/browser/browser_permissionInPrivateWindows.js b/toolkit/components/antitracking/test/browser/browser_permissionInPrivateWindows.js
new file mode 100644
index 0000000000..8c8944fe2f
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_permissionInPrivateWindows.js
@@ -0,0 +1,50 @@
+AntiTracking.runTest(
+ "Test whether we receive any persistent permissions in private windows",
+ // Blocking callback
+ async _ => {
+ // Nothing to do here!
+ },
+
+ // Non blocking callback
+ async _ => {
+ try {
+ let Services = SpecialPowers.Services;
+ // We would use TEST_3RD_PARTY_DOMAIN here, except that the variable isn't
+ // accessible in the context of the web page...
+ let principal = SpecialPowers.wrap(document).nodePrincipal;
+ for (let perm of Services.perms.getAllForPrincipal(principal)) {
+ // Ignore permissions other than storage access
+ if (!perm.type.startsWith("3rdPartyStorage^")) {
+ continue;
+ }
+ is(
+ perm.expireType,
+ Services.perms.EXPIRE_SESSION,
+ "Permission must expire at the end of session"
+ );
+ is(perm.expireTime, 0, "Permission must have no expiry time");
+ }
+ } catch (e) {
+ alert(e);
+ }
+ },
+
+ // Cleanup callback
+ async _ => {
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+ },
+ [
+ [
+ "privacy.partition.always_partition_third_party_non_cookie_storage",
+ false,
+ ],
+ ], // extra prefs
+ true, // run the window.open() test
+ true, // run the user interaction test
+ 0, // don't expect blocking notifications
+ true
+); // run in private windows
diff --git a/toolkit/components/antitracking/test/browser/browser_permissionInPrivateWindows_alwaysPartition.js b/toolkit/components/antitracking/test/browser/browser_permissionInPrivateWindows_alwaysPartition.js
new file mode 100644
index 0000000000..defb96004b
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_permissionInPrivateWindows_alwaysPartition.js
@@ -0,0 +1,49 @@
+AntiTracking.runTest(
+ "Test whether we receive any persistent permissions in private windows",
+ // Blocking callback
+ async _ => {
+ // Nothing to do here!
+ },
+
+ // Non blocking callback
+ async _ => {
+ try {
+ let Services = SpecialPowers.Services;
+ // We would use TEST_3RD_PARTY_DOMAIN here, except that the variable isn't
+ // accessible in the context of the web page...
+ let principal = SpecialPowers.wrap(document).nodePrincipal;
+ for (let perm of Services.perms.getAllForPrincipal(principal)) {
+ // Ignore permissions other than storage access
+ if (!perm.type.startsWith("3rdPartyStorage^")) {
+ continue;
+ }
+ is(
+ perm.expireType,
+ Services.perms.EXPIRE_SESSION,
+ "Permission must expire at the end of session"
+ );
+ is(perm.expireTime, 0, "Permission must have no expiry time");
+ }
+ } catch (e) {
+ alert(e);
+ }
+ },
+
+ // Cleanup callback
+ async _ => {
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+ },
+ [["privacy.partition.always_partition_third_party_non_cookie_storage", true]], // extra prefs
+ true, // run the window.open() test
+ true, // run the user interaction test
+ [
+ // expected blocking notifications
+ Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER,
+ Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_ALL,
+ ],
+ true
+); // run in private windows
diff --git a/toolkit/components/antitracking/test/browser/browser_permissionPropagation.js b/toolkit/components/antitracking/test/browser/browser_permissionPropagation.js
new file mode 100644
index 0000000000..35fd83bd31
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_permissionPropagation.js
@@ -0,0 +1,425 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* eslint-disable mozilla/no-arbitrary-setTimeout */
+
+/**
+ * This test makes sure the when we grant the storage permission, the
+ * permission is also propagated to iframes within the same agent cluster,
+ * but not to iframes in the other tabs.
+ */
+
+async function createTab(topUrl, iframeCount, opener, params) {
+ let newTab;
+ let browser;
+ if (opener) {
+ let promise = BrowserTestUtils.waitForNewTab(gBrowser, topUrl);
+ await SpecialPowers.spawn(opener, [topUrl], function (url) {
+ content.window.open(url, "_blank");
+ });
+ newTab = await promise;
+ browser = gBrowser.getBrowserForTab(newTab);
+ } else {
+ newTab = BrowserTestUtils.addTab(gBrowser, topUrl);
+
+ browser = gBrowser.getBrowserForTab(newTab);
+ await BrowserTestUtils.browserLoaded(browser);
+ }
+
+ await SpecialPowers.spawn(
+ browser,
+ [params, iframeCount, createTrackerFrame.toString()],
+ async function (params, count, fn) {
+ // eslint-disable-next-line no-eval
+ let fnCreateTrackerFrame = eval(`(() => (${fn}))()`);
+ await fnCreateTrackerFrame(params, count, ifr => {
+ ifr.contentWindow.postMessage(
+ { callback: params.msg.blockingCallback },
+ "*"
+ );
+ });
+ }
+ );
+
+ return Promise.resolve(newTab);
+}
+
+async function createTrackerFrame(params, count, callback) {
+ let iframes = [];
+ for (var i = 0; i < count; i++) {
+ iframes[i] = content.document.createElement("iframe");
+ await new content.Promise(resolve => {
+ iframes[i].id = "ifr" + i;
+ iframes[i].src = params.page;
+ iframes[i].onload = resolve;
+ content.document.body.appendChild(iframes[i]);
+ });
+
+ await new content.Promise(resolve => {
+ content.addEventListener("message", function msg(event) {
+ if (event.data.type == "finish") {
+ content.removeEventListener("message", msg);
+ resolve();
+ return;
+ }
+
+ if (event.data.type == "ok") {
+ ok(event.data.what, event.data.msg);
+ return;
+ }
+
+ if (event.data.type == "info") {
+ info(event.data.msg);
+ return;
+ }
+
+ ok(false, "Unknown message");
+ });
+
+ callback(iframes[i]);
+ });
+ }
+}
+
+async function testPermission(browser, block, params) {
+ await SpecialPowers.spawn(
+ browser,
+ [block, params],
+ async function (block, params) {
+ for (let i = 0; ; i++) {
+ let ifr = content.document.getElementById("ifr" + i);
+ if (!ifr) {
+ break;
+ }
+
+ await new content.Promise(resolve => {
+ content.addEventListener("message", function msg(event) {
+ if (event.data.type == "finish") {
+ content.removeEventListener("message", msg);
+ resolve();
+ return;
+ }
+
+ if (event.data.type == "ok") {
+ ok(event.data.what, event.data.msg);
+ return;
+ }
+
+ if (event.data.type == "info") {
+ info(event.data.msg);
+ return;
+ }
+
+ ok(false, "Unknown message");
+ });
+
+ if (block) {
+ ifr.contentWindow.postMessage(
+ { callback: params.msg.blockingCallback },
+ "*"
+ );
+ } else {
+ ifr.contentWindow.postMessage(
+ { callback: params.msg.nonBlockingCallback },
+ "*"
+ );
+ }
+ });
+ }
+ }
+ );
+}
+
+add_task(async function testPermissionGrantedOn3rdParty() {
+ info("Starting permission propagation test");
+
+ await SpecialPowers.flushPrefEnv();
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.storage_access.enabled", true],
+ [
+ "network.cookie.cookieBehavior",
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER,
+ ],
+ [
+ "network.cookie.cookieBehavior.pbmode",
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER,
+ ],
+ ["privacy.trackingprotection.enabled", false],
+ ["privacy.trackingprotection.pbmode.enabled", false],
+ ["privacy.trackingprotection.annotate_channels", true],
+ [
+ "privacy.restrict3rdpartystorage.userInteractionRequiredForHosts",
+ "tracking.example.com,tracking.example.org",
+ ],
+ ],
+ });
+
+ await UrlClassifierTestUtils.addTestTrackers();
+
+ let msg = {};
+ msg.blockingCallback = (async _ => {
+ /* import-globals-from storageAccessAPIHelpers.js */
+ await noStorageAccessInitially();
+
+ await new Promise(resolve => {
+ // eslint-disable-next-line no-undef
+ let w = worker;
+ w.addEventListener(
+ "message",
+ e => {
+ ok(!e.data, "IDB is disabled");
+ resolve();
+ },
+ { once: true }
+ );
+ w.postMessage("go");
+ });
+ }).toString();
+
+ msg.nonBlockingCallback = (async _ => {
+ /* import-globals-from storageAccessAPIHelpers.js */
+ console.log("test hasStorageAccessInitially\n");
+ await hasStorageAccessInitially();
+
+ await new Promise(resolve => {
+ // eslint-disable-next-line no-undef
+ let w = worker;
+ w.addEventListener(
+ "message",
+ e => {
+ ok(e.data, "IDB is enabled");
+ resolve();
+ },
+ { once: true }
+ );
+ w.postMessage("go");
+ });
+ }).toString();
+
+ let top = TEST_TOP_PAGE;
+ let page = TEST_3RD_PARTY_PAGE_WORKER;
+ let pageOther =
+ TEST_ANOTHER_3RD_PARTY_DOMAIN + TEST_PATH + "3rdPartyWorker.html";
+ let params = { page, msg, pageOther };
+ // Create 4 tabs:
+ // 1. The first tab has two tracker iframes, said A & B.
+ // 2. The second tab is opened by the first tab, and it has one tracker iframe, said C.
+ // 3. The third tab has one tracker iframe, said D.
+ // 4. The fourth tab is opened by the first tab but with a different top-level url).
+ // The tab has one tracker iframe, said E.
+ //
+ // This test grants permission on iframe A, which then should propagate storage
+ // permission to iframe B & C, but not D, E
+
+ info("Creating the first tab");
+ let tab1 = await createTab(top, 2, null, params);
+ let browser1 = gBrowser.getBrowserForTab(tab1);
+
+ info("Creating the second tab");
+ let tab2 = await createTab(top, 1, browser1 /* opener */, params);
+ let browser2 = gBrowser.getBrowserForTab(tab2);
+
+ info("Creating the third tab");
+ let tab3 = await createTab(top, 1, null, params);
+ let browser3 = gBrowser.getBrowserForTab(tab3);
+
+ info("Creating the fourth tab");
+ let tab4 = await createTab(TEST_TOP_PAGE_2, 1, browser1, params);
+ let browser4 = gBrowser.getBrowserForTab(tab4);
+
+ info("Grant storage permission to the first iframe in the first tab");
+ await SpecialPowers.spawn(browser1, [page, msg], async function (page, msg) {
+ await new content.Promise(resolve => {
+ content.addEventListener("message", function msg(event) {
+ if (event.data.type == "finish") {
+ content.removeEventListener("message", msg);
+ resolve();
+ return;
+ }
+
+ if (event.data.type == "ok") {
+ ok(event.data.what, event.data.msg);
+ return;
+ }
+
+ if (event.data.type == "info") {
+ info(event.data.msg);
+ return;
+ }
+
+ ok(false, "Unknown message");
+ });
+
+ let ifr = content.document.getElementById("ifr0");
+ ifr.contentWindow.postMessage(
+ {
+ callback: (async _ => {
+ /* import-globals-from storageAccessAPIHelpers.js */
+ await callRequestStorageAccess();
+ }).toString(),
+ },
+ "*"
+ );
+ });
+ });
+
+ info("Both iframs of the first tab should have stroage permission");
+ await testPermission(browser1, false /* block */, params);
+
+ info("The iframe of the second tab should have storage permission");
+ await testPermission(browser2, false /* block */, params);
+
+ info("The iframe of the third tab should not have storage permission");
+ await testPermission(browser3, true /* block */, params);
+
+ info("The iframe of the fourth tab should not have storage permission");
+ await testPermission(browser4, true /* block */, params);
+
+ info("Removing the tabs");
+ BrowserTestUtils.removeTab(tab1);
+ BrowserTestUtils.removeTab(tab2);
+ BrowserTestUtils.removeTab(tab3);
+ BrowserTestUtils.removeTab(tab4);
+
+ UrlClassifierTestUtils.cleanupTestTrackers();
+});
+
+add_task(async function () {
+ info("Cleaning up.");
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+});
+
+add_task(async function testPermissionGrantedOnFirstParty() {
+ info("Starting permission propagation test");
+
+ await SpecialPowers.flushPrefEnv();
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.storage_access.enabled", true],
+ [
+ "network.cookie.cookieBehavior",
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER,
+ ],
+ [
+ "network.cookie.cookieBehavior.pbmode",
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER,
+ ],
+ ["privacy.trackingprotection.enabled", false],
+ ["privacy.trackingprotection.pbmode.enabled", false],
+ ["privacy.trackingprotection.annotate_channels", true],
+ ],
+ });
+
+ await UrlClassifierTestUtils.addTestTrackers();
+
+ let msg = {};
+ msg.blockingCallback = (async _ => {
+ /* import-globals-from storageAccessAPIHelpers.js */
+ await noStorageAccessInitially();
+
+ await new Promise(resolve => {
+ // eslint-disable-next-line no-undef
+ let w = worker;
+ w.addEventListener(
+ "message",
+ e => {
+ ok(!e.data, "IDB is disabled");
+ resolve();
+ },
+ { once: true }
+ );
+ w.postMessage("go");
+ });
+ }).toString();
+
+ msg.nonBlockingCallback = (async _ => {
+ /* import-globals-from storageAccessAPIHelpers.js */
+ console.log("test hasStorageAccessInitially\n");
+ await hasStorageAccessInitially();
+
+ await new Promise(resolve => {
+ // eslint-disable-next-line no-undef
+ let w = worker;
+ w.addEventListener(
+ "message",
+ e => {
+ ok(e.data, "IDB is enabled");
+ resolve();
+ },
+ { once: true }
+ );
+ w.postMessage("go");
+ });
+ }).toString();
+
+ let top = TEST_TOP_PAGE;
+ let page = TEST_3RD_PARTY_PAGE_WORKER;
+ let params = { page, msg };
+ // Create 4 tabs:
+ // 1. The first tab has two tracker iframes, said A & B.
+ // 2. The second tab is opened by the first tab, and it has one tracker iframe, said C.
+ // 3. The third tab has one tracker iframe, said D.
+ // 4. The fourth tab is opened by the first tab but with a different top-level url).
+ // The tab has one tracker iframe, said E.
+ //
+ // This test grants permission on iframe A, which then should propagate storage
+ // permission to iframe B & C, but not D, E
+
+ info("Creating the first tab");
+ let tab1 = await createTab(top, 2, null, params);
+ let browser1 = gBrowser.getBrowserForTab(tab1);
+
+ info("Creating the second tab");
+ let tab2 = await createTab(top, 1, browser1 /* opener */, params);
+ let browser2 = gBrowser.getBrowserForTab(tab2);
+
+ info("Creating the third tab");
+ let tab3 = await createTab(top, 1, null, params);
+ let browser3 = gBrowser.getBrowserForTab(tab3);
+
+ info("Creating the fourth tab");
+ let tab4 = await createTab(TEST_TOP_PAGE_2, 1, browser1, params);
+ let browser4 = gBrowser.getBrowserForTab(tab4);
+
+ info("Grant storage permission to the first iframe in the first tab");
+ let promise = BrowserTestUtils.waitForNewTab(gBrowser, page);
+ await SpecialPowers.spawn(browser1, [page], async function (page) {
+ content.window.open(page, "_blank");
+ });
+ let tab = await promise;
+ BrowserTestUtils.removeTab(tab);
+
+ info("Both iframs of the first tab should have stroage permission");
+ await testPermission(browser1, false /* block */, params);
+
+ info("The iframe of the second tab should have storage permission");
+ await testPermission(browser2, false /* block */, params);
+
+ info("The iframe of the third tab should not have storage permission");
+ await testPermission(browser3, true /* block */, params);
+
+ info("The iframe of the fourth tab should not have storage permission");
+ await testPermission(browser4, true /* block */, params);
+
+ info("Removing the tabs");
+ BrowserTestUtils.removeTab(tab1);
+ BrowserTestUtils.removeTab(tab2);
+ BrowserTestUtils.removeTab(tab3);
+ BrowserTestUtils.removeTab(tab4);
+
+ UrlClassifierTestUtils.cleanupTestTrackers();
+});
+
+add_task(async function () {
+ info("Cleaning up.");
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+});
diff --git a/toolkit/components/antitracking/test/browser/browser_referrerDefaultPolicy.js b/toolkit/components/antitracking/test/browser/browser_referrerDefaultPolicy.js
new file mode 100644
index 0000000000..e9030b98e4
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_referrerDefaultPolicy.js
@@ -0,0 +1,634 @@
+"use strict";
+
+requestLongerTimeout(8);
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/browser/base/content/test/general/head.js",
+ this
+);
+
+async function openAWindow(usePrivate) {
+ info("Creating a new " + (usePrivate ? "private" : "normal") + " window");
+ let win = OpenBrowserWindow({ private: usePrivate });
+ await TestUtils.topicObserved(
+ "browser-delayed-startup-finished",
+ subject => subject == win
+ ).then(() => win);
+ await BrowserTestUtils.firstBrowserLoaded(win);
+ return win;
+}
+
+async function testOnWindowBody(win, expectedReferrer, rp) {
+ let browser = win.gBrowser;
+ let tab = browser.selectedTab;
+ let b = browser.getBrowserForTab(tab);
+ await promiseTabLoadEvent(tab, TEST_TOP_PAGE);
+
+ info("Loading tracking scripts and tracking images");
+ let referrer = await SpecialPowers.spawn(
+ b,
+ [{ rp }],
+ async function ({ rp }) {
+ {
+ let src = content.document.createElement("script");
+ let p = new content.Promise(resolve => {
+ src.onload = resolve;
+ });
+ content.document.body.appendChild(src);
+ if (rp) {
+ src.referrerPolicy = rp;
+ }
+ src.src =
+ "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/referrer.sjs?what=script";
+ await p;
+ }
+
+ {
+ let img = content.document.createElement("img");
+ let p = new content.Promise(resolve => {
+ img.onload = resolve;
+ });
+ content.document.body.appendChild(img);
+ if (rp) {
+ img.referrerPolicy = rp;
+ }
+ img.src =
+ "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/referrer.sjs?what=image";
+ await p;
+ }
+
+ {
+ let iframe = content.document.createElement("iframe");
+ let p = new content.Promise(resolve => {
+ iframe.onload = resolve;
+ });
+ content.document.body.appendChild(iframe);
+ if (rp) {
+ iframe.referrerPolicy = rp;
+ }
+ iframe.src =
+ "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/referrer.sjs?what=iframe";
+ await p;
+
+ p = new content.Promise(resolve => {
+ content.onmessage = event => {
+ resolve(event.data);
+ };
+ });
+ iframe.contentWindow.postMessage("ping", "*");
+ return p;
+ }
+ }
+ );
+
+ is(referrer, expectedReferrer, "The correct referrer must be read from DOM");
+
+ await fetch(
+ "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/referrer.sjs?result&what=script"
+ )
+ .then(r => r.text())
+ .then(text => {
+ is(text, expectedReferrer, "We sent the correct Referer header");
+ });
+
+ await fetch(
+ "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/referrer.sjs?result&what=image"
+ )
+ .then(r => r.text())
+ .then(text => {
+ is(text, expectedReferrer, "We sent the correct Referer header");
+ });
+
+ await fetch(
+ "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/referrer.sjs?result&what=iframe"
+ )
+ .then(r => r.text())
+ .then(text => {
+ is(text, expectedReferrer, "We sent the correct Referer header");
+ });
+}
+
+async function closeAWindow(win) {
+ await BrowserTestUtils.closeWindow(win);
+}
+
+let gRecording = true;
+let gScenarios = [];
+let gRPs = [];
+let gTests = { private: [], nonPrivate: [] };
+const kPBPref = "network.http.referer.defaultPolicy.trackers.pbmode";
+const kNonPBPref = "network.http.referer.defaultPolicy.trackers";
+
+function recordScenario(isPrivate, expectedReferrer, rp) {
+ if (!gRPs.includes(rp)) {
+ gRPs.push(rp);
+ }
+ gScenarios.push({
+ private: isPrivate,
+ expectedReferrer,
+ rp,
+ pbPref: Services.prefs.getIntPref(kPBPref),
+ nonPBPref: Services.prefs.getIntPref(kNonPBPref),
+ });
+}
+
+async function testOnWindow(isPrivate, expectedReferrer, rp) {
+ if (gRecording) {
+ recordScenario(isPrivate, expectedReferrer, rp);
+ }
+}
+
+function compileScenarios() {
+ let keys = { false: [], true: [] };
+ for (let s of gScenarios) {
+ let key = {
+ rp: s.rp,
+ pbPref: s.pbPref,
+ nonPBPref: s.nonPBPref,
+ };
+ let skip = false;
+ for (let k of keys[s.private]) {
+ if (
+ key.rp == k.rp &&
+ key.pbPref == k.pbPref &&
+ key.nonPBPref == k.nonPBPref
+ ) {
+ skip = true;
+ break;
+ }
+ }
+ if (!skip) {
+ keys[s.private].push(key);
+ gTests[s.private ? "private" : "nonPrivate"].push({
+ rp: s.rp,
+ pbPref: s.pbPref,
+ nonPBPref: s.nonPBPref,
+ expectedReferrer: s.expectedReferrer,
+ });
+ }
+ }
+
+ // Verify that all scenarios are checked
+ let counter = 1;
+ for (let s of gScenarios) {
+ let checked = false;
+ for (let tt in gTests) {
+ let isPrivate = tt == "private";
+ for (let t of gTests[tt]) {
+ if (
+ isPrivate == s.private &&
+ t.rp == s.rp &&
+ t.pbPref == s.pbPref &&
+ t.nonPBPref == s.nonPBPref &&
+ t.expectedReferrer == s.expectedReferrer
+ ) {
+ checked = true;
+ break;
+ }
+ }
+ }
+ ok(checked, `Scenario number ${counter++} checked`);
+ }
+}
+
+async function executeTests() {
+ compileScenarios();
+
+ gRecording = false;
+ for (let mode in gTests) {
+ info(`Open a ${mode} window`);
+ while (gTests[mode].length) {
+ let test = gTests[mode].shift();
+ info(`Running test ${test.toSource()}`);
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["network.http.referer.defaultPolicy.trackers", test.nonPBPref],
+ ["network.http.referer.defaultPolicy.trackers.pbmode", test.pbPref],
+ ["dom.security.https_first_pbm", false],
+ ["network.http.referer.disallowCrossSiteRelaxingDefault", false],
+ ],
+ });
+
+ let win = await openAWindow(mode == "private");
+
+ await testOnWindowBody(win, test.expectedReferrer, test.rp);
+
+ await closeAWindow(win);
+ }
+ }
+
+ Services.prefs.clearUserPref(kPBPref);
+ Services.prefs.clearUserPref(kNonPBPref);
+}
+
+function pn(name, isPrivate) {
+ return isPrivate ? name + ".pbmode" : name;
+}
+
+async function testOnNoReferrer(isPrivate) {
+ // no-referrer pref when no-referrer is forced
+ await SpecialPowers.pushPrefEnv({
+ set: [[pn("network.http.referer.defaultPolicy.trackers", isPrivate), 3]],
+ });
+ await testOnWindow(isPrivate, "", "no-referrer");
+
+ // strict-origin-when-cross-origin pref when no-referrer is forced
+ await SpecialPowers.pushPrefEnv({
+ set: [[pn("network.http.referer.defaultPolicy.trackers", isPrivate), 2]],
+ });
+ await testOnWindow(isPrivate, "", "no-referrer");
+
+ // same-origin pref when no-referrer is forced
+ await SpecialPowers.pushPrefEnv({
+ set: [[pn("network.http.referer.defaultPolicy.trackers", isPrivate), 1]],
+ });
+ await testOnWindow(isPrivate, "", "no-referrer");
+
+ // no-referrer pref when no-referrer is forced
+ await SpecialPowers.pushPrefEnv({
+ set: [[pn("network.http.referer.defaultPolicy.trackers", isPrivate), 0]],
+ });
+ await testOnWindow(isPrivate, "", "no-referrer");
+}
+
+async function testOnSameOrigin(isPrivate) {
+ // same-origin pref when same-origin is forced
+ await SpecialPowers.pushPrefEnv({
+ set: [[pn("network.http.referer.defaultPolicy.trackers", isPrivate), 3]],
+ });
+ await testOnWindow(isPrivate, "", "same-origin");
+
+ // strict-origin-when-cross-origin pref when same-origin is forced
+ await SpecialPowers.pushPrefEnv({
+ set: [[pn("network.http.referer.defaultPolicy.trackers", isPrivate), 2]],
+ });
+ await testOnWindow(isPrivate, "", "same-origin");
+
+ // same-origin pref when same-origin is forced
+ await SpecialPowers.pushPrefEnv({
+ set: [[pn("network.http.referer.defaultPolicy.trackers", isPrivate), 1]],
+ });
+ await testOnWindow(isPrivate, "", "same-origin");
+
+ // same-origin pref when same-origin is forced
+ await SpecialPowers.pushPrefEnv({
+ set: [[pn("network.http.referer.defaultPolicy.trackers", isPrivate), 0]],
+ });
+ await testOnWindow(isPrivate, "", "same-origin");
+}
+
+async function testOnNoReferrerWhenDowngrade(isPrivate) {
+ // The setting referrer policy will be ignored if it is
+ // no-referrer-when-downgrade in private mode. It will fallback to the default
+ // value.
+ //
+ // The pref 'network.http.referer.disallowCrossSiteRelaxingDefault.pbmode'
+ // controls this behavior in private mode.
+
+ // no-referrer-when-downgrade pref when no-referrer-when-downgrade is forced
+ await SpecialPowers.pushPrefEnv({
+ set: [[pn("network.http.referer.defaultPolicy.trackers", isPrivate), 3]],
+ });
+ await testOnWindow(isPrivate, TEST_TOP_PAGE, "no-referrer-when-downgrade");
+
+ // strict-origin-when-cross-origin pref when no-referrer-when-downgrade is forced
+ await SpecialPowers.pushPrefEnv({
+ set: [[pn("network.http.referer.defaultPolicy.trackers", isPrivate), 2]],
+ });
+ if (isPrivate) {
+ await testOnWindow(isPrivate, TEST_DOMAIN, "no-referrer-when-downgrade");
+ } else {
+ await testOnWindow(isPrivate, TEST_TOP_PAGE, "no-referrer-when-downgrade");
+ }
+
+ // same-origin pref when no-referrer-when-downgrade is forced
+ await SpecialPowers.pushPrefEnv({
+ set: [[pn("network.http.referer.defaultPolicy.trackers", isPrivate), 1]],
+ });
+ if (isPrivate) {
+ await testOnWindow(isPrivate, "", "no-referrer-when-downgrade");
+ } else {
+ await testOnWindow(isPrivate, TEST_TOP_PAGE, "no-referrer-when-downgrade");
+ }
+
+ // no-referrer pref when no-referrer-when-downgrade is forced
+ await SpecialPowers.pushPrefEnv({
+ set: [[pn("network.http.referer.defaultPolicy.trackers", isPrivate), 0]],
+ });
+ if (isPrivate) {
+ await testOnWindow(isPrivate, "", "no-referrer-when-downgrade");
+ } else {
+ await testOnWindow(isPrivate, TEST_TOP_PAGE, "no-referrer-when-downgrade");
+ }
+}
+
+async function testOnOrigin(isPrivate) {
+ // origin pref when origin is forced
+ await SpecialPowers.pushPrefEnv({
+ set: [[pn("network.http.referer.defaultPolicy.trackers", isPrivate), 3]],
+ });
+ await testOnWindow(isPrivate, TEST_DOMAIN, "origin");
+
+ // strict-origin pref when origin is forced
+ await SpecialPowers.pushPrefEnv({
+ set: [[pn("network.http.referer.defaultPolicy.trackers", isPrivate), 2]],
+ });
+ await testOnWindow(isPrivate, TEST_DOMAIN, "origin");
+
+ // same-origin pref when origin is forced
+ await SpecialPowers.pushPrefEnv({
+ set: [[pn("network.http.referer.defaultPolicy.trackers", isPrivate), 1]],
+ });
+ await testOnWindow(isPrivate, TEST_DOMAIN, "origin");
+
+ // no-referrer pref when origin is forced
+ await SpecialPowers.pushPrefEnv({
+ set: [[pn("network.http.referer.defaultPolicy.trackers", isPrivate), 0]],
+ });
+ await testOnWindow(isPrivate, TEST_DOMAIN, "origin");
+}
+
+async function testOnStrictOrigin(isPrivate) {
+ // strict-origin pref when strict-origin is forced
+ await SpecialPowers.pushPrefEnv({
+ set: [[pn("network.http.referer.defaultPolicy.trackers", isPrivate), 3]],
+ });
+ await testOnWindow(isPrivate, TEST_DOMAIN, "strict-origin");
+
+ // strict-origin pref when strict-origin is forced
+ await SpecialPowers.pushPrefEnv({
+ set: [[pn("network.http.referer.defaultPolicy.trackers", isPrivate), 2]],
+ });
+ await testOnWindow(isPrivate, TEST_DOMAIN, "strict-origin");
+
+ // same-origin pref when strict-origin is forced
+ await SpecialPowers.pushPrefEnv({
+ set: [[pn("network.http.referer.defaultPolicy.trackers", isPrivate), 1]],
+ });
+ await testOnWindow(isPrivate, TEST_DOMAIN, "strict-origin");
+
+ // no-referrer pref when strict-origin is forced
+ await SpecialPowers.pushPrefEnv({
+ set: [[pn("network.http.referer.defaultPolicy.trackers", isPrivate), 0]],
+ });
+ await testOnWindow(isPrivate, TEST_DOMAIN, "strict-origin");
+}
+
+async function testOnOriginWhenCrossOrigin(isPrivate) {
+ // The setting referrer policy will be ignored if it is
+ // origin-when-cross-origin in private mode. It will fallback to the default
+ // value. The pref controls this behavior mentioned above.
+
+ // no-referrer-when-downgrade pref when origin-when-cross-origin is forced
+ await SpecialPowers.pushPrefEnv({
+ set: [[pn("network.http.referer.defaultPolicy.trackers", isPrivate), 3]],
+ });
+ if (isPrivate) {
+ await testOnWindow(isPrivate, TEST_TOP_PAGE, "origin-when-cross-origin");
+ } else {
+ await testOnWindow(isPrivate, TEST_DOMAIN, "origin-when-cross-origin");
+ }
+
+ // strict-origin-when-cross-origin pref when origin-when-cross-origin is forced
+ await SpecialPowers.pushPrefEnv({
+ set: [[pn("network.http.referer.defaultPolicy.trackers", isPrivate), 2]],
+ });
+ await testOnWindow(isPrivate, TEST_DOMAIN, "origin-when-cross-origin");
+
+ // same-origin pref when origin-when-cross-origin is forced
+ await SpecialPowers.pushPrefEnv({
+ set: [[pn("network.http.referer.defaultPolicy.trackers", isPrivate), 1]],
+ });
+ if (isPrivate) {
+ await testOnWindow(isPrivate, "", "origin-when-cross-origin");
+ } else {
+ await testOnWindow(isPrivate, TEST_DOMAIN, "origin-when-cross-origin");
+ }
+
+ // no-referrer pref when origin-when-cross-origin is forced
+ await SpecialPowers.pushPrefEnv({
+ set: [[pn("network.http.referer.defaultPolicy.trackers", isPrivate), 0]],
+ });
+ if (isPrivate) {
+ await testOnWindow(isPrivate, "", "origin-when-cross-origin");
+ } else {
+ await testOnWindow(isPrivate, TEST_DOMAIN, "origin-when-cross-origin");
+ }
+}
+
+async function testOnStrictOriginWhenCrossOrigin(isPrivate) {
+ // origin-when-cross-origin pref when strict-origin-when-cross-origin is forced
+ await SpecialPowers.pushPrefEnv({
+ set: [[pn("network.http.referer.defaultPolicy.trackers", isPrivate), 3]],
+ });
+ await testOnWindow(isPrivate, TEST_DOMAIN, "strict-origin-when-cross-origin");
+
+ // strict-origin-when-cross-origin pref when strict-origin-when-cross-origin is forced
+ await SpecialPowers.pushPrefEnv({
+ set: [[pn("network.http.referer.defaultPolicy.trackers", isPrivate), 2]],
+ });
+ await testOnWindow(isPrivate, TEST_DOMAIN, "strict-origin-when-cross-origin");
+
+ // same-origin pref when strict-origin-when-cross-origin is forced
+ await SpecialPowers.pushPrefEnv({
+ set: [[pn("network.http.referer.defaultPolicy.trackers", isPrivate), 1]],
+ });
+ await testOnWindow(isPrivate, TEST_DOMAIN, "strict-origin-when-cross-origin");
+
+ // no-referrer pref when strict-origin-when-cross-origin is forced
+ await SpecialPowers.pushPrefEnv({
+ set: [[pn("network.http.referer.defaultPolicy.trackers", isPrivate), 0]],
+ });
+ await testOnWindow(isPrivate, TEST_DOMAIN, "strict-origin-when-cross-origin");
+}
+
+async function testOnUnsafeUrl(isPrivate) {
+ // The setting referrer policy will be ignored if it is unsafe in private
+ // mode. It will fallback to the default value. The pref controls this
+ // behavior mentioned above.
+
+ // no-referrer-when-downgrade pref when unsafe-url is forced
+ await SpecialPowers.pushPrefEnv({
+ set: [[pn("network.http.referer.defaultPolicy.trackers", isPrivate), 3]],
+ });
+ await testOnWindow(isPrivate, TEST_TOP_PAGE, "unsafe-url");
+
+ // strict-origin-when-cross-origin pref when unsafe-url is forced
+ await SpecialPowers.pushPrefEnv({
+ set: [[pn("network.http.referer.defaultPolicy.trackers", isPrivate), 2]],
+ });
+ if (isPrivate) {
+ await testOnWindow(isPrivate, TEST_DOMAIN, "unsafe-url");
+ } else {
+ await testOnWindow(isPrivate, TEST_TOP_PAGE, "unsafe-url");
+ }
+
+ // same-origin pref when unsafe-url is forced
+ await SpecialPowers.pushPrefEnv({
+ set: [[pn("network.http.referer.defaultPolicy.trackers", isPrivate), 1]],
+ });
+ if (isPrivate) {
+ await testOnWindow(isPrivate, "", "unsafe-url");
+ } else {
+ await testOnWindow(isPrivate, TEST_TOP_PAGE, "unsafe-url");
+ }
+
+ // no-referrer pref when unsafe-url is forced
+ await SpecialPowers.pushPrefEnv({
+ set: [[pn("network.http.referer.defaultPolicy.trackers", isPrivate), 0]],
+ });
+ if (isPrivate) {
+ await testOnWindow(isPrivate, "", "unsafe-url");
+ } else {
+ await testOnWindow(isPrivate, TEST_TOP_PAGE, "unsafe-url");
+ }
+}
+
+add_task(async function () {
+ info("Starting referrer default policy test");
+
+ await SpecialPowers.flushPrefEnv();
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [
+ "network.cookie.cookieBehavior",
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER,
+ ],
+ [
+ "network.cookie.cookieBehavior.pbmode",
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER,
+ ],
+ ["network.http.referer.defaultPolicy", 3],
+ ["privacy.trackingprotection.enabled", false],
+ ["privacy.trackingprotection.pbmode.enabled", false],
+ ["privacy.trackingprotection.annotate_channels", true],
+ ],
+ });
+
+ // no-referrer-when-downgrade
+ await SpecialPowers.pushPrefEnv({
+ set: [["network.http.referer.defaultPolicy.trackers", 3]],
+ });
+ await testOnWindow(false, TEST_TOP_PAGE, null);
+
+ // strict-origin-when-cross-origin
+ await SpecialPowers.pushPrefEnv({
+ set: [["network.http.referer.defaultPolicy.trackers", 2]],
+ });
+ await testOnWindow(false, TEST_DOMAIN, null);
+
+ // same-origin
+ await SpecialPowers.pushPrefEnv({
+ set: [["network.http.referer.defaultPolicy.trackers", 1]],
+ });
+ await testOnWindow(false, "", null);
+
+ // no-referrer
+ await SpecialPowers.pushPrefEnv({
+ set: [["network.http.referer.defaultPolicy.trackers", 0]],
+ });
+ await testOnWindow(false, "", null);
+
+ // override with no-referrer
+ await testOnNoReferrer(false);
+
+ // override with same-origin
+ await testOnSameOrigin(false);
+
+ // override with no-referrer-when-downgrade
+ await testOnNoReferrerWhenDowngrade(false);
+
+ // override with origin
+ await testOnOrigin(false);
+
+ // override with strict-origin
+ await testOnStrictOrigin(false);
+
+ // override with origin-when-cross-origin
+ await testOnOriginWhenCrossOrigin(false);
+
+ // override with strict-origin-when-cross-origin
+ await testOnStrictOriginWhenCrossOrigin(false);
+
+ // override with unsafe-url
+ await testOnUnsafeUrl(false);
+
+ // Reset the pref.
+ Services.prefs.clearUserPref("network.http.referer.defaultPolicy.trackers");
+
+ // no-referrer-when-downgrade
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ // Set both prefs, because if we only set the trackers pref, then the PB
+ // mode default policy pref (2) would apply!
+ ["network.http.referer.defaultPolicy.pbmode", 3],
+ ["network.http.referer.defaultPolicy.trackers.pbmode", 3],
+ ],
+ });
+ await testOnWindow(true, TEST_TOP_PAGE, null);
+
+ // strict-origin-when-cross-origin
+ await SpecialPowers.pushPrefEnv({
+ set: [["network.http.referer.defaultPolicy.trackers.pbmode", 2]],
+ });
+ await testOnWindow(true, TEST_DOMAIN, null);
+
+ // same-origin
+ await SpecialPowers.pushPrefEnv({
+ set: [["network.http.referer.defaultPolicy.trackers.pbmode", 1]],
+ });
+ await testOnWindow(true, "", null);
+
+ // no-referrer
+ await SpecialPowers.pushPrefEnv({
+ set: [["network.http.referer.defaultPolicy.trackers.pbmode", 0]],
+ });
+ await testOnWindow(true, "", null);
+
+ // override with no-referrer
+ await testOnNoReferrer(true);
+
+ // override with same-origin
+ await testOnSameOrigin(true);
+
+ // override with no-referrer-when-downgrade
+ await testOnNoReferrerWhenDowngrade(true);
+
+ // override with origin
+ await testOnOrigin(true);
+
+ // override with strict-origin
+ await testOnStrictOrigin(true);
+
+ // override with origin-when-cross-origin
+ await testOnOriginWhenCrossOrigin(true);
+
+ // override with strict-origin-when-cross-origin
+ await testOnStrictOriginWhenCrossOrigin(true);
+
+ // override with unsafe-url
+ await testOnUnsafeUrl(true);
+
+ // Reset the pref.
+ Services.prefs.clearUserPref(
+ "network.http.referer.defaultPolicy.trackers.pbmode"
+ );
+});
+
+add_task(async function () {
+ await UrlClassifierTestUtils.addTestTrackers();
+
+ await executeTests();
+
+ UrlClassifierTestUtils.cleanupTestTrackers();
+});
+
+add_task(async function () {
+ info("Cleaning up.");
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+});
diff --git a/toolkit/components/antitracking/test/browser/browser_script.js b/toolkit/components/antitracking/test/browser/browser_script.js
new file mode 100644
index 0000000000..ef6c7f67c6
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_script.js
@@ -0,0 +1,224 @@
+add_task(async function () {
+ info("Starting subResources test");
+
+ await SpecialPowers.flushPrefEnv();
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [
+ "network.cookie.cookieBehavior",
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER,
+ ],
+ [
+ "network.cookie.cookieBehavior.pbmode",
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER,
+ ],
+ ["privacy.trackingprotection.enabled", false],
+ ["privacy.trackingprotection.pbmode.enabled", false],
+ ["privacy.trackingprotection.annotate_channels", true],
+ [
+ "privacy.restrict3rdpartystorage.userInteractionRequiredForHosts",
+ "tracking.example.com,tracking.example.org",
+ ],
+ ],
+ });
+
+ await UrlClassifierTestUtils.addTestTrackers();
+
+ info("Creating a new tab");
+ let tab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE);
+ gBrowser.selectedTab = tab;
+
+ let browser = gBrowser.getBrowserForTab(tab);
+ await BrowserTestUtils.browserLoaded(browser);
+
+ info("Loading tracking scripts");
+ await SpecialPowers.spawn(
+ browser,
+ [
+ {
+ scriptURL: TEST_DOMAIN + TEST_PATH + "tracker.js",
+ page: TEST_3RD_PARTY_PAGE,
+ },
+ ],
+ async obj => {
+ info("Checking if permission is denied");
+ let callbackBlocked = async _ => {
+ try {
+ localStorage.foo = 42;
+ ok(false, "LocalStorage cannot be used!");
+ } catch (e) {
+ ok(true, "LocalStorage cannot be used!");
+ is(e.name, "SecurityError", "We want a security error message.");
+ }
+ };
+
+ let assertBlocked = () =>
+ new content.Promise(resolve => {
+ let ifr = content.document.createElement("iframe");
+ ifr.onload = function () {
+ info("Sending code to the 3rd party content");
+ ifr.contentWindow.postMessage(callbackBlocked.toString(), "*");
+ };
+
+ content.addEventListener("message", function msg(event) {
+ if (event.data.type == "finish") {
+ content.removeEventListener("message", msg);
+ resolve();
+ return;
+ }
+
+ if (event.data.type == "ok") {
+ ok(event.data.what, event.data.msg);
+ return;
+ }
+
+ if (event.data.type == "info") {
+ info(event.data.msg);
+ return;
+ }
+
+ ok(false, "Unknown message");
+ });
+
+ content.document.body.appendChild(ifr);
+ ifr.src = obj.page;
+ });
+
+ await assertBlocked();
+
+ info("Triggering a 3rd party script...");
+ let p = new content.Promise(resolve => {
+ let bc = new content.BroadcastChannel("a");
+ bc.onmessage = resolve;
+ });
+
+ let src = content.document.createElement("script");
+ content.document.body.appendChild(src);
+ src.src = obj.scriptURL;
+
+ await p;
+
+ info("Checking if permission is denied before interacting with tracker");
+ await assertBlocked();
+ }
+ );
+
+ await AntiTracking.interactWithTracker();
+
+ info("Loading tracking scripts");
+ await SpecialPowers.spawn(
+ browser,
+ [
+ {
+ scriptURL: TEST_DOMAIN + TEST_PATH + "tracker.js",
+ page: TEST_3RD_PARTY_PAGE,
+ },
+ ],
+ async obj => {
+ info("Checking if permission is denied");
+ let callbackBlocked = async _ => {
+ try {
+ localStorage.foo = 42;
+ ok(false, "LocalStorage cannot be used!");
+ } catch (e) {
+ ok(true, "LocalStorage cannot be used!");
+ is(e.name, "SecurityError", "We want a security error message.");
+ }
+ };
+
+ await new content.Promise(resolve => {
+ let ifr = content.document.createElement("iframe");
+ ifr.onload = function () {
+ info("Sending code to the 3rd party content");
+ ifr.contentWindow.postMessage(callbackBlocked.toString(), "*");
+ };
+
+ content.addEventListener("message", function msg(event) {
+ if (event.data.type == "finish") {
+ content.removeEventListener("message", msg);
+ resolve();
+ return;
+ }
+
+ if (event.data.type == "ok") {
+ ok(event.data.what, event.data.msg);
+ return;
+ }
+
+ if (event.data.type == "info") {
+ info(event.data.msg);
+ return;
+ }
+
+ ok(false, "Unknown message");
+ });
+
+ content.document.body.appendChild(ifr);
+ ifr.src = obj.page;
+ });
+
+ info("Triggering a 3rd party script...");
+ let p = new content.Promise(resolve => {
+ let bc = new content.BroadcastChannel("a");
+ bc.onmessage = resolve;
+ });
+
+ let src = content.document.createElement("script");
+ content.document.body.appendChild(src);
+ src.src = obj.scriptURL;
+
+ await p;
+
+ info("Checking if permission is granted");
+ let callbackAllowed = async _ => {
+ localStorage.foo = 42;
+ ok(true, "LocalStorage can be used!");
+ };
+
+ await new content.Promise(resolve => {
+ let ifr = content.document.createElement("iframe");
+ ifr.onload = function () {
+ info("Sending code to the 3rd party content");
+ ifr.contentWindow.postMessage(callbackAllowed.toString(), "*");
+ };
+
+ content.addEventListener("message", function msg(event) {
+ if (event.data.type == "finish") {
+ content.removeEventListener("message", msg);
+ resolve();
+ return;
+ }
+
+ if (event.data.type == "ok") {
+ ok(event.data.what, event.data.msg);
+ return;
+ }
+
+ if (event.data.type == "info") {
+ info(event.data.msg);
+ return;
+ }
+
+ ok(false, "Unknown message");
+ });
+
+ content.document.body.appendChild(ifr);
+ ifr.src = obj.page;
+ });
+ }
+ );
+
+ info("Removing the tab");
+ BrowserTestUtils.removeTab(tab);
+
+ UrlClassifierTestUtils.cleanupTestTrackers();
+});
+
+add_task(async function () {
+ info("Cleaning up.");
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+});
diff --git a/toolkit/components/antitracking/test/browser/browser_serviceWorkersWithStorageAccessGranted.js b/toolkit/components/antitracking/test/browser/browser_serviceWorkersWithStorageAccessGranted.js
new file mode 100644
index 0000000000..4ae4ba74dd
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_serviceWorkersWithStorageAccessGranted.js
@@ -0,0 +1,148 @@
+/** This tests that the service worker can be used if we have storage access
+ * permission. We manually write the storage access permission into the
+ * permission manager to simulate the storage access has been granted. We would
+ * test the service worker three times. The fist time is to check the service
+ * work is allowed. The second time is to load again and check it won't hit
+ * assertion, this assertion would only be hit if we have registered a service
+ * worker, see Bug 1631234.
+ *
+ * The third time is to load again but in a sandbox iframe to check it won't
+ * hit the assertion. See Bug 1637226 for details.
+ *
+ * The fourth time is to load again in a nested iframe to check it won't hit
+ * the assertion. See Bug 1641153 for details.
+ * */
+
+add_task(async _ => {
+ // Manually add the storage permission.
+ PermissionTestUtils.add(
+ TEST_DOMAIN,
+ "3rdPartyStorage^https://tracking.example.org",
+ Services.perms.ALLOW_ACTION
+ );
+
+ registerCleanupFunction(_ => {
+ Services.perms.removeAll();
+ });
+
+ AntiTracking._createTask({
+ name: "Test that we can use service worker if we have the storage access permission",
+ cookieBehavior: BEHAVIOR_REJECT_TRACKER,
+ allowList: false,
+ callback: async _ => {
+ await navigator.serviceWorker
+ .register("empty.js")
+ .then(
+ _ => {
+ ok(true, "ServiceWorker can be used!");
+ },
+ _ => {
+ ok(false, "ServiceWorker can be used!");
+ }
+ )
+ .catch(e => ok(false, "Promise rejected: " + e));
+ },
+ extraPrefs: [
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ ],
+ expectedBlockingNotifications: 0,
+ runInPrivateWindow: false,
+ iframeSandbox: null,
+ accessRemoval: null,
+ callbackAfterRemoval: null,
+ });
+
+ AntiTracking._createTask({
+ name: "Test again to check if we can still use service worker without hit the assertion.",
+ cookieBehavior: BEHAVIOR_REJECT_TRACKER,
+ allowList: false,
+ callback: async _ => {
+ await navigator.serviceWorker
+ .register("empty.js")
+ .then(
+ _ => {
+ ok(true, "ServiceWorker can be used!");
+ },
+ _ => {
+ ok(false, "ServiceWorker can be used!");
+ }
+ )
+ .catch(e => ok(false, "Promise rejected: " + e));
+ },
+ extraPrefs: [
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ ],
+ expectedBlockingNotifications: 0,
+ runInPrivateWindow: false,
+ iframeSandbox: null,
+ accessRemoval: null,
+ callbackAfterRemoval: null,
+ });
+
+ AntiTracking._createTask({
+ name: "Test again to check if we cannot use service worker in a sandbox iframe without hit the assertion.",
+ cookieBehavior: BEHAVIOR_REJECT_TRACKER,
+ allowList: false,
+ callback: async _ => {
+ await navigator.serviceWorker
+ .register("empty.js")
+ .then(
+ _ => {
+ ok(false, "ServiceWorker cannot be used in sandbox iframe!");
+ },
+ _ => {
+ ok(true, "ServiceWorker cannot be used in sandbox iframe!");
+ }
+ )
+ .catch(e => ok(false, "Promise rejected: " + e));
+ },
+ extraPrefs: [
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ ],
+ expectedBlockingNotifications:
+ Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER, // expect blocking notifications,
+ runInPrivateWindow: false,
+ iframeSandbox: "allow-scripts allow-same-origin",
+ accessRemoval: null,
+ callbackAfterRemoval: null,
+ });
+
+ const NESTED_THIRD_PARTY_PAGE =
+ TEST_DOMAIN + TEST_PATH + "3rdPartyRelay.html?" + TEST_3RD_PARTY_PAGE;
+
+ AntiTracking._createTask({
+ name: "Test again to check if we can use service worker in a nested iframe without hit the assertion.",
+ cookieBehavior: BEHAVIOR_REJECT_TRACKER,
+ allowList: false,
+ callback: async _ => {
+ await navigator.serviceWorker
+ .register("empty.js")
+ .then(
+ _ => {
+ ok(true, "ServiceWorker can be used in nested iframe!");
+ },
+ _ => {
+ ok(false, "ServiceWorker can be used in nested iframe!");
+ }
+ )
+ .catch(e => ok(false, "Promise rejected: " + e));
+ },
+ extraPrefs: [
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ ],
+ expectedBlockingNotifications: 0,
+ runInPrivateWindow: false,
+ iframeSandbox: null,
+ accessRemoval: null,
+ callbackAfterRemoval: null,
+ thirdPartyPage: NESTED_THIRD_PARTY_PAGE,
+ });
+});
diff --git a/toolkit/components/antitracking/test/browser/browser_siteSpecificWorkArounds.js b/toolkit/components/antitracking/test/browser/browser_siteSpecificWorkArounds.js
new file mode 100644
index 0000000000..b35fbea8c7
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_siteSpecificWorkArounds.js
@@ -0,0 +1,153 @@
+AntiTracking.runTest(
+ "localStorage with a tracker that is entitylisted via a pref",
+ async _ => {
+ let effectiveCookieBehavior = SpecialPowers.isContentWindowPrivate(window)
+ ? SpecialPowers.Services.prefs.getIntPref(
+ "network.cookie.cookieBehavior.pbmode"
+ )
+ : SpecialPowers.Services.prefs.getIntPref(
+ "network.cookie.cookieBehavior"
+ );
+
+ let shouldThrow = [
+ SpecialPowers.Ci.nsICookieService.BEHAVIOR_REJECT,
+ SpecialPowers.Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN,
+ ].includes(effectiveCookieBehavior);
+
+ let hasThrown;
+ try {
+ localStorage.foo = 42;
+ hasThrown = false;
+ } catch (e) {
+ hasThrown = true;
+ }
+
+ is(hasThrown, shouldThrow, "LocalStorage is allowed");
+ },
+ async _ => {
+ localStorage.foo = 42;
+ ok(true, "LocalStorage is allowed");
+ },
+ async _ => {
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+ },
+ [["urlclassifier.trackingAnnotationSkipURLs", "TRACKING.EXAMPLE.ORG"]],
+ false, // run the window.open() test
+ false, // run the user interaction test
+ Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER, // expect blocking notifications
+ false
+); // run in a normal window
+
+AntiTracking.runTest(
+ "localStorage with a tracker that is entitylisted via a fancy pref",
+ async _ => {
+ let effectiveCookieBehavior = SpecialPowers.isContentWindowPrivate(window)
+ ? SpecialPowers.Services.prefs.getIntPref(
+ "network.cookie.cookieBehavior.pbmode"
+ )
+ : SpecialPowers.Services.prefs.getIntPref(
+ "network.cookie.cookieBehavior"
+ );
+
+ let shouldThrow = [
+ SpecialPowers.Ci.nsICookieService.BEHAVIOR_REJECT,
+ SpecialPowers.Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN,
+ ].includes(effectiveCookieBehavior);
+
+ let hasThrown;
+ try {
+ localStorage.foo = 42;
+ hasThrown = false;
+ } catch (e) {
+ hasThrown = true;
+ }
+
+ is(hasThrown, shouldThrow, "LocalStorage is allowed");
+ },
+ async _ => {
+ localStorage.foo = 42;
+ ok(true, "LocalStorage is allowed");
+ },
+ async _ => {
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+ },
+ [
+ [
+ "urlclassifier.trackingAnnotationSkipURLs",
+ "foobar.example,*.example.org,baz.example",
+ ],
+ ],
+ false, // run the window.open() test
+ false, // run the user interaction test
+ Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER, // expect blocking notifications
+ false
+); // run in a normal window
+
+AntiTracking.runTest(
+ "localStorage with a tracker that is entitylisted via a misconfigured pref",
+ async _ => {
+ try {
+ localStorage.foo = 42;
+ ok(false, "LocalStorage cannot be used!");
+ } catch (e) {
+ ok(true, "LocalStorage cannot be used!");
+ is(e.name, "SecurityError", "We want a security error message.");
+ }
+ },
+ async _ => {
+ localStorage.foo = 42;
+ ok(true, "LocalStorage is allowed");
+ },
+ async _ => {
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+ },
+ [["urlclassifier.trackingAnnotationSkipURLs", "*.tracking.example.org"]],
+ false, // run the window.open() test
+ false, // run the user interaction test
+ Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER, // expect blocking notifications
+ false
+); // run in a normal window
+
+AntiTracking.runTest(
+ "localStorage with a tracker that is entitylisted via a pref, but skip lists are disabled.",
+ async _ => {
+ try {
+ localStorage.foo = 42;
+ ok(false, "LocalStorage cannot be used!");
+ } catch (e) {
+ ok(true, "LocalStorage cannot be used!");
+ is(e.name, "SecurityError", "We want a security error message.");
+ }
+ },
+ async _ => {
+ localStorage.foo = 42;
+ ok(true, "LocalStorage is allowed");
+ },
+ async _ => {
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+ },
+ [
+ ["urlclassifier.trackingAnnotationSkipURLs", "TRACKING.EXAMPLE.ORG"],
+ ["privacy.antitracking.enableWebcompat", false],
+ ],
+ false, // run the window.open() test
+ false, // run the user interaction test
+ Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER, // expect blocking notifications
+ false
+); // run in a normal window
diff --git a/toolkit/components/antitracking/test/browser/browser_socialtracking.js b/toolkit/components/antitracking/test/browser/browser_socialtracking.js
new file mode 100644
index 0000000000..1cbf5d9278
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_socialtracking.js
@@ -0,0 +1,147 @@
+function runTest(obj) {
+ add_task(async _ => {
+ info("Test: " + obj.testName);
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.ipc.processCount", 1],
+ [
+ "network.cookie.cookieBehavior",
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER,
+ ],
+ [
+ "network.cookie.cookieBehavior.pbmode",
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER,
+ ],
+ ["privacy.trackingprotection.enabled", false],
+ ["privacy.trackingprotection.pbmode.enabled", false],
+ ["privacy.trackingprotection.annotate_channels", true],
+ [
+ "privacy.trackingprotection.socialtracking.enabled",
+ obj.protectionEnabled,
+ ],
+ ["privacy.socialtracking.block_cookies.enabled", obj.cookieBlocking],
+ ],
+ });
+
+ await UrlClassifierTestUtils.addTestTrackers();
+
+ info("Creating a non-tracker top-level context");
+ let tab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE);
+ let browser = gBrowser.getBrowserForTab(tab);
+ await BrowserTestUtils.browserLoaded(browser);
+
+ info("The non-tracker page opens a tracker iframe");
+ await SpecialPowers.spawn(
+ browser,
+ [
+ {
+ page: TEST_3RD_PARTY_DOMAIN_STP + TEST_PATH + "localStorage.html",
+ image: TEST_3RD_PARTY_DOMAIN_STP + TEST_PATH + "raptor.jpg",
+ loading: obj.loading,
+ result: obj.result,
+ },
+ ],
+ async obj => {
+ let loading = await new content.Promise(resolve => {
+ let image = new content.Image();
+ image.src = obj.image + "?" + Math.random();
+ image.onload = _ => resolve(true);
+ image.onerror = _ => resolve(false);
+ });
+
+ is(loading, obj.loading, "Loading expected");
+
+ if (obj.loading) {
+ let ifr = content.document.createElement("iframe");
+ ifr.setAttribute("id", "ifr");
+ ifr.setAttribute("src", obj.page);
+
+ info("Iframe loading...");
+ await new content.Promise(resolve => {
+ ifr.onload = resolve;
+ content.document.body.appendChild(ifr);
+ });
+
+ let p = new Promise(resolve => {
+ content.addEventListener(
+ "message",
+ e => {
+ resolve(e.data);
+ },
+ { once: true }
+ );
+ });
+
+ info("Setting localStorage value...");
+ ifr.contentWindow.postMessage("test", "*");
+
+ info("Getting the value...");
+ let value = await p;
+ is(value.status, obj.result, "We expect to succeed");
+ }
+ }
+ );
+
+ info("Checking content blocking log.");
+ let contentBlockingLog = JSON.parse(await browser.getContentBlockingLog());
+ let origins = Object.keys(contentBlockingLog);
+ is(origins.length, 1, "There should be one origin entry in the log.");
+ for (let origin of origins) {
+ is(
+ origin + "/",
+ TEST_3RD_PARTY_DOMAIN_STP,
+ "Correct tracker origin must be reported"
+ );
+ Assert.deepEqual(
+ contentBlockingLog[origin],
+ obj.expectedLogItems,
+ "Content blocking log should be as expected"
+ );
+ }
+
+ BrowserTestUtils.removeTab(tab);
+
+ UrlClassifierTestUtils.cleanupTestTrackers();
+ });
+}
+
+runTest({
+ testName:
+ "Socialtracking-annotation feature enabled but not considered for tracking detection.",
+ protectionEnabled: false,
+ loading: true,
+ cookieBlocking: false,
+ result: true,
+ expectedLogItems: [
+ [Ci.nsIWebProgressListener.STATE_COOKIES_LOADED, true, 1],
+ [Ci.nsIWebProgressListener.STATE_COOKIES_LOADED_SOCIALTRACKER, true, 1],
+ ],
+});
+
+runTest({
+ testName:
+ "Socialtracking-annotation feature enabled and considered for tracking detection.",
+ protectionEnabled: false,
+ loading: true,
+ cookieBlocking: true,
+ result: false,
+ expectedLogItems: [
+ [Ci.nsIWebProgressListener.STATE_COOKIES_LOADED, true, 1],
+ [Ci.nsIWebProgressListener.STATE_COOKIES_LOADED_SOCIALTRACKER, true, 1],
+ [Ci.nsIWebProgressListener.STATE_LOADED_SOCIALTRACKING_CONTENT, true, 2],
+ // We cache the storage allowed decision, so we will only get one block
+ // event per window and origin.
+ [Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_SOCIALTRACKER, true, 1],
+ ],
+});
+
+runTest({
+ testName: "Socialtracking-protection feature enabled.",
+ protectionEnabled: true,
+ loading: false,
+ cookieBlocking: true,
+ result: false,
+ expectedLogItems: [
+ [Ci.nsIWebProgressListener.STATE_BLOCKED_SOCIALTRACKING_CONTENT, true, 1],
+ ],
+});
diff --git a/toolkit/components/antitracking/test/browser/browser_socialtracking_save_image.js b/toolkit/components/antitracking/test/browser/browser_socialtracking_save_image.js
new file mode 100644
index 0000000000..e7389b1456
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_socialtracking_save_image.js
@@ -0,0 +1,114 @@
+/**
+ * Bug 1663992 - Testing the 'Save Image As' works in an image document if the
+ * image is blocked by social tracker.
+ */
+"use strict";
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/toolkit/content/tests/browser/common/mockTransfer.js",
+ this
+);
+
+const TEST_IMAGE_URL =
+ "http://social-tracking.example.org/browser/toolkit/components/antitracking/test/browser/raptor.jpg";
+
+let MockFilePicker = SpecialPowers.MockFilePicker;
+MockFilePicker.init(window);
+
+const tempDir = createTemporarySaveDirectory();
+MockFilePicker.displayDirectory = tempDir;
+
+function createTemporarySaveDirectory() {
+ let saveDir = Services.dirsvc.get("TmpD", Ci.nsIFile);
+ saveDir.append("testsavedir");
+ saveDir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, 0o755);
+ return saveDir;
+}
+
+function createPromiseForTransferComplete() {
+ return new Promise(resolve => {
+ MockFilePicker.showCallback = fp => {
+ info("MockFilePicker showCallback");
+
+ let fileName = fp.defaultString;
+ let destFile = tempDir.clone();
+ destFile.append(fileName);
+
+ MockFilePicker.setFiles([destFile]);
+ MockFilePicker.filterIndex = 0; // kSaveAsType_Complete
+
+ MockFilePicker.showCallback = null;
+ mockTransferCallback = function (downloadSuccess) {
+ ok(downloadSuccess, "Image should have been downloaded successfully");
+ mockTransferCallback = () => {};
+ resolve();
+ };
+ };
+ });
+}
+
+add_setup(async function () {
+ info("Setting up the prefs.");
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["privacy.trackingprotection.socialtracking.enabled", true],
+ [
+ "urlclassifier.features.socialtracking.blacklistHosts",
+ "social-tracking.example.org",
+ ],
+ ],
+ });
+
+ info("Setting MockFilePicker.");
+ mockTransferRegisterer.register();
+
+ registerCleanupFunction(function () {
+ mockTransferRegisterer.unregister();
+ MockFilePicker.cleanup();
+ tempDir.remove(true);
+ });
+});
+
+add_task(async function () {
+ info("Open a new tab for testing");
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ TEST_IMAGE_URL
+ );
+
+ let popupShownPromise = BrowserTestUtils.waitForEvent(document, "popupshown");
+
+ let browser = gBrowser.selectedBrowser;
+
+ info("Open the context menu.");
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "img",
+ {
+ type: "contextmenu",
+ button: 2,
+ },
+ browser
+ );
+
+ await popupShownPromise;
+
+ let transferCompletePromise = createPromiseForTransferComplete();
+ let saveElement = document.getElementById(`context-saveimage`);
+ info("Triggering the save process.");
+ saveElement.doCommand();
+
+ info("Wait until the save is finished.");
+ await transferCompletePromise;
+
+ info("Close the context menu.");
+ let contextMenu = document.getElementById("contentAreaContextMenu");
+ let popupHiddenPromise = BrowserTestUtils.waitForEvent(
+ contextMenu,
+ "popuphidden"
+ );
+ contextMenu.hidePopup();
+ await popupHiddenPromise;
+
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/toolkit/components/antitracking/test/browser/browser_staticPartition_CORS_preflight.js b/toolkit/components/antitracking/test/browser/browser_staticPartition_CORS_preflight.js
new file mode 100644
index 0000000000..5c232fabc2
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_staticPartition_CORS_preflight.js
@@ -0,0 +1,149 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+const TEST_PAGE =
+ "http://example.org/browser/toolkit/components/antitracking/test/browser/empty.html";
+const TEST_ANOTHER_PAGE =
+ "http://example.com/browser/toolkit/components/antitracking/test/browser/empty.html";
+const TEST_PREFLIGHT_IFRAME_PAGE =
+ "http://mochi.test:8888/browser/toolkit/components/antitracking/test/browser/empty.html";
+const TEST_PREFLIGHT_PAGE =
+ "http://example.net/browser/toolkit/components/antitracking/test/browser/browser_staticPartition_CORS_preflight.sjs";
+
+add_task(async function () {
+ let uuidGenerator = Services.uuid;
+
+ for (let networkIsolation of [true, false]) {
+ for (let partitionPerSite of [true, false]) {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["privacy.partition.network_state", networkIsolation],
+ ["privacy.dynamic_firstparty.use_site", partitionPerSite],
+ ],
+ });
+
+ // First, create one tab under one first party.
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ TEST_PAGE
+ );
+ let token = uuidGenerator.generateUUID().toString();
+
+ // Use fetch to verify that preflight cache is working. The preflight
+ // cache is keyed by the loading principal and the url. So, we load an
+ // iframe with one origin and use fetch in there to ensure we will have
+ // the same loading principal.
+ await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [TEST_PREFLIGHT_PAGE, TEST_PREFLIGHT_IFRAME_PAGE, token],
+ async (url, iframe_url, token) => {
+ let iframe = content.document.createElement("iframe");
+
+ await new Promise(resolve => {
+ iframe.onload = () => {
+ resolve();
+ };
+ content.document.body.appendChild(iframe);
+ iframe.src = iframe_url;
+ });
+
+ await SpecialPowers.spawn(
+ iframe,
+ [url, token],
+ async (url, token) => {
+ const test_url = `${url}?token=${token}`;
+ let response = await content.fetch(
+ new content.Request(test_url, {
+ mode: "cors",
+ method: "GET",
+ headers: [["x-test-header", "check"]],
+ })
+ );
+
+ is(
+ await response.text(),
+ "1",
+ "The preflight should be sent at first time"
+ );
+
+ response = await content.fetch(
+ new content.Request(test_url, {
+ mode: "cors",
+ method: "GET",
+ headers: [["x-test-header", "check"]],
+ })
+ );
+
+ is(
+ await response.text(),
+ "0",
+ "The preflight shouldn't be sent due to the preflight cache"
+ );
+ }
+ );
+ }
+ );
+
+ // Load the tab with a different first party. And use fetch to check if
+ // the preflight cache is partitioned. The fetch will also be performed in
+ // the iframe with the same origin as above to ensure we use the same
+ // loading principal.
+ BrowserTestUtils.loadURIString(tab.linkedBrowser, TEST_ANOTHER_PAGE);
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+
+ await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [
+ TEST_PREFLIGHT_PAGE,
+ TEST_PREFLIGHT_IFRAME_PAGE,
+ token,
+ networkIsolation,
+ ],
+ async (url, iframe_url, token, partitioned) => {
+ let iframe = content.document.createElement("iframe");
+
+ await new Promise(resolve => {
+ iframe.onload = () => {
+ resolve();
+ };
+ content.document.body.appendChild(iframe);
+ iframe.src = iframe_url;
+ });
+
+ await SpecialPowers.spawn(
+ iframe,
+ [url, token, partitioned],
+ async (url, token, partitioned) => {
+ const test_url = `${url}?token=${token}`;
+
+ let response = await content.fetch(
+ new content.Request(test_url, {
+ mode: "cors",
+ method: "GET",
+ headers: [["x-test-header", "check"]],
+ })
+ );
+
+ if (partitioned) {
+ is(
+ await response.text(),
+ "1",
+ "The preflight cache should be partitioned"
+ );
+ } else {
+ is(
+ await response.text(),
+ "0",
+ "The preflight cache shouldn't be partitioned"
+ );
+ }
+ }
+ );
+ }
+ );
+
+ BrowserTestUtils.removeTab(tab);
+ }
+ }
+});
diff --git a/toolkit/components/antitracking/test/browser/browser_staticPartition_CORS_preflight.sjs b/toolkit/components/antitracking/test/browser/browser_staticPartition_CORS_preflight.sjs
new file mode 100644
index 0000000000..0f7041aaa7
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_staticPartition_CORS_preflight.sjs
@@ -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/. */
+
+"use strict";
+
+Cu.importGlobalProperties(["URLSearchParams"]);
+
+function handleRequest(request, response) {
+ let query = new URLSearchParams(request.queryString);
+ let token = query.get("token");
+
+ response.setHeader("Content-Type", "text/plain", false);
+ response.setHeader("Access-Control-Allow-Origin", "*", false);
+ response.setHeader("Access-Control-Allow-Headers", "x-test-header", false);
+
+ if (request.method == "OPTIONS") {
+ response.setHeader(
+ "Access-Control-Allow-Methods",
+ request.getHeader("Access-Control-Request-Method"),
+ false
+ );
+ response.setHeader("Access-Control-Max-Age", "20", false);
+
+ setState(token, token);
+ } else {
+ let test_op = request.getHeader("x-test-header");
+
+ if (test_op == "check") {
+ let value = getState(token);
+
+ if (value) {
+ response.write("1");
+ setState(token, "");
+ } else {
+ response.write("0");
+ }
+ }
+ }
+}
diff --git a/toolkit/components/antitracking/test/browser/browser_staticPartition_HSTS.js b/toolkit/components/antitracking/test/browser/browser_staticPartition_HSTS.js
new file mode 100644
index 0000000000..6bacac0af9
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_staticPartition_HSTS.js
@@ -0,0 +1,251 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+var unsecureEmptyURL =
+ "http://example.org/browser/toolkit/components/antitracking/test/browser/empty.html";
+var secureEmptyURL =
+ "https://example.org/browser/toolkit/components/antitracking/test/browser/empty.html";
+var secureAnotherEmptyURL =
+ "https://example.com/browser/toolkit/components/antitracking/test/browser/empty.html";
+var secureURL =
+ "https://example.com/browser/toolkit/components/antitracking/test/browser/browser_staticPartition_HSTS.sjs";
+var unsecureURL =
+ "http://example.com/browser/toolkit/components/antitracking/test/browser/browser_staticPartition_HSTS.sjs";
+var secureImgURL =
+ "https://example.com/browser/toolkit/components/antitracking/test/browser/browser_staticPartition_HSTS.sjs?image";
+var unsecureImgURL =
+ "http://example.com/browser/toolkit/components/antitracking/test/browser/browser_staticPartition_HSTS.sjs?image";
+var secureIncludeSubURL =
+ "https://example.com/browser/toolkit/components/antitracking/test/browser/browser_staticPartition_HSTS.sjs?includeSub";
+var unsecureSubEmptyURL =
+ "http://test1.example.com/browser/toolkit/components/antitracking/test/browser/empty.html";
+var secureSubEmptyURL =
+ "https://test1.example.com/browser/toolkit/components/antitracking/test/browser/empty.html";
+var unsecureNoCertSubEmptyURL =
+ "http://nocert.example.com/browser/toolkit/components/antitracking/test/browser/empty.html";
+
+function cleanupHSTS(aPartitionEnabled, aUseSite) {
+ // Ensure to remove example.com from the HSTS list.
+ let sss = Cc["@mozilla.org/ssservice;1"].getService(
+ Ci.nsISiteSecurityService
+ );
+
+ for (let origin of ["example.com", "example.org"]) {
+ let originAttributes = {};
+
+ if (aPartitionEnabled) {
+ if (aUseSite) {
+ originAttributes = { partitionKey: `(http,${origin})` };
+ } else {
+ originAttributes = { partitionKey: origin };
+ }
+ }
+
+ sss.resetState(NetUtil.newURI("http://example.com/"), originAttributes);
+ }
+}
+
+function promiseTabLoadEvent(aTab, aURL, aFinalURL) {
+ info("Wait for load tab event");
+ BrowserTestUtils.loadURIString(aTab.linkedBrowser, aURL);
+ return BrowserTestUtils.browserLoaded(aTab.linkedBrowser, false, aFinalURL);
+}
+
+function waitFor(host, type) {
+ return new Promise(resolve => {
+ const observer = channel => {
+ if (
+ channel instanceof Ci.nsIHttpChannel &&
+ channel.URI.host === host &&
+ channel.loadInfo.internalContentPolicyType === type
+ ) {
+ Services.obs.removeObserver(observer, "http-on-stop-request");
+ resolve(channel.URI.spec);
+ }
+ };
+ Services.obs.addObserver(observer, "http-on-stop-request");
+ });
+}
+
+add_task(async function () {
+ for (let networkIsolation of [true, false]) {
+ for (let partitionPerSite of [true, false]) {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["privacy.partition.network_state", networkIsolation],
+ ["privacy.dynamic_firstparty.use_site", partitionPerSite],
+ ["security.mixed_content.upgrade_display_content", false],
+ ],
+ });
+
+ let tab = (gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser));
+
+ // Let's load the secureURL as first-party in order to activate HSTS.
+ await promiseTabLoadEvent(tab, secureURL, secureURL);
+
+ // Let's test HSTS: unsecure -> secure.
+ await promiseTabLoadEvent(tab, unsecureURL, secureURL);
+ ok(true, "unsecure -> secure, first-party works!");
+
+ // Let's load a first-party.
+ await promiseTabLoadEvent(tab, unsecureEmptyURL, unsecureEmptyURL);
+
+ let finalURL = waitFor(
+ "example.com",
+ Ci.nsIContentPolicy.TYPE_INTERNAL_IFRAME
+ );
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [unsecureURL], async url => {
+ let ifr = content.document.createElement("iframe");
+ content.document.body.appendChild(ifr);
+ ifr.src = url;
+ });
+
+ if (networkIsolation) {
+ is(await finalURL, unsecureURL, "HSTS doesn't work for 3rd parties");
+ } else {
+ is(await finalURL, secureURL, "HSTS works for 3rd parties");
+ }
+
+ gBrowser.removeCurrentTab();
+ cleanupHSTS(networkIsolation, partitionPerSite);
+ }
+ }
+});
+
+add_task(async function test_subresource() {
+ for (let networkIsolation of [true, false]) {
+ for (let partitionPerSite of [true, false]) {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["privacy.partition.network_state", networkIsolation],
+ ["privacy.dynamic_firstparty.use_site", partitionPerSite],
+ ["security.mixed_content.upgrade_display_content", false],
+ ],
+ });
+
+ let tab = (gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser));
+
+ // Load a secure page as first party.
+ await promiseTabLoadEvent(tab, secureEmptyURL, secureEmptyURL);
+
+ let loadPromise = waitFor(
+ "example.com",
+ Ci.nsIContentPolicy.TYPE_INTERNAL_IMAGE
+ );
+
+ // Load a secure subresource. HSTS won't be activated, since third
+ // parties can't set HSTS.
+ await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [secureImgURL],
+ async url => {
+ let ifr = content.document.createElement("img");
+ content.document.body.appendChild(ifr);
+ ifr.src = url;
+ }
+ );
+
+ // Ensure the subresource is loaded.
+ await loadPromise;
+
+ // Reload the secure page as first party.
+ await promiseTabLoadEvent(tab, secureEmptyURL, secureEmptyURL);
+
+ let finalURL = waitFor(
+ "example.com",
+ Ci.nsIContentPolicy.TYPE_INTERNAL_IMAGE
+ );
+
+ // Load an unsecure subresource. It should not be upgraded to https.
+ await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [unsecureImgURL],
+ async url => {
+ let ifr = content.document.createElement("img");
+ content.document.body.appendChild(ifr);
+ ifr.src = url;
+ }
+ );
+
+ is(await finalURL, unsecureImgURL, "HSTS isn't set for 3rd parties");
+
+ // Load the secure page with a different origin as first party.
+ await promiseTabLoadEvent(
+ tab,
+ secureAnotherEmptyURL,
+ secureAnotherEmptyURL
+ );
+
+ finalURL = waitFor(
+ "example.com",
+ Ci.nsIContentPolicy.TYPE_INTERNAL_IMAGE
+ );
+
+ // Load a unsecure subresource
+ await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [unsecureImgURL],
+ async url => {
+ let ifr = content.document.createElement("img");
+ content.document.body.appendChild(ifr);
+ ifr.src = url;
+ }
+ );
+
+ is(await finalURL, unsecureImgURL, "HSTS isn't set for 3rd parties");
+
+ gBrowser.removeCurrentTab();
+ cleanupHSTS(networkIsolation, partitionPerSite);
+ }
+ }
+});
+
+add_task(async function test_includeSubDomains() {
+ for (let networkIsolation of [true, false]) {
+ for (let partitionPerSite of [true, false]) {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["privacy.partition.network_state", networkIsolation],
+ ["privacy.dynamic_firstparty.use_site", partitionPerSite],
+ ["security.mixed_content.upgrade_display_content", false],
+ ],
+ });
+
+ let tab = (gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser));
+
+ // Load a secure page as first party to activate HSTS.
+ await promiseTabLoadEvent(tab, secureIncludeSubURL, secureIncludeSubURL);
+
+ // Load a unsecure sub-domain page as first party to see if it's upgraded.
+ await promiseTabLoadEvent(tab, unsecureSubEmptyURL, secureSubEmptyURL);
+
+ // Load a sub domain page which will trigger the cert error page.
+ let certErrorLoaded = BrowserTestUtils.waitForErrorPage(
+ tab.linkedBrowser
+ );
+ BrowserTestUtils.loadURIString(
+ tab.linkedBrowser,
+ unsecureNoCertSubEmptyURL
+ );
+ await certErrorLoaded;
+
+ // Verify the error page has the 'badStsCert' in its query string
+ await SpecialPowers.spawn(tab.linkedBrowser, [], () => {
+ let searchParams = new content.URLSearchParams(
+ content.document.documentURI
+ );
+
+ is(
+ searchParams.get("s"),
+ "badStsCert",
+ "The cert error page has 'badStsCert' set"
+ );
+ });
+
+ gBrowser.removeCurrentTab();
+ cleanupHSTS(networkIsolation, partitionPerSite);
+ }
+ }
+});
diff --git a/toolkit/components/antitracking/test/browser/browser_staticPartition_HSTS.sjs b/toolkit/components/antitracking/test/browser/browser_staticPartition_HSTS.sjs
new file mode 100644
index 0000000000..185c7c7e05
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_staticPartition_HSTS.sjs
@@ -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/. */
+
+const IMG_BYTES = atob(
+ "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAA" +
+ "DUlEQVQImWNgY2P7DwABOgESJhRQtgAAAABJRU5ErkJggg=="
+);
+
+const PAGE = "<!DOCTYPE html><html><body><p>HSTS page</p></body></html>";
+
+function handleRequest(request, response) {
+ response.setStatusLine(request.httpVersion, "200", "OK");
+ if (request.queryString == "includeSub") {
+ response.setHeader(
+ "Strict-Transport-Security",
+ "max-age=60; includeSubDomains"
+ );
+ } else {
+ response.setHeader("Strict-Transport-Security", "max-age=60");
+ }
+
+ if (request.queryString == "image") {
+ response.setHeader("Content-Type", "image/png", false);
+ response.setHeader("Content-Length", IMG_BYTES.length + "", false);
+ response.write(IMG_BYTES);
+ return;
+ }
+
+ response.setHeader("Content-Type", "text/html", false);
+ response.setHeader("Content-Length", PAGE.length + "", false);
+ response.write(PAGE);
+}
diff --git a/toolkit/components/antitracking/test/browser/browser_staticPartition_cache.js b/toolkit/components/antitracking/test/browser/browser_staticPartition_cache.js
new file mode 100644
index 0000000000..f6bc072b75
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_staticPartition_cache.js
@@ -0,0 +1,194 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+const cacheURL =
+ "http://example.org/browser/browser/components/originattributes/test/browser/file_cache.html";
+
+function countMatchingCacheEntries(cacheEntries, domain, fileSuffix) {
+ return cacheEntries
+ .map(entry => entry.uri.asciiSpec)
+ .filter(spec => spec.includes(domain))
+ .filter(spec => spec.includes("file_thirdPartyChild." + fileSuffix)).length;
+}
+
+async function checkCache(suffixes, originAttributes) {
+ const loadContextInfo = Services.loadContextInfo.custom(
+ false,
+ originAttributes
+ );
+
+ const data = await new Promise(resolve => {
+ let cacheEntries = [];
+ let cacheVisitor = {
+ onCacheStorageInfo(num, consumption) {},
+ onCacheEntryInfo(uri, idEnhance) {
+ cacheEntries.push({ uri, idEnhance });
+ },
+ onCacheEntryVisitCompleted() {
+ resolve(cacheEntries);
+ },
+ QueryInterface: ChromeUtils.generateQI(["nsICacheStorageVisitor"]),
+ };
+ // Visiting the disk cache also visits memory storage so we do not
+ // need to use Services.cache2.memoryCacheStorage() here.
+ let storage = Services.cache2.diskCacheStorage(loadContextInfo);
+ storage.asyncVisitStorage(cacheVisitor, true);
+ });
+
+ for (let suffix of suffixes) {
+ let foundEntryCount = countMatchingCacheEntries(
+ data,
+ "example.net",
+ suffix
+ );
+ ok(
+ foundEntryCount > 0,
+ `Cache entries expected for ${suffix} and OA=${JSON.stringify(
+ originAttributes
+ )}`
+ );
+ }
+}
+
+add_task(async function () {
+ info("Disable predictor and accept all");
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["network.predictor.enabled", false],
+ ["network.predictor.enable-prefetch", false],
+ ["network.cookie.cookieBehavior", 0],
+ ],
+ });
+
+ const tests = [
+ {
+ prefValue: true,
+ originAttributes: { partitionKey: "(http,example.org)" },
+ },
+ {
+ prefValue: false,
+ originAttributes: {},
+ },
+ ];
+
+ for (let test of tests) {
+ info("Clear image and network caches");
+ let tools = SpecialPowers.Cc["@mozilla.org/image/tools;1"].getService(
+ SpecialPowers.Ci.imgITools
+ );
+ let imageCache = tools.getImgCacheForDocument(window.document);
+ imageCache.clearCache(true); // true=chrome
+ imageCache.clearCache(false); // false=content
+ Services.cache2.clear();
+
+ info("Enabling network state partitioning");
+ await SpecialPowers.pushPrefEnv({
+ set: [["privacy.partition.network_state", test.prefValue]],
+ });
+
+ info("Let's load a page to populate some entries");
+ let tab = (gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser));
+ BrowserTestUtils.loadURIString(tab.linkedBrowser, cacheURL);
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser, false, cacheURL);
+
+ let argObj = {
+ randomSuffix: Math.random(),
+ urlPrefix:
+ "http://example.net/browser/browser/components/originattributes/test/browser/",
+ };
+
+ await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [argObj],
+ async function (arg) {
+ // The CSS cache needs to be cleared in-process.
+ content.windowUtils.clearSharedStyleSheetCache();
+
+ let videoURL = arg.urlPrefix + "file_thirdPartyChild.video.ogv";
+ let audioURL = arg.urlPrefix + "file_thirdPartyChild.audio.ogg";
+ let URLSuffix = "?r=" + arg.randomSuffix;
+
+ // Create the audio and video elements.
+ let audio = content.document.createElement("audio");
+ let video = content.document.createElement("video");
+ let audioSource = content.document.createElement("source");
+
+ // Append the audio element into the body, and wait until they're finished.
+ await new content.Promise(resolve => {
+ let audioLoaded = false;
+
+ let audioListener = () => {
+ Assert.ok(true, `Audio suspended: ${audioURL + URLSuffix}`);
+ audio.removeEventListener("suspend", audioListener);
+
+ audioLoaded = true;
+ if (audioLoaded) {
+ resolve();
+ }
+ };
+
+ Assert.ok(true, `Loading audio: ${audioURL + URLSuffix}`);
+
+ // Add the event listeners before everything in case we lose events.
+ audio.addEventListener("suspend", audioListener);
+
+ // Assign attributes for the audio element.
+ audioSource.setAttribute("src", audioURL + URLSuffix);
+ audioSource.setAttribute("type", "audio/ogg");
+
+ audio.appendChild(audioSource);
+ audio.autoplay = true;
+
+ content.document.body.appendChild(audio);
+ });
+
+ // Append the video element into the body, and wait until it's finished.
+ await new content.Promise(resolve => {
+ let listener = () => {
+ Assert.ok(true, `Video suspended: ${videoURL + URLSuffix}`);
+ video.removeEventListener("suspend", listener);
+ resolve();
+ };
+
+ Assert.ok(true, `Loading video: ${videoURL + URLSuffix}`);
+
+ // Add the event listener before everything in case we lose the event.
+ video.addEventListener("suspend", listener);
+
+ // Assign attributes for the video element.
+ video.setAttribute("src", videoURL + URLSuffix);
+ video.setAttribute("type", "video/ogg");
+
+ content.document.body.appendChild(video);
+ });
+ }
+ );
+
+ let maybePartitionedSuffixes = [
+ "iframe.html",
+ "link.css",
+ "script.js",
+ "img.png",
+ "favicon.png",
+ "object.png",
+ "embed.png",
+ "xhr.html",
+ "worker.xhr.html",
+ "audio.ogg",
+ "video.ogv",
+ "fetch.html",
+ "worker.fetch.html",
+ "request.html",
+ "worker.request.html",
+ "import.js",
+ "worker.js",
+ "sharedworker.js",
+ ];
+
+ info("Query the cache (maybe) partitioned cache");
+ await checkCache(maybePartitionedSuffixes, test.originAttributes);
+
+ gBrowser.removeCurrentTab();
+ }
+});
diff --git a/toolkit/components/antitracking/test/browser/browser_staticPartition_network.js b/toolkit/components/antitracking/test/browser/browser_staticPartition_network.js
new file mode 100644
index 0000000000..a28f7d5adc
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_staticPartition_network.js
@@ -0,0 +1,116 @@
+function altSvcCacheKeyIsolated(parsed) {
+ return parsed.length > 5 && parsed[5] == "I";
+}
+
+function altSvcPartitionKey(key) {
+ let parts = key.split(":");
+ return parts[parts.length - 2];
+}
+
+const gHttpHandler = Cc["@mozilla.org/network/protocol;1?name=http"].getService(
+ Ci.nsIHttpProtocolHandler
+);
+
+add_task(async function () {
+ info("Starting tlsSessionTickets test");
+
+ await SpecialPowers.flushPrefEnv();
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.cache.disk.enable", false],
+ ["browser.cache.memory.enable", false],
+ ["network.cookie.cookieBehavior", Ci.nsICookieService.BEHAVIOR_ACCEPT],
+ [
+ "network.cookie.cookieBehavior.pbmode",
+ Ci.nsICookieService.BEHAVIOR_ACCEPT,
+ ],
+ ["network.http.altsvc.proxy_checks", false],
+ ["privacy.trackingprotection.enabled", false],
+ ["privacy.trackingprotection.pbmode.enabled", false],
+ ["privacy.trackingprotection.annotate_channels", false],
+ ["privacy.partition.network_state", true],
+ ["privacy.partition.network_state.connection_with_proxy", true],
+ ],
+ });
+
+ info("Creating a new tab");
+ let tab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE);
+ gBrowser.selectedTab = tab;
+
+ let browser = gBrowser.getBrowserForTab(tab);
+ await BrowserTestUtils.browserLoaded(browser);
+
+ const thirdPartyURL =
+ "https://tlsresumptiontest.example.org/browser/toolkit/components/antitracking/test/browser/empty-altsvc.js";
+ const partitionKey1 = "^partitionKey=%28http%2Cexample.net%29";
+ const partitionKey2 = "^partitionKey=%28http%2Cmochi.test%29";
+
+ function checkAltSvcCache(keys) {
+ let arr = gHttpHandler.altSvcCacheKeys;
+ is(
+ arr.length,
+ keys.length,
+ "Found the expected number of items in the cache"
+ );
+ for (let i = 0; i < arr.length; ++i) {
+ is(
+ altSvcPartitionKey(arr[i]),
+ keys[i],
+ "Expected top window origin found in the Alt-Svc cache key"
+ );
+ }
+ }
+
+ checkAltSvcCache([]);
+
+ info("Loading something in the tab");
+ await SpecialPowers.spawn(browser, [{ thirdPartyURL }], async function (obj) {
+ dump("AAA: " + content.window.location.href + "\n");
+ let src = content.document.createElement("script");
+ let p = new content.Promise(resolve => {
+ src.onload = resolve;
+ });
+ content.document.body.appendChild(src);
+ src.src = obj.thirdPartyURL;
+ await p;
+ });
+
+ checkAltSvcCache([partitionKey1]);
+
+ info("Creating a second tab");
+ let tab2 = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE_6);
+ gBrowser.selectedTab = tab2;
+
+ let browser2 = gBrowser.getBrowserForTab(tab2);
+ await BrowserTestUtils.browserLoaded(browser2);
+
+ info("Loading something in the second tab");
+ await SpecialPowers.spawn(
+ browser2,
+ [{ thirdPartyURL }],
+ async function (obj) {
+ let src = content.document.createElement("script");
+ let p = new content.Promise(resolve => {
+ src.onload = resolve;
+ });
+ content.document.body.appendChild(src);
+ src.src = obj.thirdPartyURL;
+ await p;
+ }
+ );
+
+ checkAltSvcCache([partitionKey1, partitionKey2]);
+
+ info("Removing the tabs");
+ BrowserTestUtils.removeTab(tab);
+ BrowserTestUtils.removeTab(tab2);
+});
+
+add_task(async function () {
+ info("Cleaning up.");
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+});
diff --git a/toolkit/components/antitracking/test/browser/browser_staticPartition_saveAs.js b/toolkit/components/antitracking/test/browser/browser_staticPartition_saveAs.js
new file mode 100644
index 0000000000..22db484e16
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_staticPartition_saveAs.js
@@ -0,0 +1,532 @@
+/**
+ * Bug 1641270 - A test case for ensuring the save channel will use the correct
+ * cookieJarSettings when doing the saving and the cache would
+ * work as expected.
+ */
+
+"use strict";
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/toolkit/content/tests/browser/common/mockTransfer.js",
+ this
+);
+
+const TEST_IMAGE_URL = TEST_DOMAIN + TEST_PATH + "file_saveAsImage.sjs";
+const TEST_VIDEO_URL = TEST_DOMAIN + TEST_PATH + "file_saveAsVideo.sjs";
+const TEST_PAGEINFO_URL = TEST_DOMAIN + TEST_PATH + "file_saveAsPageInfo.html";
+
+let MockFilePicker = SpecialPowers.MockFilePicker;
+MockFilePicker.init(window);
+
+const tempDir = createTemporarySaveDirectory();
+MockFilePicker.displayDirectory = tempDir;
+
+function createTemporarySaveDirectory() {
+ let saveDir = Services.dirsvc.get("TmpD", Ci.nsIFile);
+ saveDir.append("testsavedir");
+ saveDir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, 0o755);
+ return saveDir;
+}
+
+function createPromiseForTransferComplete(aDesirableFileName) {
+ return new Promise(resolve => {
+ MockFilePicker.showCallback = fp => {
+ info("MockFilePicker showCallback");
+
+ let fileName = fp.defaultString;
+ let destFile = tempDir.clone();
+ destFile.append(fileName);
+
+ if (aDesirableFileName) {
+ is(fileName, aDesirableFileName, "The default file name is correct.");
+ }
+
+ MockFilePicker.setFiles([destFile]);
+ MockFilePicker.filterIndex = 0; // kSaveAsType_Complete
+
+ MockFilePicker.showCallback = null;
+ mockTransferCallback = function (downloadSuccess) {
+ ok(downloadSuccess, "File should have been downloaded successfully");
+ mockTransferCallback = () => {};
+ resolve();
+ };
+ };
+ });
+}
+
+function createPromiseForObservingChannel(aURL, aPartitionKey) {
+ return new Promise(resolve => {
+ let observer = (aSubject, aTopic) => {
+ if (aTopic === "http-on-modify-request") {
+ let httpChannel = aSubject.QueryInterface(Ci.nsIHttpChannel);
+ let reqLoadInfo = httpChannel.loadInfo;
+
+ // Make sure this is the request which we want to check.
+ if (!httpChannel.URI.spec.endsWith(aURL)) {
+ return;
+ }
+
+ info(`Checking loadInfo for URI: ${httpChannel.URI.spec}\n`);
+ is(
+ reqLoadInfo.cookieJarSettings.partitionKey,
+ aPartitionKey,
+ "The loadInfo has the correct partition key"
+ );
+
+ Services.obs.removeObserver(observer, "http-on-modify-request");
+ resolve();
+ }
+ };
+
+ Services.obs.addObserver(observer, "http-on-modify-request");
+ });
+}
+
+add_setup(async function () {
+ info("Setting MockFilePicker.");
+ mockTransferRegisterer.register();
+
+ registerCleanupFunction(function () {
+ mockTransferRegisterer.unregister();
+ MockFilePicker.cleanup();
+ tempDir.remove(true);
+ });
+});
+
+add_task(async function testContextMenuSaveImage() {
+ let uuidGenerator = Services.uuid;
+
+ for (let networkIsolation of [true, false]) {
+ for (let partitionPerSite of [true, false]) {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["privacy.partition.network_state", networkIsolation],
+ ["privacy.dynamic_firstparty.use_site", partitionPerSite],
+ ],
+ });
+
+ // We use token to separate the caches.
+ let token = uuidGenerator.generateUUID().toString();
+ const testImageURL = `${TEST_IMAGE_URL}?token=${token}`;
+
+ info(`Open a new tab for testing "Save image as" in context menu.`);
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ TEST_TOP_PAGE
+ );
+
+ info(`Insert the testing image into the tab.`);
+ await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [testImageURL],
+ async url => {
+ let img = content.document.createElement("img");
+ let loaded = new content.Promise(resolve => {
+ img.onload = resolve;
+ });
+ content.document.body.appendChild(img);
+ img.setAttribute("id", "image1");
+ img.src = url;
+ await loaded;
+ }
+ );
+
+ info("Open the context menu.");
+ let popupShownPromise = BrowserTestUtils.waitForEvent(
+ document,
+ "popupshown"
+ );
+
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "#image1",
+ {
+ type: "contextmenu",
+ button: 2,
+ },
+ tab.linkedBrowser
+ );
+
+ await popupShownPromise;
+
+ let partitionKey = partitionPerSite
+ ? "(http,example.net)"
+ : "example.net";
+
+ let transferCompletePromise = createPromiseForTransferComplete();
+ let observerPromise = createPromiseForObservingChannel(
+ testImageURL,
+ partitionKey
+ );
+
+ let saveElement = document.getElementById("context-saveimage");
+ info("Triggering the save process.");
+ saveElement.doCommand();
+
+ info("Waiting for the channel.");
+ await observerPromise;
+
+ info("Wait until the save is finished.");
+ await transferCompletePromise;
+
+ info("Close the context menu.");
+ let contextMenu = document.getElementById("contentAreaContextMenu");
+ let popupHiddenPromise = BrowserTestUtils.waitForEvent(
+ contextMenu,
+ "popuphidden"
+ );
+ contextMenu.hidePopup();
+ await popupHiddenPromise;
+
+ // Check if there will be only one network request. The another one should
+ // be from cache.
+ let res = await fetch(`${TEST_IMAGE_URL}?token=${token}&result`);
+ let res_text = await res.text();
+ is(res_text, "1", "The image should be loaded only once.");
+
+ BrowserTestUtils.removeTab(tab);
+ }
+ }
+});
+
+add_task(async function testContextMenuSaveVideo() {
+ let uuidGenerator = Services.uuid;
+
+ for (let networkIsolation of [true, false]) {
+ for (let partitionPerSite of [true, false]) {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["privacy.partition.network_state", networkIsolation],
+ ["privacy.dynamic_firstparty.use_site", partitionPerSite],
+ ],
+ });
+
+ // We use token to separate the caches.
+ let token = uuidGenerator.generateUUID().toString();
+ const testVideoURL = `${TEST_VIDEO_URL}?token=${token}`;
+
+ info(`Open a new tab for testing "Save Video as" in context menu.`);
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ TEST_TOP_PAGE
+ );
+
+ info(`Insert the testing video into the tab.`);
+ await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [testVideoURL],
+ async url => {
+ let video = content.document.createElement("video");
+ let loaded = new content.Promise(resolve => {
+ video.onloadeddata = resolve;
+ });
+ content.document.body.appendChild(video);
+ video.setAttribute("id", "video1");
+ video.src = url;
+ await loaded;
+ }
+ );
+
+ info("Open the context menu.");
+ let popupShownPromise = BrowserTestUtils.waitForEvent(
+ document,
+ "popupshown"
+ );
+
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "#video1",
+ {
+ type: "contextmenu",
+ button: 2,
+ },
+ tab.linkedBrowser
+ );
+
+ await popupShownPromise;
+
+ let partitionKey = partitionPerSite
+ ? "(http,example.net)"
+ : "example.net";
+
+ // We also check the default file name, see Bug 1679325.
+ let transferCompletePromise = createPromiseForTransferComplete(
+ "file_saveAsVideo.webm"
+ );
+ let observerPromise = createPromiseForObservingChannel(
+ testVideoURL,
+ partitionKey
+ );
+
+ let saveElement = document.getElementById("context-savevideo");
+ info("Triggering the save process.");
+ saveElement.doCommand();
+
+ info("Waiting for the channel.");
+ await observerPromise;
+
+ info("Wait until the save is finished.");
+ await transferCompletePromise;
+
+ info("Close the context menu.");
+ let contextMenu = document.getElementById("contentAreaContextMenu");
+ let popupHiddenPromise = BrowserTestUtils.waitForEvent(
+ contextMenu,
+ "popuphidden"
+ );
+ contextMenu.hidePopup();
+ await popupHiddenPromise;
+
+ // Check if there will be only one network request. The another one should
+ // be from cache.
+ let res = await fetch(`${TEST_VIDEO_URL}?token=${token}&result`);
+ let res_text = await res.text();
+ is(res_text, "1", "The video should be loaded only once.");
+
+ BrowserTestUtils.removeTab(tab);
+ }
+ }
+});
+
+add_task(async function testSavePageInOfflineMode() {
+ for (let networkIsolation of [true, false]) {
+ for (let partitionPerSite of [true, false]) {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["privacy.partition.network_state", networkIsolation],
+ ["privacy.dynamic_firstparty.use_site", partitionPerSite],
+ ],
+ });
+
+ let partitionKey = partitionPerSite
+ ? "(http,example.net)"
+ : "example.net";
+
+ info(`Open a new tab which loads an image`);
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ TEST_IMAGE_URL
+ );
+
+ info("Toggle on the offline mode");
+ BrowserOffline.toggleOfflineStatus();
+
+ info("Open file menu and trigger 'Save Page As'");
+ let menubar = document.getElementById("main-menubar");
+ let filePopup = document.getElementById("menu_FilePopup");
+
+ // We only use the shortcut keys to open the file menu in Windows and Linux.
+ // Mac doesn't have a shortcut to only open the file menu. Instead, we directly
+ // trigger the save in MAC without any UI interactions.
+ if (Services.appinfo.OS !== "Darwin") {
+ let menubarActive = BrowserTestUtils.waitForEvent(
+ menubar,
+ "DOMMenuBarActive"
+ );
+ EventUtils.synthesizeKey("KEY_F10");
+ await menubarActive;
+
+ let popupShownPromise = BrowserTestUtils.waitForEvent(
+ filePopup,
+ "popupshown"
+ );
+ // In window, it still needs one extra down key to open the file menu.
+ if (Services.appinfo.OS === "WINNT") {
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ }
+ await popupShownPromise;
+ }
+
+ let transferCompletePromise = createPromiseForTransferComplete();
+ let observerPromise = createPromiseForObservingChannel(
+ TEST_IMAGE_URL,
+ partitionKey
+ );
+
+ info("Triggering the save process.");
+ let fileSavePageAsElement = document.getElementById("menu_savePage");
+ fileSavePageAsElement.doCommand();
+
+ info("Waiting for the channel.");
+ await observerPromise;
+
+ info("Wait until the save is finished.");
+ await transferCompletePromise;
+
+ // Close the file menu.
+ if (Services.appinfo.OS !== "Darwin") {
+ let popupHiddenPromise = BrowserTestUtils.waitForEvent(
+ filePopup,
+ "popuphidden"
+ );
+ filePopup.hidePopup();
+ await popupHiddenPromise;
+ }
+
+ info("Toggle off the offline mode");
+ BrowserOffline.toggleOfflineStatus();
+
+ // Clean up
+ BrowserTestUtils.removeTab(tab);
+
+ // Clean up the cache count on the server side.
+ await fetch(`${TEST_IMAGE_URL}?result`);
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+ }
+ }
+});
+
+add_task(async function testPageInfoMediaSaveAs() {
+ for (let networkIsolation of [true, false]) {
+ for (let partitionPerSite of [true, false]) {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["privacy.partition.network_state", networkIsolation],
+ ["privacy.dynamic_firstparty.use_site", partitionPerSite],
+ ],
+ });
+
+ let partitionKey = partitionPerSite
+ ? "(http,example.net)"
+ : "example.net";
+
+ info(
+ `Open a new tab for testing "Save AS" in the media panel of the page info.`
+ );
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ TEST_PAGEINFO_URL
+ );
+
+ info("Open the media panel of the pageinfo.");
+ let pageInfo = BrowserPageInfo(
+ gBrowser.selectedBrowser.currentURI.spec,
+ "mediaTab"
+ );
+
+ await BrowserTestUtils.waitForEvent(pageInfo, "page-info-init");
+
+ let imageTree = pageInfo.document.getElementById("imagetree");
+ let imageRowsNum = imageTree.view.rowCount;
+
+ is(imageRowsNum, 2, "There should be two media items here.");
+
+ for (let i = 0; i < imageRowsNum; i++) {
+ imageTree.view.selection.select(i);
+ imageTree.ensureRowIsVisible(i);
+ imageTree.focus();
+
+ // Wait until the preview is loaded.
+ let preview = pageInfo.document.getElementById("thepreviewimage");
+ let mediaType = pageInfo.gImageView.data[i][1]; // COL_IMAGE_TYPE
+ if (mediaType == "Image") {
+ await BrowserTestUtils.waitForEvent(preview, "load");
+ } else if (mediaType == "Video") {
+ await BrowserTestUtils.waitForEvent(preview, "canplaythrough");
+ }
+
+ let url = pageInfo.gImageView.data[i][0]; // COL_IMAGE_ADDRESS
+ info(`Start to save the media item with URL: ${url}`);
+
+ let transferCompletePromise = createPromiseForTransferComplete();
+
+ // Observe the channel and check if it has the correct partitionKey.
+ let observerPromise = createPromiseForObservingChannel(
+ url,
+ partitionKey
+ );
+
+ info("Triggering the save process.");
+ let saveElement = pageInfo.document.getElementById("imagesaveasbutton");
+ saveElement.doCommand();
+
+ info("Waiting for the channel.");
+ await observerPromise;
+
+ info("Wait until the save is finished.");
+ await transferCompletePromise;
+ }
+
+ pageInfo.close();
+ BrowserTestUtils.removeTab(tab);
+ }
+ }
+});
+
+add_task(async function testPageInfoMediaMultipleSelectedSaveAs() {
+ for (let networkIsolation of [true, false]) {
+ for (let partitionPerSite of [true, false]) {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["privacy.partition.network_state", networkIsolation],
+ ["privacy.dynamic_firstparty.use_site", partitionPerSite],
+ ],
+ });
+
+ let partitionKey = partitionPerSite
+ ? "(http,example.net)"
+ : "example.net";
+
+ info(
+ `Open a new tab for testing "Save AS" in the media panel of the page info.`
+ );
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ TEST_PAGEINFO_URL
+ );
+
+ info("Open the media panel of the pageinfo.");
+ let pageInfo = BrowserPageInfo(
+ gBrowser.selectedBrowser.currentURI.spec,
+ "mediaTab"
+ );
+
+ await BrowserTestUtils.waitForEvent(pageInfo, "page-info-init");
+
+ // Make sure the preview image is loaded in order to avoid interfering
+ // following tests.
+ let preview = pageInfo.document.getElementById("thepreviewimage");
+ await BrowserTestUtils.waitForCondition(() => {
+ return preview.complete;
+ });
+
+ let imageTree = pageInfo.document.getElementById("imagetree");
+ let imageRowsNum = imageTree.view.rowCount;
+
+ is(imageRowsNum, 2, "There should be two media items here.");
+
+ imageTree.view.selection.selectAll();
+ imageTree.focus();
+
+ let url = pageInfo.gImageView.data[0][0]; // COL_IMAGE_ADDRESS
+ info(`Start to save the media item with URL: ${url}`);
+
+ let transferCompletePromise = createPromiseForTransferComplete();
+ let observerPromises = [];
+
+ // Observe all channels and check if they have the correct partitionKey.
+ for (let i = 0; i < imageRowsNum; ++i) {
+ let observerPromise = createPromiseForObservingChannel(
+ url,
+ partitionKey
+ );
+ observerPromises.push(observerPromise);
+ }
+
+ info("Triggering the save process.");
+ let saveElement = pageInfo.document.getElementById("imagesaveasbutton");
+ saveElement.doCommand();
+
+ info("Waiting for the all channels.");
+ await Promise.all(observerPromises);
+
+ info("Wait until the save is finished.");
+ await transferCompletePromise;
+
+ pageInfo.close();
+ BrowserTestUtils.removeTab(tab);
+ }
+ }
+});
diff --git a/toolkit/components/antitracking/test/browser/browser_staticPartition_tls_session.js b/toolkit/components/antitracking/test/browser/browser_staticPartition_tls_session.js
new file mode 100644
index 0000000000..e131f71169
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_staticPartition_tls_session.js
@@ -0,0 +1,115 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+"use strict";
+
+/**
+ * Tests that we correctly partition TLS sessions by inspecting the
+ * socket's peerId. The peerId contains the OriginAttributes suffix which
+ * includes the partitionKey.
+ */
+
+const TEST_ORIGIN_A = "https://example.com";
+const TEST_ORIGIN_B = "https://example.org";
+const TEST_ORIGIN_C = "https://w3c-test.org";
+
+const TEST_ENDPOINT =
+ "/browser/toolkit/components/antitracking/test/browser/empty.js";
+
+const TEST_URL_C = TEST_ORIGIN_C + TEST_ENDPOINT;
+
+/**
+ * Waits for a load with the given URL to happen and returns the peerId.
+ * @param {string} url - The URL expected to load.
+ * @returns {Promise<string>} a promise which resolves on load with the
+ * associated socket peerId.
+ */
+async function waitForLoad(url) {
+ return new Promise(resolve => {
+ const TOPIC = "http-on-examine-response";
+
+ function observer(subject, topic, data) {
+ if (topic != TOPIC) {
+ return;
+ }
+ subject.QueryInterface(Ci.nsIChannel);
+ if (subject.URI.spec != url) {
+ return;
+ }
+
+ Services.obs.removeObserver(observer, TOPIC);
+
+ resolve(subject.securityInfo.peerId);
+ }
+ Services.obs.addObserver(observer, TOPIC);
+ });
+}
+
+/**
+ * Loads a url in the given browser and returns the tls socket's peer id
+ * associated with the load.
+ * Note: Loads are identified by URI. If multiple loads with the same URI happen
+ * concurrently, this method may not map them correctly.
+ * @param {MozBrowser} browser
+ * @param {string} url
+ * @returns {Promise<string>} Resolves on load with the peer id associated with
+ * the load.
+ */
+function loadURLInFrame(browser, url) {
+ let loadPromise = waitForLoad(url);
+ ContentTask.spawn(browser, [url], async testURL => {
+ let frame = content.document.createElement("iframe");
+ frame.src = testURL;
+ content.document.body.appendChild(frame);
+ });
+ return loadPromise;
+}
+
+add_task(async () => {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.cache.disk.enable", false],
+ ["browser.cache.memory.enable", false],
+ ["privacy.partition.network_state", true],
+ // The test harness acts as a proxy, so we need to make sure to also
+ // partition for proxies.
+ ["privacy.partition.network_state.connection_with_proxy", true],
+ ],
+ });
+
+ // C (first party)
+ let loadPromiseC = waitForLoad(TEST_URL_C);
+ await BrowserTestUtils.withNewTab(TEST_URL_C, async () => {});
+ let peerIdC = await loadPromiseC;
+
+ // C embedded in C (same origin)
+ let peerIdCC;
+ await BrowserTestUtils.withNewTab(TEST_ORIGIN_C, async browser => {
+ peerIdCC = await loadURLInFrame(browser, TEST_URL_C);
+ });
+
+ // C embedded in A (third party)
+ let peerIdAC;
+ await BrowserTestUtils.withNewTab(TEST_ORIGIN_A, async browser => {
+ peerIdAC = await loadURLInFrame(browser, TEST_URL_C);
+ });
+
+ // C embedded in B (third party)
+ let peerIdBC;
+ await BrowserTestUtils.withNewTab(TEST_ORIGIN_B, async browser => {
+ peerIdBC = await loadURLInFrame(browser, TEST_URL_C);
+ });
+
+ info("Test that top level load and same origin frame have the same peerId.");
+ is(peerIdC, peerIdCC, "Should have the same peerId");
+
+ info("Test that all partitioned peer ids are distinct.");
+ isnot(peerIdCC, peerIdAC, "Should have different peerId partitioned under A");
+ isnot(peerIdCC, peerIdBC, "Should have different peerId partitioned under B");
+ isnot(
+ peerIdAC,
+ peerIdBC,
+ "Should have a different peerId under different first parties."
+ );
+});
diff --git a/toolkit/components/antitracking/test/browser/browser_staticPartition_websocket.js b/toolkit/components/antitracking/test/browser/browser_staticPartition_websocket.js
new file mode 100644
index 0000000000..11f53d69ed
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_staticPartition_websocket.js
@@ -0,0 +1,198 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+"use strict";
+
+const FIRST_PARTY_A = "http://example.com";
+const FIRST_PARTY_B = "http://example.org";
+const THIRD_PARTY = "http://example.net";
+const WS_ENDPOINT_HOST = "mochi.test:8888";
+
+function getWSTestUrlForHost(host) {
+ return (
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ `ws://${host}`
+ ) + `file_ws_handshake_delay`
+ );
+}
+
+function connect(browsingContext, host, protocol) {
+ let url = getWSTestUrlForHost(host);
+ info("Creating websocket with endpoint " + url);
+
+ // Create websocket connection in third party iframe.
+ let createPromise = SpecialPowers.spawn(
+ browsingContext.children[0],
+ [url, protocol],
+ (url, protocol) => {
+ let ws = new content.WebSocket(url, [protocol]);
+ ws.addEventListener("error", () => {
+ ws._testError = true;
+ });
+ if (!content.ws) {
+ content.ws = {};
+ }
+ content.ws[protocol] = ws;
+ }
+ );
+
+ let openPromise = createPromise.then(() =>
+ SpecialPowers.spawn(
+ browsingContext.children[0],
+ [protocol],
+ async protocol => {
+ let ws = content.ws[protocol];
+ if (ws.readyState != 0) {
+ return !ws._testError;
+ }
+ // Still connecting.
+ let result = await Promise.race([
+ ContentTaskUtils.waitForEvent(ws, "open"),
+ ContentTaskUtils.waitForEvent(ws, "error"),
+ ]);
+ return result.type != "error";
+ }
+ )
+ );
+
+ let result = { createPromise, openPromise };
+ return result;
+}
+
+// Open 3 websockets which target the same ip/port combination, but have
+// different principals. We send a protocol identifier to the server to signal
+// how long the request should be delayed.
+//
+// When partitioning is disabled A blocks B and B blocks C. The timeline will
+// look like this:
+// A________
+// B____
+// C_
+//
+// When partitioning is enabled, connection handshakes for A and B will run
+// (semi-) parallel since they have different origin attributes. B and C share
+// origin attributes and therefore still run serially.
+// A________
+// B____
+// C_
+//
+// By observing the order of the handshakes we can ensure that the queue
+// partitioning is working correctly.
+async function runTest(partitioned) {
+ await SpecialPowers.pushPrefEnv({
+ set: [["privacy.partition.network_state", partitioned]],
+ });
+
+ let tabA = BrowserTestUtils.addTab(gBrowser, FIRST_PARTY_A);
+ await BrowserTestUtils.browserLoaded(tabA.linkedBrowser);
+ let tabB = BrowserTestUtils.addTab(gBrowser, FIRST_PARTY_B);
+ await BrowserTestUtils.browserLoaded(tabB.linkedBrowser);
+
+ for (let tab of [tabA, tabB]) {
+ await SpecialPowers.spawn(tab.linkedBrowser, [THIRD_PARTY], async src => {
+ let frame = content.document.createElement("iframe");
+ frame.src = src;
+ let loadPromise = ContentTaskUtils.waitForEvent(frame, "load");
+ content.document.body.appendChild(frame);
+ await loadPromise;
+ });
+ }
+
+ // First ensure that we can open websocket connections to the test endpoint.
+ let { openPromise, createPromise } = await connect(
+ tabA.linkedBrowser.browsingContext,
+ WS_ENDPOINT_HOST,
+ false
+ );
+ await createPromise;
+ let openPromiseResult = await openPromise;
+ ok(openPromiseResult, "Websocket endpoint accepts connections.");
+
+ let openedA;
+ let openedB;
+ let openedC;
+
+ let { createPromise: createPromiseA, openPromise: openPromiseA } = connect(
+ tabA.linkedBrowser.browsingContext,
+ WS_ENDPOINT_HOST,
+ "test-6"
+ );
+
+ openPromiseA = openPromiseA.then(opened => {
+ openedA = opened;
+ info("Completed WS connection A");
+ if (partitioned) {
+ ok(openedA, "Should have opened A");
+ ok(openedB, "Should have opened B");
+ } else {
+ ok(openedA, "Should have opened A");
+ ok(openedB == null, "B should be pending");
+ }
+ });
+ await createPromiseA;
+
+ // The frame of connection B is embedded in a different first party as A.
+ let { createPromise: createPromiseB, openPromise: openPromiseB } = connect(
+ tabB.linkedBrowser.browsingContext,
+ WS_ENDPOINT_HOST,
+ "test-3"
+ );
+ openPromiseB = openPromiseB.then(opened => {
+ openedB = opened;
+ info("Completed WS connection B");
+ if (partitioned) {
+ ok(openedA == null, "A should be pending");
+ ok(openedB, "Should have opened B");
+ ok(openedC == null, "C should be pending");
+ } else {
+ ok(openedA, "Should have opened A");
+ ok(openedB, "Should have opened B");
+ ok(openedC == null, "C should be pending");
+ }
+ });
+ await createPromiseB;
+
+ // The frame of connection C is embedded in the same first party as B.
+ let { createPromise: createPromiseC, openPromise: openPromiseC } = connect(
+ tabB.linkedBrowser.browsingContext,
+ WS_ENDPOINT_HOST,
+ "test-0"
+ );
+ openPromiseC = openPromiseC.then(opened => {
+ openedC = opened;
+ info("Completed WS connection C");
+ if (partitioned) {
+ ok(openedB, "Should have opened B");
+ ok(openedC, "Should have opened C");
+ } else {
+ ok(opened, "Should have opened B");
+ ok(opened, "Should have opened C");
+ }
+ });
+ await createPromiseC;
+
+ // Wait for all connections to complete before closing the tabs.
+ await Promise.all([openPromiseA, openPromiseB, openPromiseC]);
+
+ BrowserTestUtils.removeTab(tabA);
+ BrowserTestUtils.removeTab(tabB);
+
+ await SpecialPowers.popPrefEnv();
+}
+
+add_setup(async function () {
+ // This test relies on a WS connection timeout > 6 seconds.
+ await SpecialPowers.pushPrefEnv({
+ set: [["network.websocket.timeout.open", 20]],
+ });
+});
+
+add_task(async function test_non_partitioned() {
+ await runTest(false);
+});
+
+add_task(async function test_partitioned() {
+ await runTest(true);
+});
diff --git a/toolkit/components/antitracking/test/browser/browser_storageAccessAutograntRequiresUserInteraction.js b/toolkit/components/antitracking/test/browser/browser_storageAccessAutograntRequiresUserInteraction.js
new file mode 100644
index 0000000000..c2d6898313
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_storageAccessAutograntRequiresUserInteraction.js
@@ -0,0 +1,57 @@
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/browser/modules/test/browser/head.js",
+ this
+);
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/toolkit/components/antitracking/test/browser/storage_access_head.js",
+ this
+);
+
+async function setAutograntPreferences() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.storage_access.auto_grants", true],
+ ["dom.storage_access.max_concurrent_auto_grants", 1],
+ ],
+ });
+}
+
+add_task(async function testPopupWithUserInteraction() {
+ await setPreferences();
+ await setAutograntPreferences();
+
+ // Test that requesting storage access initially does not autogrant.
+ // If the autogrant doesn't occur, we click reject on the door hanger
+ // and expect the promise returned by requestStorageAccess to reject.
+ await openPageAndRunCode(
+ TEST_TOP_PAGE,
+ getExpectPopupAndClick("reject"),
+ TEST_3RD_PARTY_PAGE,
+ requestStorageAccessAndExpectFailure
+ );
+ // Grant the storageAccessAPI permission to the third-party.
+ // This signifies that it has been interacted with and should allow autogrants
+ // among other behaviors.
+ const uri = Services.io.newURI(TEST_3RD_PARTY_DOMAIN);
+ const principal = Services.scriptSecurityManager.createContentPrincipal(
+ uri,
+ {}
+ );
+ Services.perms.addFromPrincipal(
+ principal,
+ "storageAccessAPI",
+ Services.perms.ALLOW_ACTION
+ );
+
+ // Test that requesting storage access autogrants here. If a popup occurs,
+ // expectNoPopup will cause an error in this test.
+ await openPageAndRunCode(
+ TEST_TOP_PAGE,
+ expectNoPopup,
+ TEST_3RD_PARTY_PAGE,
+ requestStorageAccessAndExpectSuccess
+ );
+
+ await cleanUpData();
+ await SpecialPowers.flushPrefEnv();
+});
diff --git a/toolkit/components/antitracking/test/browser/browser_storageAccessDeniedGivesNoUserInteraction.js b/toolkit/components/antitracking/test/browser/browser_storageAccessDeniedGivesNoUserInteraction.js
new file mode 100644
index 0000000000..cf47c43a14
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_storageAccessDeniedGivesNoUserInteraction.js
@@ -0,0 +1,30 @@
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/browser/modules/test/browser/head.js",
+ this
+);
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/toolkit/components/antitracking/test/browser/storage_access_head.js",
+ this
+);
+
+add_task(async function testGrantGivesPermission() {
+ await setPreferences();
+
+ await openPageAndRunCode(
+ TEST_TOP_PAGE,
+ getExpectPopupAndClick("reject"),
+ TEST_3RD_PARTY_PAGE,
+ requestStorageAccessAndExpectFailure
+ );
+
+ await SpecialPowers.testPermission(
+ "storageAccessAPI",
+ SpecialPowers.Services.perms.UNKNOWN_ACTION,
+ {
+ url: "https://tracking.example.org/",
+ }
+ );
+
+ await cleanUpData();
+ await SpecialPowers.flushPrefEnv();
+});
diff --git a/toolkit/components/antitracking/test/browser/browser_storageAccessDoorHanger.js b/toolkit/components/antitracking/test/browser/browser_storageAccessDoorHanger.js
new file mode 100644
index 0000000000..733eb34240
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_storageAccessDoorHanger.js
@@ -0,0 +1,372 @@
+/* eslint-disable mozilla/no-arbitrary-setTimeout */
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/browser/modules/test/browser/head.js",
+ this
+);
+
+const BLOCK = 0;
+const ALLOW = 1;
+
+async function testDoorHanger(
+ choice,
+ showPrompt,
+ useEscape,
+ topPage,
+ maxConcurrent,
+ disableWebcompat = false
+) {
+ info(
+ `Running doorhanger test with choice #${choice}, showPrompt: ${showPrompt} and ` +
+ `useEscape: ${useEscape}, topPage: ${topPage}, maxConcurrent: ${maxConcurrent}`
+ );
+
+ if (!showPrompt) {
+ is(choice, ALLOW, "When not showing a prompt, we can only auto-grant");
+ ok(
+ !useEscape,
+ "When not showing a prompt, we should not be trying to use the Esc key"
+ );
+ }
+
+ await SpecialPowers.flushPrefEnv();
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["privacy.antitracking.enableWebcompat", !disableWebcompat],
+ ["dom.storage_access.auto_grants", true],
+ ["dom.storage_access.auto_grants.delayed", false],
+ ["dom.storage_access.enabled", true],
+ ["dom.storage_access.max_concurrent_auto_grants", maxConcurrent],
+ ["dom.storage_access.prompt.testing", false],
+ [
+ "network.cookie.cookieBehavior",
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER,
+ ],
+ [
+ "network.cookie.cookieBehavior.pbmode",
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER,
+ ],
+ ["privacy.trackingprotection.enabled", false],
+ ["privacy.trackingprotection.pbmode.enabled", false],
+ ["privacy.trackingprotection.annotate_channels", true],
+ [
+ "privacy.restrict3rdpartystorage.userInteractionRequiredForHosts",
+ "tracking.example.com,tracking.example.org",
+ ],
+ // Bug 1617611: Fix all the tests broken by "cookies SameSite=lax by default"
+ ["network.cookie.sameSite.laxByDefault", false],
+ ],
+ });
+
+ await UrlClassifierTestUtils.addTestTrackers();
+
+ let tab = BrowserTestUtils.addTab(gBrowser, topPage);
+ gBrowser.selectedTab = tab;
+
+ let browser = gBrowser.getBrowserForTab(tab);
+ await BrowserTestUtils.browserLoaded(browser);
+
+ async function runChecks() {
+ // We need to repeat this constant here since runChecks is stringified
+ // and sent to the content process.
+ const BLOCK = 0;
+
+ await new Promise(resolve => {
+ addEventListener(
+ "message",
+ function onMessage(e) {
+ if (e.data.startsWith("choice:")) {
+ window.choice = e.data.split(":")[1];
+ window.useEscape = e.data.split(":")[3];
+ removeEventListener("message", onMessage);
+ resolve();
+ }
+ },
+ false
+ );
+ parent.postMessage("getchoice", "*");
+ });
+
+ /* import-globals-from storageAccessAPIHelpers.js */
+ await noStorageAccessInitially();
+
+ is(document.cookie, "", "No cookies for me");
+ document.cookie = "name=value";
+ is(document.cookie, "", "No cookies for me");
+
+ await fetch("server.sjs")
+ .then(r => r.text())
+ .then(text => {
+ is(text, "cookie-not-present", "We should not have cookies");
+ });
+ // Let's do it twice.
+ await fetch("server.sjs")
+ .then(r => r.text())
+ .then(text => {
+ is(text, "cookie-not-present", "We should not have cookies");
+ });
+
+ is(document.cookie, "", "Still no cookies for me");
+
+ /* import-globals-from storageAccessAPIHelpers.js */
+ await callRequestStorageAccess();
+
+ if (choice == BLOCK) {
+ // We've said no, so cookies are still blocked
+ is(document.cookie, "", "Still no cookies for me");
+ document.cookie = "name=value";
+ is(document.cookie, "", "No cookies for me");
+ } else {
+ // We've said yes, so cookies are allowed now
+ is(document.cookie, "", "No cookies for me");
+ document.cookie = "name=value";
+ is(document.cookie, "name=value", "I have the cookies!");
+ }
+ }
+
+ let permChanged;
+ // Only create the promise when we're going to click one of the allow buttons.
+ if (choice != BLOCK) {
+ permChanged = TestUtils.topicObserved("perm-changed", (subject, data) => {
+ let result;
+ if (choice == ALLOW) {
+ result =
+ subject &&
+ subject
+ .QueryInterface(Ci.nsIPermission)
+ .type.startsWith("3rdPartyStorage^") &&
+ subject.principal.origin == new URL(topPage).origin &&
+ data == "added";
+ }
+ return result;
+ });
+ }
+ let shownPromise = BrowserTestUtils.waitForEvent(
+ PopupNotifications.panel,
+ "popupshown"
+ );
+ shownPromise.then(async _ => {
+ if (topPage != gBrowser.currentURI.spec) {
+ return;
+ }
+ ok(showPrompt, "We shouldn't show the prompt when we don't intend to");
+ let notification = await new Promise(function poll(resolve) {
+ let notification = PopupNotifications.getNotification(
+ "storage-access",
+ browser
+ );
+ if (notification) {
+ resolve(notification);
+ return;
+ }
+ setTimeout(poll, 10);
+ });
+ Assert.ok(notification, "Should have gotten the notification");
+
+ if (choice == BLOCK) {
+ if (useEscape) {
+ EventUtils.synthesizeKey("KEY_Escape", {}, window);
+ } else {
+ await clickSecondaryAction();
+ }
+ } else if (choice == ALLOW) {
+ await clickMainAction();
+ }
+ if (choice != BLOCK) {
+ await permChanged;
+ }
+ });
+
+ let url = TEST_3RD_PARTY_PAGE + "?disableWaitUntilPermission";
+ let ct = SpecialPowers.spawn(
+ browser,
+ [{ page: url, callback: runChecks.toString(), choice, useEscape }],
+ async function (obj) {
+ await new content.Promise(resolve => {
+ let ifr = content.document.createElement("iframe");
+ ifr.onload = function () {
+ info("Sending code to the 3rd party content");
+ ifr.contentWindow.postMessage(obj.callback, "*");
+ };
+
+ content.addEventListener("message", function msg(event) {
+ if (event.data.type == "finish") {
+ content.removeEventListener("message", msg);
+ resolve();
+ return;
+ }
+
+ if (event.data.type == "ok") {
+ ok(event.data.what, event.data.msg);
+ return;
+ }
+
+ if (event.data.type == "info") {
+ info(event.data.msg);
+ return;
+ }
+
+ if (event.data == "getchoice") {
+ ifr.contentWindow.postMessage(
+ "choice:" + obj.choice + ":useEscape:" + obj.useEscape,
+ "*"
+ );
+ return;
+ }
+
+ ok(false, "Unknown message");
+ });
+
+ content.document.body.appendChild(ifr);
+ ifr.src = obj.page;
+ });
+ }
+ );
+ if (showPrompt) {
+ await Promise.all([ct, shownPromise]);
+ } else {
+ await Promise.all([ct, permChanged]);
+ }
+
+ let permissionPopupPromise = BrowserTestUtils.waitForEvent(
+ window,
+ "popupshown",
+ true,
+ event => event.target == gPermissionPanel._permissionPopup
+ );
+ gPermissionPanel._identityPermissionBox.click();
+ await permissionPopupPromise;
+ let permissionItem = document.querySelector(
+ ".permission-popup-permission-item-3rdPartyStorage"
+ );
+ ok(permissionItem, "Permission item exists");
+ ok(
+ BrowserTestUtils.is_visible(permissionItem),
+ "Permission item visible in the identity panel"
+ );
+ let permissionLearnMoreLink = document.getElementById(
+ "permission-popup-storage-access-permission-learn-more"
+ );
+ ok(permissionLearnMoreLink, "Permission learn more link exists");
+ ok(
+ BrowserTestUtils.is_visible(permissionLearnMoreLink),
+ "Permission learn more link is visible in the identity panel"
+ );
+ permissionPopupPromise = BrowserTestUtils.waitForEvent(
+ gPermissionPanel._permissionPopup,
+ "popuphidden"
+ );
+ gPermissionPanel._permissionPopup.hidePopup();
+ await permissionPopupPromise;
+
+ BrowserTestUtils.removeTab(tab);
+
+ UrlClassifierTestUtils.cleanupTestTrackers();
+}
+
+async function preparePermissionsFromOtherSites(topPage) {
+ info("Faking permissions from other sites");
+ let type = "3rdPartyStorage^https://tracking.example.org";
+ let permission = Services.perms.ALLOW_ACTION;
+ let expireType = Services.perms.EXPIRE_SESSION;
+ if (topPage == TEST_TOP_PAGE) {
+ // For the first page, don't do anything
+ } else if (topPage == TEST_TOP_PAGE_2) {
+ // For the second page, only add the permission from the first page
+ PermissionTestUtils.add(TEST_DOMAIN, type, permission, expireType, 0);
+ } else if (topPage == TEST_TOP_PAGE_3) {
+ // For the third page, add the permissions from the first two pages
+ PermissionTestUtils.add(TEST_DOMAIN, type, permission, expireType, 0);
+ PermissionTestUtils.add(TEST_DOMAIN_2, type, permission, expireType, 0);
+ } else if (topPage == TEST_TOP_PAGE_4) {
+ // For the fourth page, add the permissions from the first three pages
+ PermissionTestUtils.add(TEST_DOMAIN, type, permission, expireType, 0);
+ PermissionTestUtils.add(TEST_DOMAIN_2, type, permission, expireType, 0);
+ PermissionTestUtils.add(TEST_DOMAIN_3, type, permission, expireType, 0);
+ } else if (topPage == TEST_TOP_PAGE_5) {
+ // For the fifth page, add the permissions from the first four pages
+ PermissionTestUtils.add(TEST_DOMAIN, type, permission, expireType, 0);
+ PermissionTestUtils.add(TEST_DOMAIN_2, type, permission, expireType, 0);
+ PermissionTestUtils.add(TEST_DOMAIN_3, type, permission, expireType, 0);
+ PermissionTestUtils.add(TEST_DOMAIN_4, type, permission, expireType, 0);
+ } else if (topPage == TEST_TOP_PAGE_6) {
+ // For the sixth page, add the permissions from the first five pages
+ PermissionTestUtils.add(TEST_DOMAIN, type, permission, expireType, 0);
+ PermissionTestUtils.add(TEST_DOMAIN_2, type, permission, expireType, 0);
+ PermissionTestUtils.add(TEST_DOMAIN_3, type, permission, expireType, 0);
+ PermissionTestUtils.add(TEST_DOMAIN_4, type, permission, expireType, 0);
+ PermissionTestUtils.add(TEST_DOMAIN_5, type, permission, expireType, 0);
+ } else {
+ ok(false, "Unexpected top page: " + topPage);
+ }
+}
+
+async function cleanUp() {
+ info("Cleaning up.");
+ SpecialPowers.clearUserPref("network.cookie.sameSite.laxByDefault");
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+}
+
+async function runRound(topPage, showPrompt, maxConcurrent, disableWebcompat) {
+ if (showPrompt) {
+ await preparePermissionsFromOtherSites(topPage);
+ await testDoorHanger(
+ BLOCK,
+ showPrompt,
+ true,
+ topPage,
+ maxConcurrent,
+ disableWebcompat
+ );
+ await cleanUp();
+ await preparePermissionsFromOtherSites(topPage);
+ await testDoorHanger(
+ BLOCK,
+ showPrompt,
+ false,
+ topPage,
+ maxConcurrent,
+ disableWebcompat
+ );
+ await cleanUp();
+ await preparePermissionsFromOtherSites(topPage);
+ await testDoorHanger(
+ ALLOW,
+ showPrompt,
+ false,
+ topPage,
+ maxConcurrent,
+ disableWebcompat
+ );
+ await cleanUp();
+ } else {
+ await preparePermissionsFromOtherSites(topPage);
+ await testDoorHanger(
+ ALLOW,
+ showPrompt,
+ false,
+ topPage,
+ maxConcurrent,
+ disableWebcompat
+ );
+ }
+ await cleanUp();
+}
+
+add_task(async function test_combinations() {
+ await runRound(TEST_TOP_PAGE, false, 1);
+ await runRound(TEST_TOP_PAGE_2, true, 1);
+ await runRound(TEST_TOP_PAGE, false, 5);
+ await runRound(TEST_TOP_PAGE_2, false, 5);
+ await runRound(TEST_TOP_PAGE_3, false, 5);
+ await runRound(TEST_TOP_PAGE_4, false, 5);
+ await runRound(TEST_TOP_PAGE_5, false, 5);
+ await runRound(TEST_TOP_PAGE_6, true, 5);
+});
+
+add_task(async function test_disableWebcompat() {
+ await runRound(TEST_TOP_PAGE, true, 5, true);
+});
diff --git a/toolkit/components/antitracking/test/browser/browser_storageAccessFrameInteractionGrantsUserInteraction.js b/toolkit/components/antitracking/test/browser/browser_storageAccessFrameInteractionGrantsUserInteraction.js
new file mode 100644
index 0000000000..13b882c0c7
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_storageAccessFrameInteractionGrantsUserInteraction.js
@@ -0,0 +1,70 @@
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/browser/modules/test/browser/head.js",
+ this
+);
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/toolkit/components/antitracking/test/browser/storage_access_head.js",
+ this
+);
+/* import-globals-from storageAccessAPIHelpers.js */
+
+async function testEmbeddedPageBehavior() {
+ // Get the storage access permission
+ SpecialPowers.wrap(document).notifyUserGestureActivation();
+ let p = document.requestStorageAccess();
+ try {
+ await p;
+ ok(true, "gain storage access.");
+ } catch {
+ ok(false, "denied storage access.");
+ }
+ SpecialPowers.wrap(document).clearUserGestureActivation();
+
+ // Wait until we have the permission before we remove it.
+ waitUntilPermission(
+ "https://tracking.example.org/",
+ "storageAccessAPI",
+ SpecialPowers.Services.perms.ALLOW_ACTION
+ );
+
+ // Remove the storageAccessAPI permission
+ SpecialPowers.removePermission(
+ "storageAccessAPI",
+ "https://tracking.example.org/"
+ );
+
+ // Wait until the permission is removed
+ waitUntilPermission(
+ "https://tracking.example.org/",
+ "storageAccessAPI",
+ SpecialPowers.Services.perms.UNKNOWN_ACTION
+ );
+
+ // Interact with the third-party iframe and wait for the permission to appear
+ SpecialPowers.wrap(document).userInteractionForTesting();
+ waitUntilPermission(
+ "https://tracking.example.org/",
+ "storageAccessAPI",
+ SpecialPowers.Services.perms.ALLOW_ACTION
+ );
+}
+
+// This test verifies that interacting with a third-party iframe with
+// storage access gives the storageAccessAPI permission, as if it were a first
+// party. This is done by loading a page, then within an iframe in that page
+// requesting storage access, ensuring there is no storageAccessAPI permission,
+// then interacting with the page and waiting for that storageAccessAPI
+// permission to reappear.
+add_task(async function testInteractionGivesPermission() {
+ await setPreferences();
+
+ await openPageAndRunCode(
+ TEST_TOP_PAGE,
+ getExpectPopupAndClick("accept"),
+ TEST_3RD_PARTY_PAGE,
+ testEmbeddedPageBehavior
+ );
+
+ await cleanUpData();
+ await SpecialPowers.flushPrefEnv();
+});
diff --git a/toolkit/components/antitracking/test/browser/browser_storageAccessGrantedGivesUserInteraction.js b/toolkit/components/antitracking/test/browser/browser_storageAccessGrantedGivesUserInteraction.js
new file mode 100644
index 0000000000..cd70d8c059
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_storageAccessGrantedGivesUserInteraction.js
@@ -0,0 +1,40 @@
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/browser/modules/test/browser/head.js",
+ this
+);
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/toolkit/components/antitracking/test/browser/storage_access_head.js",
+ this
+);
+/* import-globals-from storageAccessAPIHelpers.js */
+
+async function testEmbeddedPageBehavior() {
+ SpecialPowers.wrap(document).notifyUserGestureActivation();
+ var p = document.requestStorageAccess();
+ try {
+ await p;
+ ok(true, "gain storage access.");
+ } catch {
+ ok(false, "denied storage access.");
+ }
+ SpecialPowers.wrap(document).clearUserGestureActivation();
+ waitUntilPermission(
+ "https://tracking.example.org/",
+ "storageAccessAPI",
+ SpecialPowers.Services.perms.ALLOW_ACTION
+ );
+}
+
+add_task(async function testGrantGivesPermission() {
+ await setPreferences();
+
+ await openPageAndRunCode(
+ TEST_TOP_PAGE,
+ getExpectPopupAndClick("accept"),
+ TEST_3RD_PARTY_PAGE,
+ testEmbeddedPageBehavior
+ );
+
+ await cleanUpData();
+ await SpecialPowers.flushPrefEnv();
+});
diff --git a/toolkit/components/antitracking/test/browser/browser_storageAccessPrivilegeAPI.js b/toolkit/components/antitracking/test/browser/browser_storageAccessPrivilegeAPI.js
new file mode 100644
index 0000000000..ba109768be
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_storageAccessPrivilegeAPI.js
@@ -0,0 +1,617 @@
+//
+// Bug 1724376 - Tests for the privilege requestStorageAccessForOrigin API.
+//
+
+/* import-globals-from storageAccessAPIHelpers.js */
+
+"use strict";
+
+const TEST_ANOTHER_TRACKER_DOMAIN = "https://itisatracker.org/";
+const TEST_ANOTHER_TRACKER_PAGE =
+ TEST_ANOTHER_TRACKER_DOMAIN + TEST_PATH + "3rdParty.html";
+const TEST_ANOTHER_4TH_PARTY_DOMAIN = "https://test1.example.org/";
+const TEST_ANOTHER_4TH_PARTY_PAGE =
+ TEST_ANOTHER_4TH_PARTY_DOMAIN + TEST_PATH + "3rdParty.html";
+
+// Insert an iframe with the given id into the content.
+async function insertSubFrame(browser, url, id) {
+ return SpecialPowers.spawn(browser, [url, id], async (url, id) => {
+ let ifr = content.document.createElement("iframe");
+ ifr.setAttribute("id", id);
+
+ let loaded = ContentTaskUtils.waitForEvent(ifr, "load", false);
+ content.document.body.appendChild(ifr);
+ ifr.src = url;
+ await loaded;
+ });
+}
+
+// Run the given script in the iframe with the given id.
+function runScriptInSubFrame(browser, id, script) {
+ return SpecialPowers.spawn(
+ browser,
+ [{ callback: script.toString(), id }],
+ async obj => {
+ await new content.Promise(resolve => {
+ let ifr = content.document.getElementById(obj.id);
+
+ content.addEventListener("message", function msg(event) {
+ if (event.data.type == "finish") {
+ content.removeEventListener("message", msg);
+ resolve();
+ return;
+ }
+
+ if (event.data.type == "ok") {
+ ok(event.data.what, event.data.msg);
+ return;
+ }
+
+ if (event.data.type == "info") {
+ info(event.data.msg);
+ return;
+ }
+
+ ok(false, "Unknown message");
+ });
+
+ ifr.contentWindow.postMessage(obj.callback, "*");
+ });
+ }
+ );
+}
+
+function waitStoragePermission(trackingOrigin) {
+ return TestUtils.topicObserved("perm-changed", (aSubject, aData) => {
+ let permission = aSubject.QueryInterface(Ci.nsIPermission);
+ let uri = Services.io.newURI(TEST_DOMAIN);
+ return (
+ permission.type == `3rdPartyStorage^${trackingOrigin}` &&
+ permission.principal.equalsURI(uri)
+ );
+ });
+}
+
+function clearStoragePermission(trackingOrigin) {
+ return SpecialPowers.removePermission(
+ `3rdPartyStorage^${trackingOrigin}`,
+ TEST_TOP_PAGE
+ );
+}
+
+function triggerCommand(button) {
+ let notifications = PopupNotifications.panel.children;
+ let notification = notifications[0];
+ EventUtils.synthesizeMouseAtCenter(notification[button], {});
+}
+
+function triggerMainCommand() {
+ triggerCommand("button");
+}
+
+function triggerSecondaryCommand() {
+ triggerCommand("secondaryButton");
+}
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.storage_access.auto_grants", true],
+ ["dom.storage_access.auto_grants.delayed", false],
+ ["dom.storage_access.enabled", true],
+ ["dom.storage_access.prompt.testing", false],
+ ["privacy.trackingprotection.enabled", false],
+ ["privacy.trackingprotection.pbmode.enabled", false],
+ ["privacy.trackingprotection.annotate_channels", true],
+ // Bug 1617611: Fix all the tests broken by "cookies SameSite=lax by default"
+ ["network.cookie.sameSite.laxByDefault", false],
+ ],
+ });
+
+ await UrlClassifierTestUtils.addTestTrackers();
+
+ registerCleanupFunction(() => {
+ SpecialPowers.clearUserPref("network.cookie.sameSite.laxByDefault");
+ UrlClassifierTestUtils.cleanupTestTrackers();
+ });
+});
+
+add_task(async function test_api_only_available_in_privilege_scope() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ TEST_TOP_PAGE
+ );
+ let browser = tab.linkedBrowser;
+
+ await SpecialPowers.spawn(browser, [], async _ => {
+ ok(
+ content.document.requestStorageAccessForOrigin,
+ "The privilege API is available in system privilege code."
+ );
+ });
+
+ // Open an iframe and check if the privilege is not available in content code.
+ await insertSubFrame(browser, TEST_3RD_PARTY_PAGE, "test");
+ await runScriptInSubFrame(browser, "test", async function check() {
+ ok(
+ !document.requestStorageAccessForOrigin,
+ "The privilege API is not available in content code."
+ );
+ });
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function test_privilege_api_with_reject_tracker() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [
+ "network.cookie.cookieBehavior",
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER,
+ ],
+ [
+ "network.cookie.cookieBehavior.pbmode",
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER,
+ ],
+ ],
+ });
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ TEST_TOP_PAGE
+ );
+ let browser = tab.linkedBrowser;
+
+ await insertSubFrame(browser, TEST_3RD_PARTY_PAGE, "test");
+
+ // Verify that the third party tracker doesn't have storage access at
+ // beginning.
+ await runScriptInSubFrame(browser, "test", async _ => {
+ await noStorageAccessInitially();
+
+ is(document.cookie, "", "No cookies for me");
+ document.cookie = "name=value";
+ is(document.cookie, "", "Setting cookie is blocked");
+ });
+
+ let storagePermissionPromise = waitStoragePermission(
+ "https://tracking.example.org"
+ );
+
+ // Verify if the prompt has been shown.
+ let shownPromise = BrowserTestUtils.waitForEvent(
+ PopupNotifications.panel,
+ "popupshown"
+ );
+
+ // Call the privilege API.
+ let callAPIPromise = SpecialPowers.spawn(browser, [], async _ => {
+ // The privilege API requires user activation. So, we set the user
+ // activation flag before we call the API.
+ content.document.notifyUserGestureActivation();
+
+ try {
+ await content.document.requestStorageAccessForOrigin(
+ "https://tracking.example.org/"
+ );
+ } catch (e) {
+ ok(false, "The API shouldn't throw.");
+ }
+
+ content.document.clearUserGestureActivation();
+ });
+
+ await shownPromise;
+
+ // Accept the prompt
+ triggerMainCommand();
+
+ await callAPIPromise;
+
+ // Verify if the storage access permission is set correctly.
+ await storagePermissionPromise;
+
+ // Verify if the existing third-party tracker iframe gains the storage
+ // access.
+ await runScriptInSubFrame(browser, "test", async _ => {
+ await hasStorageAccessInitially();
+
+ is(document.cookie, "", "Still no cookies for me");
+ document.cookie = "name=value";
+ is(document.cookie, "name=value", "Successfully set cookies.");
+ });
+
+ // Insert another third-party tracker iframe and check if it has storage access.
+ await insertSubFrame(browser, TEST_3RD_PARTY_PAGE, "test2");
+ await runScriptInSubFrame(browser, "test2", async _ => {
+ await hasStorageAccessInitially();
+
+ is(document.cookie, "name=value", "Some cookies for me");
+ });
+
+ // Insert another iframe with different third-party tracker and check it has
+ // no storage access.
+ await insertSubFrame(browser, TEST_ANOTHER_TRACKER_PAGE, "test3");
+ await runScriptInSubFrame(browser, "test3", async _ => {
+ await noStorageAccessInitially();
+
+ is(document.cookie, "", "No cookies for me");
+ document.cookie = "name=value";
+ is(document.cookie, "", "Setting cookie is blocked for another tracker.");
+ });
+
+ await clearStoragePermission("https://tracking.example.org");
+ Services.cookies.removeAll();
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function test_privilege_api_with_dFPI() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [
+ "network.cookie.cookieBehavior",
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN,
+ ],
+ [
+ "network.cookie.cookieBehavior.pbmode",
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN,
+ ],
+ ],
+ });
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ TEST_TOP_PAGE
+ );
+ let browser = tab.linkedBrowser;
+
+ await insertSubFrame(browser, TEST_4TH_PARTY_PAGE, "test");
+
+ // Verify that the third-party context doesn't have storage access at
+ // beginning.
+ await runScriptInSubFrame(browser, "test", async _ => {
+ await noStorageAccessInitially();
+
+ is(document.cookie, "", "No cookies for me");
+ document.cookie = "name=partitioned";
+ is(
+ document.cookie,
+ "name=partitioned",
+ "Setting cookie in partitioned context."
+ );
+ });
+
+ let storagePermissionPromise = waitStoragePermission(
+ "http://not-tracking.example.com"
+ );
+
+ // Verify if the prompt has been shown.
+ let shownPromise = BrowserTestUtils.waitForEvent(
+ PopupNotifications.panel,
+ "popupshown"
+ );
+
+ // Call the privilege API.
+ let callAPIPromise = SpecialPowers.spawn(browser, [], async _ => {
+ // The privilege API requires a user gesture. So, we set the user handling
+ // flag before we call the API.
+ content.document.notifyUserGestureActivation();
+
+ try {
+ await content.document.requestStorageAccessForOrigin(
+ "http://not-tracking.example.com/"
+ );
+ } catch (e) {
+ ok(false, "The API shouldn't throw.");
+ }
+
+ content.document.clearUserGestureActivation();
+ });
+
+ await shownPromise;
+
+ // Accept the prompt
+ triggerMainCommand();
+
+ await callAPIPromise;
+
+ // Verify if the storage access permission is set correctly.
+ await storagePermissionPromise;
+
+ // Verify if the existing third-party iframe gains the storage access.
+ await runScriptInSubFrame(browser, "test", async _ => {
+ await hasStorageAccessInitially();
+
+ is(document.cookie, "", "No unpartitioned cookies");
+ document.cookie = "name=unpartitioned";
+ is(document.cookie, "name=unpartitioned", "Successfully set cookies.");
+ });
+
+ // Insert another third-party content iframe and check if it has storage access.
+ await insertSubFrame(browser, TEST_4TH_PARTY_PAGE, "test2");
+ await runScriptInSubFrame(browser, "test2", async _ => {
+ await hasStorageAccessInitially();
+
+ is(
+ document.cookie,
+ "name=unpartitioned",
+ "Some cookies for unpartitioned context"
+ );
+ });
+
+ // Insert another iframe with different third-party content and check it has
+ // no storage access.
+ await insertSubFrame(browser, TEST_ANOTHER_4TH_PARTY_PAGE, "test3");
+ await runScriptInSubFrame(browser, "test3", async _ => {
+ await noStorageAccessInitially();
+
+ is(document.cookie, "", "No cookies for me");
+ document.cookie = "name=value";
+ is(document.cookie, "name=value", "Setting cookie to partitioned context.");
+ });
+
+ await clearStoragePermission("http://not-tracking.example.com");
+ Services.cookies.removeAll();
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function test_prompt() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.storage_access.auto_grants", false],
+ [
+ "network.cookie.cookieBehavior",
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER,
+ ],
+ [
+ "network.cookie.cookieBehavior.pbmode",
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER,
+ ],
+ ],
+ });
+
+ for (const allow of [false, true]) {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ TEST_TOP_PAGE
+ );
+ let browser = tab.linkedBrowser;
+
+ // Verify if the prompt has been shown.
+ let shownPromise = BrowserTestUtils.waitForEvent(
+ PopupNotifications.panel,
+ "popupshown"
+ );
+
+ let hiddenPromise = BrowserTestUtils.waitForEvent(
+ PopupNotifications.panel,
+ "popuphidden"
+ );
+
+ // Call the privilege API.
+ let callAPIPromise = SpecialPowers.spawn(browser, [allow], async allow => {
+ // The privilege API requires a user gesture. So, we set the user handling
+ // flag before we call the API.
+ content.document.notifyUserGestureActivation();
+ let isThrown = false;
+
+ try {
+ await content.document.requestStorageAccessForOrigin(
+ "https://tracking.example.org"
+ );
+ } catch (e) {
+ isThrown = true;
+ }
+
+ is(isThrown, !allow, `The API ${allow ? "shouldn't" : "should"} throw.`);
+
+ content.document.clearUserGestureActivation();
+ });
+
+ await shownPromise;
+
+ let notification = await TestUtils.waitForCondition(_ =>
+ PopupNotifications.getNotification("storage-access", browser)
+ );
+ ok(notification, "Should have gotten the notification");
+
+ // Click the popup button.
+ if (allow) {
+ triggerMainCommand();
+ } else {
+ triggerSecondaryCommand();
+ }
+
+ // Wait until the popup disappears.
+ await hiddenPromise;
+
+ // Wait until the API finishes.
+ await callAPIPromise;
+
+ await insertSubFrame(browser, TEST_3RD_PARTY_PAGE, "test");
+
+ if (allow) {
+ await runScriptInSubFrame(browser, "test", async _ => {
+ await hasStorageAccessInitially();
+
+ is(document.cookie, "", "Still no cookies for me");
+ document.cookie = "name=value";
+ is(document.cookie, "name=value", "Successfully set cookies.");
+ });
+ } else {
+ await runScriptInSubFrame(browser, "test", async _ => {
+ await noStorageAccessInitially();
+
+ is(document.cookie, "", "Still no cookies for me");
+ document.cookie = "name=value";
+ is(document.cookie, "", "No cookie after setting.");
+ });
+ }
+
+ BrowserTestUtils.removeTab(tab);
+ }
+
+ await clearStoragePermission("https://tracking.example.org");
+ Services.cookies.removeAll();
+});
+
+// Tests that the priviledged rSA method should show a prompt when auto grants
+// are enabled, but we don't have user activation. When requiring user
+// activation, rSA should still reject.
+add_task(async function test_prompt_no_user_activation() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.storage_access.auto_grants", true],
+ [
+ "network.cookie.cookieBehavior",
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER,
+ ],
+ [
+ "network.cookie.cookieBehavior.pbmode",
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER,
+ ],
+ ],
+ });
+
+ for (let requireUserActivation of [false, true]) {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ TEST_TOP_PAGE
+ );
+ let browser = tab.linkedBrowser;
+
+ let shownPromise, hiddenPromise;
+
+ // Verify if the prompt has been shown.
+ if (!requireUserActivation) {
+ shownPromise = BrowserTestUtils.waitForEvent(
+ PopupNotifications.panel,
+ "popupshown"
+ );
+
+ hiddenPromise = BrowserTestUtils.waitForEvent(
+ PopupNotifications.panel,
+ "popuphidden"
+ );
+ }
+
+ // Call the privilege API.
+ let callAPIPromise = SpecialPowers.spawn(
+ browser,
+ [requireUserActivation],
+ async requireUserActivation => {
+ let isThrown = false;
+
+ try {
+ await content.document.requestStorageAccessForOrigin(
+ "https://tracking.example.org",
+ requireUserActivation
+ );
+ } catch (e) {
+ isThrown = true;
+ }
+
+ is(
+ isThrown,
+ requireUserActivation,
+ `The API ${requireUserActivation ? "shouldn't" : "should"} throw.`
+ );
+ }
+ );
+
+ if (!requireUserActivation) {
+ await shownPromise;
+
+ let notification = await TestUtils.waitForCondition(_ =>
+ PopupNotifications.getNotification("storage-access", browser)
+ );
+ ok(notification, "Should have gotten the notification");
+
+ // Click the popup button.
+ triggerMainCommand();
+
+ // Wait until the popup disappears.
+ await hiddenPromise;
+ }
+
+ // Wait until the API finishes.
+ await callAPIPromise;
+
+ await insertSubFrame(browser, TEST_3RD_PARTY_PAGE, "test");
+
+ if (!requireUserActivation) {
+ await runScriptInSubFrame(browser, "test", async _ => {
+ await hasStorageAccessInitially();
+
+ is(document.cookie, "", "Still no cookies for me");
+ document.cookie = "name=value";
+ is(document.cookie, "name=value", "Successfully set cookies.");
+ });
+ } else {
+ await runScriptInSubFrame(browser, "test", async _ => {
+ await noStorageAccessInitially();
+
+ is(document.cookie, "", "Still no cookies for me");
+ document.cookie = "name=value";
+ is(document.cookie, "", "No cookie after setting.");
+ });
+ }
+
+ BrowserTestUtils.removeTab(tab);
+ await clearStoragePermission("https://tracking.example.org");
+ Services.cookies.removeAll();
+ }
+});
+
+add_task(async function test_invalid_input() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ TEST_TOP_PAGE
+ );
+ let browser = tab.linkedBrowser;
+
+ await SpecialPowers.spawn(browser, [], async _ => {
+ let isThrown = false;
+ try {
+ await content.document.requestStorageAccessForOrigin(
+ "https://tracking.example.org"
+ );
+ } catch (e) {
+ isThrown = true;
+ }
+ ok(isThrown, "The API should throw without user gesture.");
+
+ content.document.notifyUserGestureActivation();
+ isThrown = false;
+ try {
+ await content.document.requestStorageAccessForOrigin();
+ } catch (e) {
+ isThrown = true;
+ }
+ ok(isThrown, "The API should throw with no input.");
+
+ content.document.notifyUserGestureActivation();
+ isThrown = false;
+ try {
+ await content.document.requestStorageAccessForOrigin("");
+ } catch (e) {
+ isThrown = true;
+ is(e.name, "NS_ERROR_MALFORMED_URI", "The input is not a valid url");
+ }
+ ok(isThrown, "The API should throw with empty string.");
+
+ content.document.notifyUserGestureActivation();
+ isThrown = false;
+ try {
+ await content.document.requestStorageAccessForOrigin("invalid url");
+ } catch (e) {
+ isThrown = true;
+ is(e.name, "NS_ERROR_MALFORMED_URI", "The input is not a valid url");
+ }
+ ok(isThrown, "The API should throw with invalid url.");
+
+ content.document.clearUserGestureActivation();
+ });
+
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/toolkit/components/antitracking/test/browser/browser_storageAccessPromiseRejectHandlerUserInteraction.js b/toolkit/components/antitracking/test/browser/browser_storageAccessPromiseRejectHandlerUserInteraction.js
new file mode 100644
index 0000000000..835c02e262
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_storageAccessPromiseRejectHandlerUserInteraction.js
@@ -0,0 +1,36 @@
+AntiTracking.runTest(
+ "Storage Access API returns promises that do not maintain user activation for calling its reject handler",
+ // blocking callback
+ async _ => {
+ /* import-globals-from storageAccessAPIHelpers.js */
+ let [threw, rejected] = await callRequestStorageAccess(() => {
+ ok(
+ !SpecialPowers.wrap(document).hasValidTransientUserGestureActivation,
+ "Promise reject handler must not have user activation"
+ );
+ }, true);
+ ok(!threw, "requestStorageAccess should not throw");
+ ok(rejected, "requestStorageAccess should not be available");
+ },
+
+ null, // non-blocking callback
+ // cleanup function
+ async _ => {
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+ },
+ [
+ [
+ "privacy.partition.always_partition_third_party_non_cookie_storage",
+ false,
+ ],
+ ], // extra prefs
+ false, // no window open test
+ false, // no user-interaction test
+ 0, // expected blocking notifications
+ false, // private window
+ "allow-scripts allow-same-origin allow-popups" // iframe sandbox
+);
diff --git a/toolkit/components/antitracking/test/browser/browser_storageAccessPromiseRejectHandlerUserInteraction_alwaysPartition.js b/toolkit/components/antitracking/test/browser/browser_storageAccessPromiseRejectHandlerUserInteraction_alwaysPartition.js
new file mode 100644
index 0000000000..21bf8b7639
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_storageAccessPromiseRejectHandlerUserInteraction_alwaysPartition.js
@@ -0,0 +1,31 @@
+AntiTracking.runTest(
+ "Storage Access API returns promises that do not maintain user activation for calling its reject handler",
+ // blocking callback
+ async _ => {
+ /* import-globals-from storageAccessAPIHelpers.js */
+ let [threw, rejected] = await callRequestStorageAccess(() => {
+ ok(
+ !SpecialPowers.wrap(document).hasValidTransientUserGestureActivation,
+ "Promise reject handler must not have user activation"
+ );
+ }, true);
+ ok(!threw, "requestStorageAccess should not throw");
+ ok(rejected, "requestStorageAccess should not be available");
+ },
+
+ null, // non-blocking callback
+ // cleanup function
+ async _ => {
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+ },
+ [["privacy.partition.always_partition_third_party_non_cookie_storage", true]], // extra prefs
+ false, // no window open test
+ false, // no user-interaction test
+ Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER, // expected blocking notifications
+ false, // private window
+ "allow-scripts allow-same-origin allow-popups" // iframe sandbox
+);
diff --git a/toolkit/components/antitracking/test/browser/browser_storageAccessPromiseResolveHandlerUserInteraction.js b/toolkit/components/antitracking/test/browser/browser_storageAccessPromiseResolveHandlerUserInteraction.js
new file mode 100644
index 0000000000..6db7c4c241
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_storageAccessPromiseResolveHandlerUserInteraction.js
@@ -0,0 +1,37 @@
+AntiTracking.runTest(
+ "Storage Access API returns promises that maintain user activation",
+ // blocking callback
+ async _ => {
+ /* import-globals-from storageAccessAPIHelpers.js */
+ let [threw, rejected] = await callRequestStorageAccess(() => {
+ ok(
+ SpecialPowers.wrap(document).hasValidTransientUserGestureActivation,
+ "Promise handler must run as if we're handling user input"
+ );
+ });
+ ok(!threw, "requestStorageAccess should not throw");
+ ok(!rejected, "requestStorageAccess should be available");
+
+ SpecialPowers.wrap(document).notifyUserGestureActivation();
+
+ await document.hasStorageAccess();
+
+ ok(
+ SpecialPowers.wrap(document).hasValidTransientUserGestureActivation,
+ "Promise handler must run as if we're handling user input"
+ );
+ },
+
+ null, // non-blocking callback
+ // cleanup function
+ async _ => {
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+ },
+ null, // extra prefs
+ false, // no window open test
+ false // no user-interaction test
+);
diff --git a/toolkit/components/antitracking/test/browser/browser_storageAccessRemovalNavigateSubframe.js b/toolkit/components/antitracking/test/browser/browser_storageAccessRemovalNavigateSubframe.js
new file mode 100644
index 0000000000..f658dde790
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_storageAccessRemovalNavigateSubframe.js
@@ -0,0 +1,46 @@
+AntiTracking.runTest(
+ "Storage Access is removed when subframe navigates",
+ // blocking callback
+ async _ => {
+ /* import-globals-from storageAccessAPIHelpers.js */
+ await noStorageAccessInitially();
+ },
+
+ // non-blocking callback
+ async _ => {
+ /* import-globals-from storageAccessAPIHelpers.js */
+ await hasStorageAccessInitially();
+
+ /* import-globals-from storageAccessAPIHelpers.js */
+ let [threw, rejected] = await callRequestStorageAccess();
+ ok(!threw, "requestStorageAccess should not throw");
+ ok(!rejected, "requestStorageAccess should be available");
+ },
+ // cleanup function
+ async _ => {
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+ },
+ [
+ [
+ "privacy.partition.always_partition_third_party_non_cookie_storage",
+ false,
+ ],
+ ], // extra prefs
+ false, // no window open test
+ false, // no user-interaction test
+ 0, // no blocking notifications
+ false, // run in normal window
+ null, // no iframe sandbox
+ "navigate-subframe", // access removal type
+ // after-removal callback
+ async _ => {
+ /* import-globals-from storageAccessAPIHelpers.js */
+ // TODO: this is just a temporarily fixed, we should update the testcase
+ // in Bug 1649399
+ await hasStorageAccessInitially();
+ }
+);
diff --git a/toolkit/components/antitracking/test/browser/browser_storageAccessRemovalNavigateSubframe_alwaysPartition.js b/toolkit/components/antitracking/test/browser/browser_storageAccessRemovalNavigateSubframe_alwaysPartition.js
new file mode 100644
index 0000000000..b4355f2a24
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_storageAccessRemovalNavigateSubframe_alwaysPartition.js
@@ -0,0 +1,41 @@
+AntiTracking.runTest(
+ "Storage Access is removed when subframe navigates",
+ // blocking callback
+ async _ => {
+ /* import-globals-from storageAccessAPIHelpers.js */
+ await noStorageAccessInitially();
+ },
+
+ // non-blocking callback
+ async _ => {
+ /* import-globals-from storageAccessAPIHelpers.js */
+ await hasStorageAccessInitially();
+
+ /* import-globals-from storageAccessAPIHelpers.js */
+ let [threw, rejected] = await callRequestStorageAccess();
+ ok(!threw, "requestStorageAccess should not throw");
+ ok(!rejected, "requestStorageAccess should be available");
+ },
+ // cleanup function
+ async _ => {
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+ },
+ [["privacy.partition.always_partition_third_party_non_cookie_storage", true]], // extra prefs
+ false, // no window open test
+ false, // no user-interaction test
+ Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER, // expected blocking notifications
+ false, // run in normal window
+ null, // no iframe sandbox
+ "navigate-subframe", // access removal type
+ // after-removal callback
+ async _ => {
+ /* import-globals-from storageAccessAPIHelpers.js */
+ // TODO: this is just a temporarily fixed, we should update the testcase
+ // in Bug 1649399
+ await hasStorageAccessInitially();
+ }
+);
diff --git a/toolkit/components/antitracking/test/browser/browser_storageAccessRemovalNavigateTopframe.js b/toolkit/components/antitracking/test/browser/browser_storageAccessRemovalNavigateTopframe.js
new file mode 100644
index 0000000000..fe79a8d935
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_storageAccessRemovalNavigateTopframe.js
@@ -0,0 +1,44 @@
+AntiTracking.runTest(
+ "Storage Access is removed when topframe navigates",
+ // blocking callback
+ async _ => {
+ /* import-globals-from storageAccessAPIHelpers.js */
+ await noStorageAccessInitially();
+ },
+
+ // non-blocking callback
+ async _ => {
+ /* import-globals-from storageAccessAPIHelpers.js */
+ await hasStorageAccessInitially();
+
+ /* import-globals-from storageAccessAPIHelpers.js */
+ let [threw, rejected] = await callRequestStorageAccess();
+ ok(!threw, "requestStorageAccess should not throw");
+ ok(!rejected, "requestStorageAccess should be available");
+ },
+ // cleanup function
+ async _ => {
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+ },
+ [
+ [
+ "privacy.partition.always_partition_third_party_non_cookie_storage",
+ false,
+ ],
+ ], // extra prefs
+ false, // no window open test
+ false, // no user-interaction test
+ 0, // no blocking notifications
+ false, // run in normal window
+ null, // no iframe sandbox
+ "navigate-topframe", // access removal type
+ // after-removal callback
+ async _ => {
+ /* import-globals-from storageAccessAPIHelpers.js */
+ await noStorageAccessInitially();
+ }
+);
diff --git a/toolkit/components/antitracking/test/browser/browser_storageAccessRemovalNavigateTopframe_alwaysPartition.js b/toolkit/components/antitracking/test/browser/browser_storageAccessRemovalNavigateTopframe_alwaysPartition.js
new file mode 100644
index 0000000000..63a0480e83
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_storageAccessRemovalNavigateTopframe_alwaysPartition.js
@@ -0,0 +1,39 @@
+AntiTracking.runTest(
+ "Storage Access is removed when topframe navigates",
+ // blocking callback
+ async _ => {
+ /* import-globals-from storageAccessAPIHelpers.js */
+ await noStorageAccessInitially();
+ },
+
+ // non-blocking callback
+ async _ => {
+ /* import-globals-from storageAccessAPIHelpers.js */
+ await hasStorageAccessInitially();
+
+ /* import-globals-from storageAccessAPIHelpers.js */
+ let [threw, rejected] = await callRequestStorageAccess();
+ ok(!threw, "requestStorageAccess should not throw");
+ ok(!rejected, "requestStorageAccess should be available");
+ },
+ // cleanup function
+ async _ => {
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+ },
+ [["privacy.partition.always_partition_third_party_non_cookie_storage", true]], // extra prefs
+ false, // no window open test
+ false, // no user-interaction test
+ Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER, // expected blocking notifications
+ false, // run in normal window
+ null, // no iframe sandbox
+ "navigate-topframe", // access removal type
+ // after-removal callback
+ async _ => {
+ /* import-globals-from storageAccessAPIHelpers.js */
+ await noStorageAccessInitially();
+ }
+);
diff --git a/toolkit/components/antitracking/test/browser/browser_storageAccessSandboxed.js b/toolkit/components/antitracking/test/browser/browser_storageAccessSandboxed.js
new file mode 100644
index 0000000000..d83bcf4b7b
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_storageAccessSandboxed.js
@@ -0,0 +1,214 @@
+/* import-globals-from storageAccessAPIHelpers.js */
+
+const APS_PREF =
+ "privacy.partition.always_partition_third_party_non_cookie_storage";
+
+AntiTracking.runTest(
+ "Storage Access API called in a sandboxed iframe",
+ // blocking callback
+ async _ => {
+ let [threw, rejected] = await callRequestStorageAccess();
+ ok(!threw, "requestStorageAccess should not throw");
+ ok(rejected, "requestStorageAccess shouldn't be available");
+ },
+
+ null, // non-blocking callback
+ // cleanup function
+ async _ => {
+ // Only clear the user-interaction permissions for the tracker here so that
+ // the next test has a clean slate.
+ await new Promise(resolve => {
+ Services.clearData.deleteDataFromHost(
+ Services.io.newURI(TEST_3RD_PARTY_DOMAIN).host,
+ true,
+ Ci.nsIClearDataService.CLEAR_PERMISSIONS,
+ value => resolve()
+ );
+ });
+ },
+ [
+ ["dom.storage_access.enabled", true],
+ [APS_PREF, false],
+ ], // extra prefs
+ false, // no window open test
+ false, // no user-interaction test
+ 0, // no blocking notifications
+ false, // run in normal window
+ "allow-scripts allow-same-origin allow-popups"
+);
+
+AntiTracking.runTest(
+ "Exception List can work in a sandboxed iframe",
+ // blocking callback
+ async _ => {
+ await hasStorageAccessInitially();
+
+ try {
+ await navigator.serviceWorker.register("empty.js");
+
+ ok(
+ true,
+ "ServiceWorker can be registered in allowlisted sandboxed iframe!"
+ );
+ } catch (e) {
+ info("Promise rejected: " + e);
+ ok(
+ false,
+ "ServiceWorker should be able to be registered in allowlisted sandboxed iframe"
+ );
+ }
+ },
+
+ null, // non-blocking callback
+ null, // cleanup function
+ [
+ ["dom.storage_access.enabled", true],
+ [
+ "privacy.restrict3rdpartystorage.skip_list",
+ "http://example.net,https://tracking.example.org",
+ ],
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ [APS_PREF, false],
+ ], // extra prefs
+ false, // no window open test
+ false, // no user-interaction test
+ 0, // no blocking notifications
+ false, // run in normal window
+ "allow-scripts allow-same-origin allow-popups"
+);
+
+AntiTracking.runTest(
+ "Storage Access API called in a sandboxed iframe with" +
+ " allow-storage-access-by-user-activation",
+ // blocking callback
+ async _ => {
+ await noStorageAccessInitially();
+
+ let [threw, rejected] = await callRequestStorageAccess();
+ ok(!threw, "requestStorageAccess should not throw");
+ ok(!rejected, "requestStorageAccess should be available");
+ },
+
+ null, // non-blocking callback
+ null, // cleanup function
+ [
+ ["dom.storage_access.enabled", true],
+ [APS_PREF, false],
+ ], // extra prefs
+ false, // no window open test
+ false, // no user-interaction test
+ Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER, // expect blocking notifications
+ false, // run in normal window
+ "allow-scripts allow-same-origin allow-popups allow-storage-access-by-user-activation"
+);
+
+AntiTracking.runTest(
+ "Verify that sandboxed contexts don't get the saved permission",
+ // blocking callback
+ async _ => {
+ await noStorageAccessInitially();
+
+ try {
+ localStorage.foo = 42;
+ ok(false, "LocalStorage cannot be used!");
+ } catch (e) {
+ ok(true, "LocalStorage cannot be used!");
+ is(e.name, "SecurityError", "We want a security error message.");
+ }
+ },
+
+ null, // non-blocking callback
+ null, // cleanup function
+ [
+ ["dom.storage_access.enabled", true],
+ [APS_PREF, false],
+ ], // extra prefs
+ false, // no window open test
+ false, // no user-interaction test
+ Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER, // expect blocking notifications
+ false, // run in normal window
+ "allow-scripts allow-same-origin allow-popups"
+);
+
+AntiTracking.runTest(
+ "Verify that sandboxed contexts with" +
+ " allow-storage-access-by-user-activation get the" +
+ " saved permission",
+ // blocking callback
+ async _ => {
+ await hasStorageAccessInitially();
+
+ localStorage.foo = 42;
+ ok(true, "LocalStorage can be used!");
+ },
+
+ null, // non-blocking callback
+ null, // cleanup function
+ [
+ ["dom.storage_access.enabled", true],
+ [APS_PREF, false],
+ ], // extra prefs
+ false, // no window open test
+ false, // no user-interaction test
+ 0, // no blocking notifications
+ false, // run in normal window
+ "allow-scripts allow-same-origin allow-popups allow-storage-access-by-user-activation"
+);
+
+AntiTracking.runTest(
+ "Verify that private browsing contexts don't get the saved permission",
+ // blocking callback
+ async _ => {
+ await noStorageAccessInitially();
+
+ try {
+ localStorage.foo = 42;
+ ok(false, "LocalStorage cannot be used!");
+ } catch (e) {
+ ok(true, "LocalStorage cannot be used!");
+ is(e.name, "SecurityError", "We want a security error message.");
+ }
+ },
+
+ null, // non-blocking callback
+ null, // cleanup function
+ [
+ ["dom.storage_access.enabled", true],
+ [APS_PREF, false],
+ ], // extra prefs
+ false, // no window open test
+ false, // no user-interaction test
+ Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER, // expect blocking notifications
+ true, // run in private window
+ null // iframe sandbox
+);
+
+AntiTracking.runTest(
+ "Verify that non-sandboxed contexts get the saved permission",
+ // blocking callback
+ async _ => {
+ await hasStorageAccessInitially();
+
+ localStorage.foo = 42;
+ ok(true, "LocalStorage can be used!");
+ },
+
+ null, // non-blocking callback
+ // cleanup function
+ async _ => {
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+ },
+ [
+ ["dom.storage_access.enabled", true],
+ [APS_PREF, false],
+ ], // extra prefs
+ false, // no window open test
+ false, // no user-interaction test
+ 0 // no blocking notifications
+);
diff --git a/toolkit/components/antitracking/test/browser/browser_storageAccessSandboxed_alwaysPartition.js b/toolkit/components/antitracking/test/browser/browser_storageAccessSandboxed_alwaysPartition.js
new file mode 100644
index 0000000000..402ba74ec8
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_storageAccessSandboxed_alwaysPartition.js
@@ -0,0 +1,214 @@
+/* import-globals-from storageAccessAPIHelpers.js */
+
+const APS_PREF =
+ "privacy.partition.always_partition_third_party_non_cookie_storage";
+
+AntiTracking.runTest(
+ "Storage Access API called in a sandboxed iframe",
+ // blocking callback
+ async _ => {
+ let [threw, rejected] = await callRequestStorageAccess();
+ ok(!threw, "requestStorageAccess should not throw");
+ ok(rejected, "requestStorageAccess shouldn't be available");
+ },
+
+ null, // non-blocking callback
+ // cleanup function
+ async _ => {
+ // Only clear the user-interaction permissions for the tracker here so that
+ // the next test has a clean slate.
+ await new Promise(resolve => {
+ Services.clearData.deleteDataFromHost(
+ Services.io.newURI(TEST_3RD_PARTY_DOMAIN).host,
+ true,
+ Ci.nsIClearDataService.CLEAR_PERMISSIONS,
+ value => resolve()
+ );
+ });
+ },
+ [
+ ["dom.storage_access.enabled", true],
+ [APS_PREF, true],
+ ], // extra prefs
+ false, // no window open test
+ false, // no user-interaction test
+ Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER, // expected blocking notifications
+ false, // run in normal window
+ "allow-scripts allow-same-origin allow-popups"
+);
+
+AntiTracking.runTest(
+ "Exception List can work in a sandboxed iframe",
+ // blocking callback
+ async _ => {
+ await hasStorageAccessInitially();
+
+ try {
+ await navigator.serviceWorker.register("empty.js");
+
+ ok(
+ true,
+ "ServiceWorker can be registered in allowlisted sandboxed iframe!"
+ );
+ } catch (e) {
+ info("Promise rejected: " + e);
+ ok(
+ false,
+ "ServiceWorker should be able to be registered in allowlisted sandboxed iframe"
+ );
+ }
+ },
+
+ null, // non-blocking callback
+ null, // cleanup function
+ [
+ ["dom.storage_access.enabled", true],
+ [
+ "privacy.restrict3rdpartystorage.skip_list",
+ "http://example.net,https://tracking.example.org",
+ ],
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ [APS_PREF, true],
+ ], // extra prefs
+ false, // no window open test
+ false, // no user-interaction test
+ 0, // no blocking notifications
+ false, // run in normal window
+ "allow-scripts allow-same-origin allow-popups"
+);
+
+AntiTracking.runTest(
+ "Storage Access API called in a sandboxed iframe with" +
+ " allow-storage-access-by-user-activation",
+ // blocking callback
+ async _ => {
+ await noStorageAccessInitially();
+
+ let [threw, rejected] = await callRequestStorageAccess();
+ ok(!threw, "requestStorageAccess should not throw");
+ ok(!rejected, "requestStorageAccess should be available");
+ },
+
+ null, // non-blocking callback
+ null, // cleanup function
+ [
+ ["dom.storage_access.enabled", true],
+ [APS_PREF, true],
+ ], // extra prefs
+ false, // no window open test
+ false, // no user-interaction test
+ Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER, // expect blocking notifications
+ false, // run in normal window
+ "allow-scripts allow-same-origin allow-popups allow-storage-access-by-user-activation"
+);
+
+AntiTracking.runTest(
+ "Verify that sandboxed contexts don't get the saved permission",
+ // blocking callback
+ async _ => {
+ await noStorageAccessInitially();
+
+ try {
+ localStorage.foo = 42;
+ ok(false, "LocalStorage cannot be used!");
+ } catch (e) {
+ ok(true, "LocalStorage cannot be used!");
+ is(e.name, "SecurityError", "We want a security error message.");
+ }
+ },
+
+ null, // non-blocking callback
+ null, // cleanup function
+ [
+ ["dom.storage_access.enabled", true],
+ [APS_PREF, true],
+ ], // extra prefs
+ false, // no window open test
+ false, // no user-interaction test
+ Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER, // expect blocking notifications
+ false, // run in normal window
+ "allow-scripts allow-same-origin allow-popups"
+);
+
+AntiTracking.runTest(
+ "Verify that sandboxed contexts with" +
+ " allow-storage-access-by-user-activation get the" +
+ " saved permission",
+ // blocking callback
+ async _ => {
+ await hasStorageAccessInitially();
+
+ localStorage.foo = 42;
+ ok(true, "LocalStorage can be used!");
+ },
+
+ null, // non-blocking callback
+ null, // cleanup function
+ [
+ ["dom.storage_access.enabled", true],
+ [APS_PREF, true],
+ ], // extra prefs
+ false, // no window open test
+ false, // no user-interaction test
+ 0, // no blocking notifications
+ false, // run in normal window
+ "allow-scripts allow-same-origin allow-popups allow-storage-access-by-user-activation"
+);
+
+AntiTracking.runTest(
+ "Verify that private browsing contexts don't get the saved permission",
+ // blocking callback
+ async _ => {
+ await noStorageAccessInitially();
+
+ try {
+ localStorage.foo = 42;
+ ok(false, "LocalStorage cannot be used!");
+ } catch (e) {
+ ok(true, "LocalStorage cannot be used!");
+ is(e.name, "SecurityError", "We want a security error message.");
+ }
+ },
+
+ null, // non-blocking callback
+ null, // cleanup function
+ [
+ ["dom.storage_access.enabled", true],
+ [APS_PREF, true],
+ ], // extra prefs
+ false, // no window open test
+ false, // no user-interaction test
+ Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER, // expect blocking notifications
+ true, // run in private window
+ null // iframe sandbox
+);
+
+AntiTracking.runTest(
+ "Verify that non-sandboxed contexts get the saved permission",
+ // blocking callback
+ async _ => {
+ await hasStorageAccessInitially();
+
+ localStorage.foo = 42;
+ ok(true, "LocalStorage can be used!");
+ },
+
+ null, // non-blocking callback
+ // cleanup function
+ async _ => {
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+ },
+ [
+ ["dom.storage_access.enabled", true],
+ [APS_PREF, true],
+ ], // extra prefs
+ false, // no window open test
+ false, // no user-interaction test
+ 0 // no blocking notifications
+);
diff --git a/toolkit/components/antitracking/test/browser/browser_storageAccessScopeDifferentSite.js b/toolkit/components/antitracking/test/browser/browser_storageAccessScopeDifferentSite.js
new file mode 100644
index 0000000000..5b3edec49e
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_storageAccessScopeDifferentSite.js
@@ -0,0 +1,64 @@
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/browser/modules/test/browser/head.js",
+ this
+);
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/toolkit/components/antitracking/test/browser/storage_access_head.js",
+ this
+);
+
+add_task(async function testInitialBlock() {
+ await setPreferences();
+
+ await openPageAndRunCode(
+ TEST_TOP_PAGE_7,
+ getExpectPopupAndClick("reject"),
+ TEST_3RD_PARTY_PAGE,
+ requestStorageAccessAndExpectFailure
+ );
+
+ await cleanUpData();
+ await SpecialPowers.flushPrefEnv();
+});
+
+add_task(async function testDifferentSitePermission() {
+ await setPreferences(/*alwaysPartitionStorage*/ false);
+
+ await openPageAndRunCode(
+ TEST_TOP_PAGE_7,
+ getExpectPopupAndClick("accept"),
+ TEST_3RD_PARTY_PAGE,
+ requestStorageAccessAndExpectSuccess
+ );
+
+ await openPageAndRunCode(
+ TEST_TOP_PAGE,
+ getExpectPopupAndClick("reject"),
+ TEST_3RD_PARTY_PAGE,
+ requestStorageAccessAndExpectFailure
+ );
+
+ await cleanUpData();
+ await SpecialPowers.flushPrefEnv();
+});
+
+add_task(async function testDifferentSitePermissionAPS() {
+ await setPreferences(/*alwaysPartitionStorage*/ true);
+
+ await openPageAndRunCode(
+ TEST_TOP_PAGE_7,
+ getExpectPopupAndClick("accept"),
+ TEST_3RD_PARTY_PAGE,
+ requestStorageAccessAndExpectSuccess
+ );
+
+ await openPageAndRunCode(
+ TEST_TOP_PAGE,
+ getExpectPopupAndClick("reject"),
+ TEST_3RD_PARTY_PAGE,
+ requestStorageAccessAndExpectFailure
+ );
+
+ await cleanUpData();
+ await SpecialPowers.flushPrefEnv();
+});
diff --git a/toolkit/components/antitracking/test/browser/browser_storageAccessScopeSameOrigin.js b/toolkit/components/antitracking/test/browser/browser_storageAccessScopeSameOrigin.js
new file mode 100644
index 0000000000..f0b1aca0b6
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_storageAccessScopeSameOrigin.js
@@ -0,0 +1,43 @@
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/browser/modules/test/browser/head.js",
+ this
+);
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/toolkit/components/antitracking/test/browser/storage_access_head.js",
+ this
+);
+
+add_task(async function testInitialBlock() {
+ await setPreferences();
+
+ await openPageAndRunCode(
+ TEST_TOP_PAGE_7,
+ getExpectPopupAndClick("reject"),
+ TEST_3RD_PARTY_PAGE,
+ requestStorageAccessAndExpectFailure
+ );
+
+ await cleanUpData();
+ await SpecialPowers.flushPrefEnv();
+});
+
+add_task(async function testSameOriginPermission() {
+ await setPreferences();
+
+ await openPageAndRunCode(
+ TEST_TOP_PAGE_7,
+ getExpectPopupAndClick("accept"),
+ TEST_3RD_PARTY_PAGE,
+ requestStorageAccessAndExpectSuccess
+ );
+
+ await openPageAndRunCode(
+ TEST_TOP_PAGE_7,
+ expectNoPopup,
+ TEST_3RD_PARTY_PAGE,
+ requestStorageAccessAndExpectSuccess
+ );
+
+ await cleanUpData();
+ await SpecialPowers.flushPrefEnv();
+});
diff --git a/toolkit/components/antitracking/test/browser/browser_storageAccessScopeSameSiteRead.js b/toolkit/components/antitracking/test/browser/browser_storageAccessScopeSameSiteRead.js
new file mode 100644
index 0000000000..0dd893eda6
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_storageAccessScopeSameSiteRead.js
@@ -0,0 +1,43 @@
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/browser/modules/test/browser/head.js",
+ this
+);
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/toolkit/components/antitracking/test/browser/storage_access_head.js",
+ this
+);
+
+add_task(async function testInitialBlock() {
+ await setPreferences();
+
+ await openPageAndRunCode(
+ TEST_TOP_PAGE_7,
+ getExpectPopupAndClick("reject"),
+ TEST_3RD_PARTY_PAGE,
+ requestStorageAccessAndExpectFailure
+ );
+
+ await cleanUpData();
+ await SpecialPowers.flushPrefEnv();
+});
+
+add_task(async function testSameSitePermission() {
+ await setPreferences();
+
+ await openPageAndRunCode(
+ TEST_TOP_PAGE_7,
+ getExpectPopupAndClick("accept"),
+ TEST_3RD_PARTY_PAGE,
+ requestStorageAccessAndExpectSuccess
+ );
+
+ await openPageAndRunCode(
+ TEST_TOP_PAGE_8,
+ expectNoPopup,
+ TEST_3RD_PARTY_PAGE,
+ requestStorageAccessAndExpectSuccess
+ );
+
+ await cleanUpData();
+ await SpecialPowers.flushPrefEnv();
+});
diff --git a/toolkit/components/antitracking/test/browser/browser_storageAccessScopeSameSiteWrite.js b/toolkit/components/antitracking/test/browser/browser_storageAccessScopeSameSiteWrite.js
new file mode 100644
index 0000000000..4b99cb266e
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_storageAccessScopeSameSiteWrite.js
@@ -0,0 +1,43 @@
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/browser/modules/test/browser/head.js",
+ this
+);
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/toolkit/components/antitracking/test/browser/storage_access_head.js",
+ this
+);
+
+add_task(async function testInitialBlock() {
+ await setPreferences();
+
+ await openPageAndRunCode(
+ TEST_TOP_PAGE_7,
+ getExpectPopupAndClick("reject"),
+ TEST_3RD_PARTY_PAGE,
+ requestStorageAccessAndExpectFailure
+ );
+
+ await cleanUpData();
+ await SpecialPowers.flushPrefEnv();
+});
+
+add_task(async function testSameSitePermissionReversed() {
+ await setPreferences();
+
+ await openPageAndRunCode(
+ TEST_TOP_PAGE_8,
+ getExpectPopupAndClick("accept"),
+ TEST_3RD_PARTY_PAGE,
+ requestStorageAccessAndExpectSuccess
+ );
+
+ await openPageAndRunCode(
+ TEST_TOP_PAGE_7,
+ expectNoPopup,
+ TEST_3RD_PARTY_PAGE,
+ requestStorageAccessAndExpectSuccess
+ );
+
+ await cleanUpData();
+ await SpecialPowers.flushPrefEnv();
+});
diff --git a/toolkit/components/antitracking/test/browser/browser_storageAccessThirdPartyChecks.js b/toolkit/components/antitracking/test/browser/browser_storageAccessThirdPartyChecks.js
new file mode 100644
index 0000000000..5a8c7970c5
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_storageAccessThirdPartyChecks.js
@@ -0,0 +1,157 @@
+const APS_PREF =
+ "privacy.partition.always_partition_third_party_non_cookie_storage";
+
+AntiTracking._createTask({
+ name: "Test that after a storage access grant we have full first-party access",
+ cookieBehavior: BEHAVIOR_REJECT_TRACKER,
+ blockingByContentBlockingRTUI: true,
+ allowList: false,
+ callback: async _ => {
+ /* import-globals-from storageAccessAPIHelpers.js */
+ await noStorageAccessInitially();
+
+ await callRequestStorageAccess();
+
+ const TRACKING_PAGE =
+ "http://another-tracking.example.net/browser/browser/base/content/test/protectionsUI/trackingPage.html";
+ async function runChecks(name) {
+ let iframe = document.createElement("iframe");
+ iframe.src = TRACKING_PAGE;
+ document.body.appendChild(iframe);
+ await new Promise(resolve => {
+ iframe.onload = resolve;
+ });
+
+ await SpecialPowers.spawn(iframe, [name], name => {
+ content.postMessage(name, "*");
+ });
+
+ await new Promise(resolve => {
+ onmessage = e => {
+ if (e.data == "done") {
+ resolve();
+ }
+ };
+ });
+ }
+
+ await runChecks("image");
+ },
+ extraPrefs: [[APS_PREF, false]],
+ expectedBlockingNotifications:
+ Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER,
+ runInPrivateWindow: false,
+ iframeSandbox: null,
+ accessRemoval: null,
+ callbackAfterRemoval: null,
+ thirdPartyPage: TEST_3RD_PARTY_PAGE_HTTP,
+ errorMessageDomains: [
+ "http://tracking.example.org",
+ "http://tracking.example.org",
+ "http://tracking.example.org",
+ ],
+});
+
+add_task(async _ => {
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+});
+
+AntiTracking._createTask({
+ name: "Test that we never grant access to cookieBehavior=1, when network.cookie.rejectForeignWithExceptions.enabled is set to false",
+ cookieBehavior: BEHAVIOR_REJECT_FOREIGN,
+ allowList: false,
+ callback: async _ => {
+ /* import-globals-from storageAccessAPIHelpers.js */
+ await noStorageAccessInitially();
+
+ await callRequestStorageAccess(null, true);
+ },
+ extraPrefs: [
+ ["network.cookie.rejectForeignWithExceptions.enabled", false],
+ [APS_PREF, false],
+ ],
+ expectedBlockingNotifications: 0,
+ runInPrivateWindow: false,
+ iframeSandbox: null,
+ accessRemoval: null,
+ callbackAfterRemoval: null,
+ thirdPartyPage: TEST_3RD_PARTY_PAGE_HTTP,
+ errorMessageDomains: [
+ "http://tracking.example.org",
+ "http://tracking.example.org",
+ ],
+});
+
+add_task(async _ => {
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+});
+
+AntiTracking._createTask({
+ name: "Test that we never grant access to cookieBehavior=2",
+ cookieBehavior: BEHAVIOR_REJECT,
+ allowList: false,
+ callback: async _ => {
+ /* import-globals-from storageAccessAPIHelpers.js */
+ await noStorageAccessInitially();
+
+ await callRequestStorageAccess(null, true);
+ },
+ extraPrefs: [[APS_PREF, false]],
+ expectedBlockingNotifications: 0,
+ runInPrivateWindow: false,
+ iframeSandbox: null,
+ accessRemoval: null,
+ callbackAfterRemoval: null,
+ thirdPartyPage: TEST_3RD_PARTY_PAGE_HTTP,
+ errorMessageDomains: [
+ "http://tracking.example.org",
+ "http://tracking.example.org",
+ ],
+});
+
+add_task(async _ => {
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+});
+
+AntiTracking._createTask({
+ name: "Test that we never grant access to cookieBehavior=3",
+ cookieBehavior: BEHAVIOR_LIMIT_FOREIGN,
+ allowList: false,
+ callback: async _ => {
+ /* import-globals-from storageAccessAPIHelpers.js */
+ await noStorageAccessInitially();
+
+ await callRequestStorageAccess(null, true);
+ },
+ extraPrefs: [[APS_PREF, false]],
+ expectedBlockingNotifications: 0,
+ runInPrivateWindow: false,
+ iframeSandbox: null,
+ accessRemoval: null,
+ callbackAfterRemoval: null,
+ thirdPartyPage: TEST_3RD_PARTY_PAGE_HTTP,
+ errorMessageDomains: [
+ "http://tracking.example.org",
+ "http://tracking.example.org",
+ ],
+});
+
+add_task(async _ => {
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+});
diff --git a/toolkit/components/antitracking/test/browser/browser_storageAccessThirdPartyChecks_alwaysPartition.js b/toolkit/components/antitracking/test/browser/browser_storageAccessThirdPartyChecks_alwaysPartition.js
new file mode 100644
index 0000000000..1ba0b89230
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_storageAccessThirdPartyChecks_alwaysPartition.js
@@ -0,0 +1,153 @@
+const allBlocked = Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_ALL;
+const foreignBlocked = Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_FOREIGN;
+
+const APS_PREF =
+ "privacy.partition.always_partition_third_party_non_cookie_storage";
+
+AntiTracking._createTask({
+ name: "Test that after a storage access grant we have full first-party access",
+ cookieBehavior: BEHAVIOR_REJECT_TRACKER,
+ blockingByContentBlockingRTUI: true,
+ allowList: false,
+ callback: async _ => {
+ /* import-globals-from storageAccessAPIHelpers.js */
+ await noStorageAccessInitially();
+
+ await callRequestStorageAccess();
+
+ const TRACKING_PAGE =
+ "http://another-tracking.example.net/browser/browser/base/content/test/protectionsUI/trackingPage.html";
+ async function runChecks(name) {
+ let iframe = document.createElement("iframe");
+ iframe.src = TRACKING_PAGE;
+ document.body.appendChild(iframe);
+ await new Promise(resolve => {
+ iframe.onload = resolve;
+ });
+
+ await SpecialPowers.spawn(iframe, [name], name => {
+ content.postMessage(name, "*");
+ });
+
+ await new Promise(resolve => {
+ onmessage = e => {
+ if (e.data == "done") {
+ resolve();
+ }
+ };
+ });
+ }
+
+ await runChecks("image");
+ },
+ extraPrefs: [[APS_PREF, true]],
+ expectedBlockingNotifications:
+ Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER,
+ runInPrivateWindow: false,
+ iframeSandbox: null,
+ accessRemoval: null,
+ callbackAfterRemoval: null,
+ thirdPartyPage: TEST_3RD_PARTY_PAGE_HTTP,
+ errorMessageDomains: [
+ "http://tracking.example.org",
+ "http://tracking.example.org",
+ "http://tracking.example.org",
+ "http://tracking.example.org",
+ "http://trackertest.org",
+ ],
+});
+
+add_task(async _ => {
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+});
+
+AntiTracking._createTask({
+ name: "Test that we never grant access to cookieBehavior=1, when network.cookie.rejectForeignWithExceptions.enabled is set to false",
+ cookieBehavior: BEHAVIOR_REJECT_FOREIGN,
+ allowList: false,
+ callback: async _ => {
+ /* import-globals-from storageAccessAPIHelpers.js */
+ await noStorageAccessInitially();
+
+ await callRequestStorageAccess(null, true);
+ },
+ extraPrefs: [
+ ["network.cookie.rejectForeignWithExceptions.enabled", false],
+ [APS_PREF, true],
+ ],
+ expectedBlockingNotifications: foreignBlocked,
+ runInPrivateWindow: false,
+ iframeSandbox: null,
+ accessRemoval: null,
+ callbackAfterRemoval: null,
+ thirdPartyPage: TEST_3RD_PARTY_PAGE_HTTP,
+ errorMessageDomains: ["http://tracking.example.org"],
+});
+
+add_task(async _ => {
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+});
+
+AntiTracking._createTask({
+ name: "Test that we never grant access to cookieBehavior=2",
+ cookieBehavior: BEHAVIOR_REJECT,
+ allowList: false,
+ callback: async _ => {
+ /* import-globals-from storageAccessAPIHelpers.js */
+ await noStorageAccessInitially();
+
+ await callRequestStorageAccess(null, true);
+ },
+ extraPrefs: [[APS_PREF, true]],
+ expectedBlockingNotifications: allBlocked,
+ runInPrivateWindow: false,
+ iframeSandbox: null,
+ accessRemoval: null,
+ callbackAfterRemoval: null,
+ thirdPartyPage: TEST_3RD_PARTY_PAGE_HTTP,
+ errorMessageDomains: ["http://example.net", "http://tracking.example.org"],
+});
+
+add_task(async _ => {
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+});
+
+AntiTracking._createTask({
+ name: "Test that we never grant access to cookieBehavior=3",
+ cookieBehavior: BEHAVIOR_LIMIT_FOREIGN,
+ allowList: false,
+ callback: async _ => {
+ /* import-globals-from storageAccessAPIHelpers.js */
+ await noStorageAccessInitially();
+
+ await callRequestStorageAccess(null, true);
+ },
+ extraPrefs: [[APS_PREF, true]],
+ expectedBlockingNotifications: foreignBlocked,
+ runInPrivateWindow: false,
+ iframeSandbox: null,
+ accessRemoval: null,
+ callbackAfterRemoval: null,
+ thirdPartyPage: TEST_3RD_PARTY_PAGE_HTTP,
+ errorMessageDomains: ["http://tracking.example.org"],
+});
+
+add_task(async _ => {
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+});
diff --git a/toolkit/components/antitracking/test/browser/browser_storageAccessWithDynamicFpi.js b/toolkit/components/antitracking/test/browser/browser_storageAccessWithDynamicFpi.js
new file mode 100644
index 0000000000..f50e515c84
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_storageAccessWithDynamicFpi.js
@@ -0,0 +1,612 @@
+/* vim: set ts=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/. */
+
+"use strict";
+
+const { RemoteSettings } = ChromeUtils.importESModule(
+ "resource://services-settings/remote-settings.sys.mjs"
+);
+
+XPCOMUtils.defineLazyServiceGetter(
+ this,
+ "peuService",
+ "@mozilla.org/partitioning/exception-list-service;1",
+ "nsIPartitioningExceptionListService"
+);
+
+const TEST_REDIRECT_TOP_PAGE =
+ TEST_3RD_PARTY_DOMAIN + TEST_PATH + "redirect.sjs?" + TEST_TOP_PAGE;
+const TEST_REDIRECT_3RD_PARTY_PAGE =
+ TEST_DOMAIN + TEST_PATH + "redirect.sjs?" + TEST_3RD_PARTY_PARTITIONED_PAGE;
+
+const COLLECTION_NAME = "partitioning-exempt-urls";
+const EXCEPTION_LIST_PREF_NAME = "privacy.restrict3rdpartystorage.skip_list";
+
+async function cleanup() {
+ Services.prefs.clearUserPref(EXCEPTION_LIST_PREF_NAME);
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+}
+
+add_setup(async function () {
+ await SpecialPowers.flushPrefEnv();
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [
+ "network.cookie.cookieBehavior",
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN,
+ ],
+ [
+ "network.cookie.cookieBehavior.pbmode",
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN,
+ ],
+ ["privacy.restrict3rdpartystorage.heuristic.redirect", false],
+ ["privacy.trackingprotection.enabled", false],
+ ["privacy.trackingprotection.pbmode.enabled", false],
+ ["privacy.trackingprotection.annotate_channels", true],
+ // Bug 1617611: Fix all the tests broken by "cookies SameSite=lax by default"
+ ["network.cookie.sameSite.laxByDefault", false],
+ ],
+ });
+ registerCleanupFunction(cleanup);
+});
+
+function executeContentScript(browser, callback, options = {}) {
+ return SpecialPowers.spawn(
+ browser,
+ [
+ {
+ callback: callback.toString(),
+ ...options,
+ },
+ ],
+ obj => {
+ return new content.Promise(async resolve => {
+ if (obj.page) {
+ // third-party
+ let ifr = content.document.createElement("iframe");
+ ifr.onload = async () => {
+ info("Sending code to the 3rd party content");
+ ifr.contentWindow.postMessage(
+ { cb: obj.callback, value: obj.value },
+ "*"
+ );
+ };
+
+ content.addEventListener("message", event => resolve(event.data), {
+ once: true,
+ });
+
+ content.document.body.appendChild(ifr);
+ ifr.src = obj.page;
+ } else {
+ // first-party
+ let runnableStr = `(() => {return (${obj.callback});})();`;
+ let runnable = eval(runnableStr); // eslint-disable-line no-eval
+ resolve(await runnable.call(content, content, obj.value));
+ }
+ });
+ }
+ );
+}
+
+function readNetworkCookie(win) {
+ return win
+ .fetch("cookies.sjs")
+ .then(r => r.text())
+ .then(text => {
+ return text.substring("cookie:foopy=".length);
+ });
+}
+
+async function writeNetworkCookie(win, value) {
+ await win.fetch("cookies.sjs?" + value).then(r => r.text());
+ return true;
+}
+
+function createDataInFirstParty(browser, value) {
+ return executeContentScript(browser, writeNetworkCookie, { value });
+}
+function getDataFromFirstParty(browser) {
+ return executeContentScript(browser, readNetworkCookie, {});
+}
+function createDataInThirdParty(browser, value) {
+ return executeContentScript(browser, writeNetworkCookie, {
+ page: TEST_3RD_PARTY_PARTITIONED_PAGE,
+ value,
+ });
+}
+function getDataFromThirdParty(browser) {
+ return executeContentScript(browser, readNetworkCookie, {
+ page: TEST_3RD_PARTY_PARTITIONED_PAGE,
+ });
+}
+
+async function redirectWithUserInteraction(browser, url, wait = null) {
+ await executeContentScript(
+ browser,
+ (content, value) => {
+ content.document.userInteractionForTesting();
+
+ let link = content.document.createElement("a");
+ link.appendChild(content.document.createTextNode("click me!"));
+ link.href = value;
+ content.document.body.appendChild(link);
+ link.click();
+ },
+ {
+ value: url,
+ }
+ );
+ await BrowserTestUtils.browserLoaded(browser, false, wait || url);
+}
+
+async function checkData(browser, options) {
+ if ("firstParty" in options) {
+ is(
+ await getDataFromFirstParty(browser),
+ options.firstParty,
+ "correct first-party data"
+ );
+ }
+ if ("thirdParty" in options) {
+ is(
+ await getDataFromThirdParty(browser),
+ options.thirdParty,
+ "correct third-party data"
+ );
+ }
+}
+
+async function runTestRedirectHeuristic(disableHeuristics) {
+ info("Starting Dynamic FPI Redirect Heuristic test...");
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["privacy.restrict3rdpartystorage.heuristic.recently_visited", true],
+ ["privacy.antitracking.enableWebcompat", !disableHeuristics],
+ ],
+ });
+
+ // mark third-party as tracker
+ await UrlClassifierTestUtils.addTestTrackers();
+
+ info("Creating a new tab");
+ let tab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE);
+ gBrowser.selectedTab = tab;
+
+ let browser = gBrowser.getBrowserForTab(tab);
+ await BrowserTestUtils.browserLoaded(browser);
+
+ info("initializing...");
+ await checkData(browser, { firstParty: "", thirdParty: "" });
+
+ await Promise.all([
+ createDataInFirstParty(browser, "firstParty"),
+ createDataInThirdParty(browser, "thirdParty"),
+ ]);
+
+ await checkData(browser, {
+ firstParty: "firstParty",
+ thirdParty: "",
+ });
+
+ info("load third-party content as first-party");
+ await redirectWithUserInteraction(
+ browser,
+ TEST_REDIRECT_3RD_PARTY_PAGE,
+ TEST_3RD_PARTY_PARTITIONED_PAGE
+ );
+
+ await checkData(browser, { firstParty: "" });
+ await createDataInFirstParty(browser, "heuristicFirstParty");
+ await checkData(browser, { firstParty: "heuristicFirstParty" });
+
+ info("redirect back to first-party page");
+ await redirectWithUserInteraction(
+ browser,
+ TEST_REDIRECT_TOP_PAGE,
+ TEST_TOP_PAGE
+ );
+
+ info("third-party tracker should NOT able to access first-party data");
+ await checkData(browser, {
+ firstParty: "firstParty",
+ thirdParty: "",
+ });
+
+ // remove third-party from tracker
+ await UrlClassifierTestUtils.cleanupTestTrackers();
+
+ info("load third-party content as first-party");
+ await redirectWithUserInteraction(
+ browser,
+ TEST_REDIRECT_3RD_PARTY_PAGE,
+ TEST_3RD_PARTY_PARTITIONED_PAGE
+ );
+
+ await checkData(browser, {
+ firstParty: "heuristicFirstParty",
+ });
+
+ info("redirect back to first-party page");
+ await redirectWithUserInteraction(
+ browser,
+ TEST_REDIRECT_TOP_PAGE,
+ TEST_TOP_PAGE
+ );
+
+ info(
+ `third-party page should ${
+ disableHeuristics ? "not " : ""
+ }be able to access first-party data`
+ );
+ await checkData(browser, {
+ firstParty: "firstParty",
+ thirdParty: disableHeuristics ? "" : "heuristicFirstParty",
+ });
+
+ info("Removing the tab");
+ BrowserTestUtils.removeTab(tab);
+
+ await SpecialPowers.popPrefEnv();
+
+ await cleanup();
+}
+
+add_task(async function testRedirectHeuristic() {
+ await runTestRedirectHeuristic(false);
+});
+
+add_task(async function testRedirectHeuristicDisabled() {
+ await runTestRedirectHeuristic(true);
+});
+
+class UpdateEvent extends EventTarget {}
+function waitForEvent(element, eventName) {
+ return new Promise(function (resolve) {
+ element.addEventListener(eventName, e => resolve(e.detail), { once: true });
+ });
+}
+
+// The test URLs have a trailing / which means they're not valid origins.
+const TEST_ORIGIN = TEST_DOMAIN.substring(0, TEST_DOMAIN.length - 1);
+const TEST_3RD_PARTY_ORIGIN = TEST_3RD_PARTY_DOMAIN.substring(
+ 0,
+ TEST_3RD_PARTY_DOMAIN.length - 1
+);
+
+async function runTestExceptionListPref(disableHeuristics) {
+ info("Starting Dynamic FPI exception list test pref");
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["privacy.restrict3rdpartystorage.heuristic.recently_visited", false],
+ ["privacy.antitracking.enableWebcompat", !disableHeuristics],
+ ],
+ });
+
+ info("Creating new tabs");
+ let tabThirdParty = BrowserTestUtils.addTab(
+ gBrowser,
+ TEST_3RD_PARTY_PARTITIONED_PAGE
+ );
+ gBrowser.selectedTab = tabThirdParty;
+
+ let browserThirdParty = gBrowser.getBrowserForTab(tabThirdParty);
+ await BrowserTestUtils.browserLoaded(browserThirdParty);
+
+ let tabFirstParty = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE);
+ gBrowser.selectedTab = tabFirstParty;
+
+ let browserFirstParty = gBrowser.getBrowserForTab(tabFirstParty);
+ await BrowserTestUtils.browserLoaded(browserFirstParty);
+
+ info("initializing...");
+ await Promise.all([
+ checkData(browserFirstParty, { firstParty: "", thirdParty: "" }),
+ checkData(browserThirdParty, { firstParty: "" }),
+ ]);
+
+ info("fill default data");
+ await Promise.all([
+ createDataInFirstParty(browserFirstParty, "firstParty"),
+ createDataInThirdParty(browserFirstParty, "thirdParty"),
+ createDataInFirstParty(browserThirdParty, "ExceptionListFirstParty"),
+ ]);
+
+ info("check data");
+ await Promise.all([
+ checkData(browserFirstParty, {
+ firstParty: "firstParty",
+ thirdParty: "thirdParty",
+ }),
+ checkData(browserThirdParty, { firstParty: "ExceptionListFirstParty" }),
+ ]);
+
+ info("set exception list pref");
+ Services.prefs.setStringPref(
+ EXCEPTION_LIST_PREF_NAME,
+ `${TEST_ORIGIN},${TEST_3RD_PARTY_ORIGIN}`
+ );
+
+ info("check data");
+ await Promise.all([
+ checkData(browserFirstParty, {
+ firstParty: "firstParty",
+ thirdParty: disableHeuristics ? "thirdParty" : "ExceptionListFirstParty",
+ }),
+ checkData(browserThirdParty, { firstParty: "ExceptionListFirstParty" }),
+ ]);
+
+ info("set incomplete exception list pref");
+ Services.prefs.setStringPref(EXCEPTION_LIST_PREF_NAME, `${TEST_ORIGIN}`);
+
+ info("check data");
+ await Promise.all([
+ checkData(browserFirstParty, {
+ firstParty: "firstParty",
+ thirdParty: "thirdParty",
+ }),
+ checkData(browserThirdParty, { firstParty: "ExceptionListFirstParty" }),
+ ]);
+
+ info("set exception list pref, with extra semicolons");
+ Services.prefs.setStringPref(
+ EXCEPTION_LIST_PREF_NAME,
+ `;${TEST_ORIGIN},${TEST_3RD_PARTY_ORIGIN};;`
+ );
+
+ info("check data");
+ await Promise.all([
+ checkData(browserFirstParty, {
+ firstParty: "firstParty",
+ thirdParty: disableHeuristics ? "thirdParty" : "ExceptionListFirstParty",
+ }),
+ checkData(browserThirdParty, { firstParty: "ExceptionListFirstParty" }),
+ ]);
+
+ info("set exception list pref, with subdomain wildcard");
+ Services.prefs.setStringPref(
+ EXCEPTION_LIST_PREF_NAME,
+ `${TEST_ORIGIN},${TEST_3RD_PARTY_ORIGIN.replace("tracking", "*")}`
+ );
+
+ info("check data");
+ await Promise.all([
+ checkData(browserFirstParty, {
+ firstParty: "firstParty",
+ thirdParty: disableHeuristics ? "thirdParty" : "ExceptionListFirstParty",
+ }),
+ checkData(browserThirdParty, { firstParty: "ExceptionListFirstParty" }),
+ ]);
+
+ info("Removing the tab");
+ BrowserTestUtils.removeTab(tabFirstParty);
+ BrowserTestUtils.removeTab(tabThirdParty);
+
+ await SpecialPowers.popPrefEnv();
+
+ await cleanup();
+}
+
+add_task(async function testExceptionListPref() {
+ await runTestExceptionListPref(false);
+});
+
+add_task(async function testExceptionListPrefDisabled() {
+ await runTestExceptionListPref(true);
+});
+
+add_task(async function testExceptionListRemoteSettings() {
+ info("Starting Dynamic FPI exception list test (remote settings)");
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["privacy.restrict3rdpartystorage.heuristic.recently_visited", false],
+ ],
+ });
+
+ // Make sure we have a pref initially, since the exception list service
+ // requires it.
+ Services.prefs.setStringPref(EXCEPTION_LIST_PREF_NAME, "");
+
+ // Add some initial data
+ let db = RemoteSettings(COLLECTION_NAME).db;
+ await db.importChanges({}, Date.now(), []);
+
+ // make peuSerivce start working by calling
+ // registerAndRunExceptionListObserver
+ let updateEvent = new UpdateEvent();
+ let obs = data => {
+ let event = new CustomEvent("update", { detail: data });
+ updateEvent.dispatchEvent(event);
+ };
+ let promise = waitForEvent(updateEvent, "update");
+ peuService.registerAndRunExceptionListObserver(obs);
+ await promise;
+
+ info("Creating new tabs");
+ let tabThirdParty = BrowserTestUtils.addTab(
+ gBrowser,
+ TEST_3RD_PARTY_PARTITIONED_PAGE
+ );
+ gBrowser.selectedTab = tabThirdParty;
+
+ let browserThirdParty = gBrowser.getBrowserForTab(tabThirdParty);
+ await BrowserTestUtils.browserLoaded(browserThirdParty);
+
+ let tabFirstParty = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE);
+ gBrowser.selectedTab = tabFirstParty;
+
+ let browserFirstParty = gBrowser.getBrowserForTab(tabFirstParty);
+ await BrowserTestUtils.browserLoaded(browserFirstParty);
+
+ info("initializing...");
+ await Promise.all([
+ checkData(browserFirstParty, { firstParty: "", thirdParty: "" }),
+ checkData(browserThirdParty, { firstParty: "" }),
+ ]);
+
+ info("fill default data");
+ await Promise.all([
+ createDataInFirstParty(browserFirstParty, "firstParty"),
+ createDataInThirdParty(browserFirstParty, "thirdParty"),
+ createDataInFirstParty(browserThirdParty, "ExceptionListFirstParty"),
+ ]);
+
+ info("check data");
+ await Promise.all([
+ checkData(browserFirstParty, {
+ firstParty: "firstParty",
+ thirdParty: "thirdParty",
+ }),
+ checkData(browserThirdParty, { firstParty: "ExceptionListFirstParty" }),
+ ]);
+
+ info("set exception list remote settings");
+
+ // set records
+ promise = waitForEvent(updateEvent, "update");
+ await RemoteSettings(COLLECTION_NAME).emit("sync", {
+ data: {
+ current: [
+ {
+ id: "1",
+ last_modified: 1000000000000001,
+ firstPartyOrigin: TEST_ORIGIN,
+ thirdPartyOrigin: TEST_3RD_PARTY_ORIGIN,
+ },
+ ],
+ },
+ });
+
+ let list = await promise;
+ is(
+ list,
+ `${TEST_ORIGIN},${TEST_3RD_PARTY_ORIGIN}`,
+ "exception list is correctly set"
+ );
+
+ info("check data");
+ await Promise.all([
+ checkData(browserFirstParty, {
+ firstParty: "firstParty",
+ thirdParty: "ExceptionListFirstParty",
+ }),
+ checkData(browserThirdParty, { firstParty: "ExceptionListFirstParty" }),
+ ]);
+
+ info("Removing the tab");
+ BrowserTestUtils.removeTab(tabFirstParty);
+ BrowserTestUtils.removeTab(tabThirdParty);
+
+ promise = waitForEvent(updateEvent, "update");
+ await RemoteSettings(COLLECTION_NAME).emit("sync", {
+ data: {
+ current: [],
+ },
+ });
+ is(await promise, "", "Exception list is cleared");
+
+ peuService.unregisterExceptionListObserver(obs);
+ await cleanup();
+});
+
+add_task(async function testWildcardExceptionListPref() {
+ info("Starting Dynamic FPI wirdcard exception list test pref");
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["privacy.restrict3rdpartystorage.heuristic.recently_visited", false],
+ ],
+ });
+
+ info("Creating new tabs");
+ let tabThirdParty = BrowserTestUtils.addTab(
+ gBrowser,
+ TEST_3RD_PARTY_PARTITIONED_PAGE
+ );
+ gBrowser.selectedTab = tabThirdParty;
+
+ let browserThirdParty = gBrowser.getBrowserForTab(tabThirdParty);
+ await BrowserTestUtils.browserLoaded(browserThirdParty);
+
+ let tabFirstParty = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE);
+ gBrowser.selectedTab = tabFirstParty;
+
+ let browserFirstParty = gBrowser.getBrowserForTab(tabFirstParty);
+ await BrowserTestUtils.browserLoaded(browserFirstParty);
+
+ info("initializing...");
+ await Promise.all([
+ checkData(browserFirstParty, { firstParty: "", thirdParty: "" }),
+ checkData(browserThirdParty, { firstParty: "" }),
+ ]);
+
+ info("fill default data");
+ await Promise.all([
+ createDataInFirstParty(browserFirstParty, "firstParty"),
+ createDataInThirdParty(browserFirstParty, "thirdParty"),
+ createDataInFirstParty(browserThirdParty, "ExceptionListFirstParty"),
+ ]);
+
+ info("check initial data");
+ await Promise.all([
+ checkData(browserFirstParty, {
+ firstParty: "firstParty",
+ thirdParty: "thirdParty",
+ }),
+ checkData(browserThirdParty, { firstParty: "ExceptionListFirstParty" }),
+ ]);
+
+ info("set wildcard (1st-party) pref");
+ Services.prefs.setStringPref(
+ EXCEPTION_LIST_PREF_NAME,
+ `*,${TEST_3RD_PARTY_ORIGIN}`
+ );
+
+ info("check wildcard (1st-party) data");
+ await Promise.all([
+ checkData(browserFirstParty, {
+ firstParty: "firstParty",
+ thirdParty: "ExceptionListFirstParty",
+ }),
+ checkData(browserThirdParty, { firstParty: "ExceptionListFirstParty" }),
+ ]);
+
+ info("set invalid exception list pref");
+ Services.prefs.setStringPref(EXCEPTION_LIST_PREF_NAME, "*,*");
+
+ info("check initial data");
+ await Promise.all([
+ checkData(browserFirstParty, {
+ firstParty: "firstParty",
+ thirdParty: "thirdParty",
+ }),
+ checkData(browserThirdParty, { firstParty: "ExceptionListFirstParty" }),
+ ]);
+
+ info("set wildcard (3rd-party) pref");
+ Services.prefs.setStringPref(EXCEPTION_LIST_PREF_NAME, `${TEST_ORIGIN},*`);
+
+ info("check wildcard (3rd-party) data");
+ await Promise.all([
+ checkData(browserFirstParty, {
+ firstParty: "firstParty",
+ thirdParty: "ExceptionListFirstParty",
+ }),
+ checkData(browserThirdParty, { firstParty: "ExceptionListFirstParty" }),
+ ]);
+
+ info("Removing the tab");
+ BrowserTestUtils.removeTab(tabFirstParty);
+ BrowserTestUtils.removeTab(tabThirdParty);
+
+ await cleanup();
+});
diff --git a/toolkit/components/antitracking/test/browser/browser_storageAccessWithHeuristics.js b/toolkit/components/antitracking/test/browser/browser_storageAccessWithHeuristics.js
new file mode 100644
index 0000000000..5b68975d03
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_storageAccessWithHeuristics.js
@@ -0,0 +1,912 @@
+function waitStoragePermission() {
+ return new Promise(resolve => {
+ let id = setInterval(async _ => {
+ if (
+ await SpecialPowers.testPermission(
+ `3rdPartyStorage^${TEST_3RD_PARTY_DOMAIN.slice(0, -1)}`,
+ SpecialPowers.Services.perms.ALLOW_ACTION,
+ TEST_DOMAIN
+ )
+ ) {
+ clearInterval(id);
+ resolve();
+ }
+ }, 0);
+ });
+}
+
+add_setup(async function () {
+ info("Starting subResources test");
+
+ await SpecialPowers.flushPrefEnv();
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.storage_access.enabled", true],
+ [
+ "network.cookie.cookieBehavior",
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER,
+ ],
+ [
+ "network.cookie.cookieBehavior.pbmode",
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER,
+ ],
+ ["privacy.trackingprotection.enabled", false],
+ ["privacy.trackingprotection.pbmode.enabled", false],
+ ["privacy.trackingprotection.annotate_channels", true],
+ [
+ "privacy.restrict3rdpartystorage.userInteractionRequiredForHosts",
+ "tracking.example.com,tracking.example.org",
+ ],
+ ],
+ });
+
+ await UrlClassifierTestUtils.addTestTrackers();
+});
+
+async function runTestWindowOpenHeuristic(disableHeuristics) {
+ info(
+ `Starting window.open() heuristic test with heuristic ${
+ disableHeuristics ? "disabled" : "enabled"
+ }.`
+ );
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["privacy.antitracking.enableWebcompat", !disableHeuristics]],
+ });
+
+ info("Creating a new tab");
+ let tab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE);
+ gBrowser.selectedTab = tab;
+
+ let browser = gBrowser.getBrowserForTab(tab);
+ await BrowserTestUtils.browserLoaded(browser);
+
+ info("Loading tracking scripts");
+ await SpecialPowers.spawn(
+ browser,
+ [
+ {
+ page: TEST_3RD_PARTY_PAGE_WO,
+ disableHeuristics,
+ },
+ ],
+ async obj => {
+ let msg = {};
+ msg.blockingCallback = (async _ => {
+ /* import-globals-from storageAccessAPIHelpers.js */
+ await noStorageAccessInitially();
+ }).toString();
+
+ // If the heuristic is disabled, we won't get storage access.
+ if (obj.disableHeuristics) {
+ msg.nonBlockingCallback = (async _ => {
+ /* import-globals-from storageAccessAPIHelpers.js */
+ await stillNoStorageAccess();
+ }).toString();
+ } else {
+ msg.nonBlockingCallback = (async _ => {
+ /* import-globals-from storageAccessAPIHelpers.js */
+ await hasStorageAccessInitially();
+ }).toString();
+ }
+
+ info("Checking if storage access is denied");
+ await new content.Promise(resolve => {
+ let ifr = content.document.createElement("iframe");
+ ifr.onload = function () {
+ info("Sending code to the 3rd party content");
+ ifr.contentWindow.postMessage(msg, "*");
+ };
+
+ content.addEventListener("message", function msg(event) {
+ if (event.data.type == "finish") {
+ content.removeEventListener("message", msg);
+ resolve();
+ return;
+ }
+
+ if (event.data.type == "ok") {
+ ok(event.data.what, event.data.msg);
+ return;
+ }
+
+ if (event.data.type == "info") {
+ info(event.data.msg);
+ return;
+ }
+
+ ok(false, "Unknown message");
+ });
+
+ content.document.body.appendChild(ifr);
+ ifr.src = obj.page;
+ });
+ }
+ );
+
+ info("Removing the tab");
+ BrowserTestUtils.removeTab(tab);
+
+ await SpecialPowers.popPrefEnv();
+
+ info("Cleaning up.");
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+}
+
+add_task(async function testWindowOpenHeuristic() {
+ await runTestWindowOpenHeuristic(false);
+});
+
+add_task(async function testWindowOpenHeuristicDisabled() {
+ await runTestWindowOpenHeuristic(true);
+});
+
+add_task(async function testDoublyNestedWindowOpenHeuristic() {
+ info("Starting doubly nested window.open() heuristic test...");
+
+ info("Creating a new tab");
+ let tab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE);
+ gBrowser.selectedTab = tab;
+
+ let browser = gBrowser.getBrowserForTab(tab);
+ await BrowserTestUtils.browserLoaded(browser);
+
+ info("Loading tracking scripts");
+ await SpecialPowers.spawn(
+ browser,
+ [
+ {
+ page: TEST_3RD_PARTY_PAGE_RELAY + "?" + TEST_3RD_PARTY_PAGE_WO,
+ },
+ ],
+ async obj => {
+ let msg = {};
+ msg.blockingCallback = (async _ => {
+ /* import-globals-from storageAccessAPIHelpers.js */
+ await noStorageAccessInitially();
+ }).toString();
+
+ msg.nonBlockingCallback = (async _ => {
+ /* import-globals-from storageAccessAPIHelpers.js */
+ await hasStorageAccessInitially();
+ }).toString();
+
+ info("Checking if storage access is denied");
+ await new content.Promise(resolve => {
+ let ifr = content.document.createElement("iframe");
+ ifr.onload = function () {
+ info("Sending code to the 3rd party content");
+ ifr.contentWindow.postMessage(msg, "*");
+ };
+
+ content.addEventListener("message", function msg(event) {
+ if (event.data.type == "finish") {
+ content.removeEventListener("message", msg);
+ resolve();
+ return;
+ }
+
+ if (event.data.type == "ok") {
+ ok(event.data.what, event.data.msg);
+ return;
+ }
+
+ if (event.data.type == "info") {
+ info(event.data.msg);
+ return;
+ }
+
+ ok(false, "Unknown message");
+ });
+
+ content.document.body.appendChild(ifr);
+ ifr.src = obj.page;
+ });
+ }
+ );
+
+ info("Removing the tab");
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function () {
+ info("Cleaning up.");
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+});
+
+async function runTestUserInteractionHeuristic(disableHeuristics) {
+ info(
+ `Starting user interaction heuristic test with heuristic ${
+ disableHeuristics ? "disabled" : "enabled"
+ }.`
+ );
+ await SpecialPowers.pushPrefEnv({
+ set: [["privacy.antitracking.enableWebcompat", !disableHeuristics]],
+ });
+
+ info("Creating a new tab");
+ let tab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE);
+ gBrowser.selectedTab = tab;
+
+ let browser = gBrowser.getBrowserForTab(tab);
+ await BrowserTestUtils.browserLoaded(browser);
+
+ info("Loading tracking scripts");
+ await SpecialPowers.spawn(
+ browser,
+ [
+ {
+ page: TEST_3RD_PARTY_PAGE_UI,
+ popup: TEST_POPUP_PAGE,
+ },
+ ],
+ async obj => {
+ let msg = {};
+ msg.blockingCallback = (async _ => {
+ /* import-globals-from storageAccessAPIHelpers.js */
+ await noStorageAccessInitially();
+ }).toString();
+
+ info("Checking if storage access is denied");
+
+ let ifr = content.document.createElement("iframe");
+ let loading = new content.Promise(resolve => {
+ ifr.onload = resolve;
+ });
+ content.document.body.appendChild(ifr);
+ ifr.src = obj.page;
+ await loading;
+
+ info(
+ "The 3rd party content should not have access to first party storage."
+ );
+ await new content.Promise(resolve => {
+ content.addEventListener("message", function msg(event) {
+ if (event.data.type == "finish") {
+ content.removeEventListener("message", msg);
+ resolve();
+ return;
+ }
+
+ if (event.data.type == "ok") {
+ ok(event.data.what, event.data.msg);
+ return;
+ }
+
+ if (event.data.type == "info") {
+ info(event.data.msg);
+ return;
+ }
+
+ ok(false, "Unknown message");
+ });
+ ifr.contentWindow.postMessage({ callback: msg.blockingCallback }, "*");
+ });
+
+ info("Opening a window from the iframe.");
+ await SpecialPowers.spawn(ifr, [obj.popup], async popup => {
+ let windowClosed = new content.Promise(resolve => {
+ Services.ww.registerNotification(function notification(
+ aSubject,
+ aTopic,
+ aData
+ ) {
+ // We need to check the document URI for Fission. It's because the
+ // 'domwindowclosed' would be triggered twice, one for the
+ // 'about:blank' page and another for the tracker page.
+ if (
+ aTopic == "domwindowclosed" &&
+ aSubject.document.documentURI ==
+ "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/3rdPartyOpenUI.html"
+ ) {
+ Services.ww.unregisterNotification(notification);
+ resolve();
+ }
+ });
+ });
+
+ content.open(popup);
+
+ info("Let's wait for the window to be closed");
+ await windowClosed;
+ });
+
+ info("The 3rd party content should have access to first party storage.");
+ await new content.Promise(resolve => {
+ content.addEventListener("message", function msg(event) {
+ if (event.data.type == "finish") {
+ content.removeEventListener("message", msg);
+ resolve();
+ return;
+ }
+
+ if (event.data.type == "ok") {
+ ok(event.data.what, event.data.msg);
+ return;
+ }
+
+ if (event.data.type == "info") {
+ info(event.data.msg);
+ return;
+ }
+
+ ok(false, "Unknown message");
+ });
+ ifr.contentWindow.postMessage({ callback: msg.blockingCallback }, "*");
+ });
+ }
+ );
+
+ await AntiTracking.interactWithTracker();
+
+ info("Loading tracking scripts");
+ await SpecialPowers.spawn(
+ browser,
+ [
+ {
+ page: TEST_3RD_PARTY_PAGE_UI,
+ popup: TEST_POPUP_PAGE,
+ disableHeuristics,
+ },
+ ],
+ async obj => {
+ let msg = {};
+
+ msg.blockingCallback = (async _ => {
+ await noStorageAccessInitially();
+ }).toString();
+
+ // If the heuristic is disabled, we won't get storage access.
+ if (obj.disableHeuristics) {
+ msg.nonBlockingCallback = (async _ => {
+ /* import-globals-from storageAccessAPIHelpers.js */
+ await stillNoStorageAccess();
+ }).toString();
+ } else {
+ msg.nonBlockingCallback = (async _ => {
+ /* import-globals-from storageAccessAPIHelpers.js */
+ await hasStorageAccessInitially();
+ }).toString();
+ }
+
+ info("Checking if storage access is denied");
+
+ let ifr = content.document.createElement("iframe");
+ let loading = new content.Promise(resolve => {
+ ifr.onload = resolve;
+ });
+ content.document.body.appendChild(ifr);
+ ifr.src = obj.page;
+ await loading;
+
+ info(
+ "The 3rd party content should not have access to first party storage."
+ );
+ await new content.Promise(resolve => {
+ content.addEventListener("message", function msg(event) {
+ if (event.data.type == "finish") {
+ content.removeEventListener("message", msg);
+ resolve();
+ return;
+ }
+
+ if (event.data.type == "ok") {
+ ok(event.data.what, event.data.msg);
+ return;
+ }
+
+ if (event.data.type == "info") {
+ info(event.data.msg);
+ return;
+ }
+
+ ok(false, "Unknown message");
+ });
+ ifr.contentWindow.postMessage({ callback: msg.blockingCallback }, "*");
+ });
+
+ info("Opening a window from the iframe.");
+ await SpecialPowers.spawn(ifr, [obj.popup], async popup => {
+ let windowClosed = new content.Promise(resolve => {
+ Services.ww.registerNotification(function notification(
+ aSubject,
+ aTopic,
+ aData
+ ) {
+ // We need to check the document URI here as well for the same
+ // reason above.
+ if (
+ aTopic == "domwindowclosed" &&
+ aSubject.document.documentURI ==
+ "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/3rdPartyOpenUI.html"
+ ) {
+ Services.ww.unregisterNotification(notification);
+ resolve();
+ }
+ });
+ });
+
+ content.open(popup);
+
+ info("Let's wait for the window to be closed");
+ await windowClosed;
+ });
+
+ info("The 3rd party content should have access to first party storage.");
+ await new content.Promise(resolve => {
+ content.addEventListener("message", function msg(event) {
+ if (event.data.type == "finish") {
+ content.removeEventListener("message", msg);
+ resolve();
+ return;
+ }
+
+ if (event.data.type == "ok") {
+ ok(event.data.what, event.data.msg);
+ return;
+ }
+
+ if (event.data.type == "info") {
+ info(event.data.msg);
+ return;
+ }
+
+ ok(false, "Unknown message");
+ });
+ ifr.contentWindow.postMessage(
+ { callback: msg.nonBlockingCallback },
+ "*"
+ );
+ });
+ }
+ );
+
+ info("Removing the tab");
+ BrowserTestUtils.removeTab(tab);
+
+ if (!disableHeuristics) {
+ info("Wait until the storage permission is ready before cleaning up.");
+ await waitStoragePermission();
+ }
+
+ info("Cleaning up.");
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+
+ await SpecialPowers.popPrefEnv();
+}
+
+add_task(async function testUserInteractionHeuristic() {
+ await runTestUserInteractionHeuristic(false);
+});
+
+add_task(async function testUserInteractionHeuristicDisabled() {
+ await runTestUserInteractionHeuristic(true);
+});
+
+add_task(async function testDoublyNestedUserInteractionHeuristic() {
+ info("Starting doubly nested user interaction heuristic test...");
+
+ info("Creating a new tab");
+ let tab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE);
+ gBrowser.selectedTab = tab;
+
+ let browser = gBrowser.getBrowserForTab(tab);
+ await BrowserTestUtils.browserLoaded(browser);
+
+ info("Loading tracking scripts");
+ await SpecialPowers.spawn(
+ browser,
+ [
+ {
+ page: TEST_3RD_PARTY_PAGE_RELAY + "?" + TEST_3RD_PARTY_PAGE_UI,
+ popup: TEST_POPUP_PAGE,
+ },
+ ],
+ async obj => {
+ let msg = {};
+ msg.blockingCallback = (async _ => {
+ /* import-globals-from storageAccessAPIHelpers.js */
+ await noStorageAccessInitially();
+ }).toString();
+
+ msg.openWindowCallback = (async url => {
+ open(url);
+ }).toString();
+
+ info("Checking if storage access is denied");
+
+ let ifr = content.document.createElement("iframe");
+ let loading = new content.Promise(resolve => {
+ ifr.onload = resolve;
+ });
+ content.document.body.appendChild(ifr);
+ ifr.src = obj.page;
+ await loading;
+
+ info(
+ "The 3rd party content should not have access to first party storage."
+ );
+ await new content.Promise(resolve => {
+ content.addEventListener("message", function msg(event) {
+ if (event.data.type == "finish") {
+ content.removeEventListener("message", msg);
+ resolve();
+ return;
+ }
+
+ if (event.data.type == "ok") {
+ ok(event.data.what, event.data.msg);
+ return;
+ }
+
+ if (event.data.type == "info") {
+ info(event.data.msg);
+ return;
+ }
+
+ ok(false, "Unknown message");
+ });
+ ifr.contentWindow.postMessage({ callback: msg.blockingCallback }, "*");
+ });
+
+ let windowClosed = new content.Promise(resolve => {
+ Services.ww.registerNotification(function notification(
+ aSubject,
+ aTopic,
+ aData
+ ) {
+ if (aTopic == "domwindowclosed") {
+ Services.ww.unregisterNotification(notification);
+ resolve();
+ }
+ });
+ });
+
+ info("Opening a window from the iframe.");
+ ifr.contentWindow.postMessage(
+ { callback: msg.openWindowCallback, arg: obj.popup },
+ "*"
+ );
+
+ info("Let's wait for the window to be closed");
+ await windowClosed;
+
+ info("The 3rd party content should have access to first party storage.");
+ await new content.Promise(resolve => {
+ content.addEventListener("message", function msg(event) {
+ if (event.data.type == "finish") {
+ content.removeEventListener("message", msg);
+ resolve();
+ return;
+ }
+
+ if (event.data.type == "ok") {
+ ok(event.data.what, event.data.msg);
+ return;
+ }
+
+ if (event.data.type == "info") {
+ info(event.data.msg);
+ return;
+ }
+
+ ok(false, "Unknown message");
+ });
+ ifr.contentWindow.postMessage({ callback: msg.blockingCallback }, "*");
+ });
+ }
+ );
+
+ await AntiTracking.interactWithTracker();
+
+ info("Loading tracking scripts");
+ await SpecialPowers.spawn(
+ browser,
+ [
+ {
+ page: TEST_3RD_PARTY_PAGE_RELAY + "?" + TEST_3RD_PARTY_PAGE_UI,
+ popup: TEST_POPUP_PAGE,
+ },
+ ],
+ async obj => {
+ let msg = {};
+ msg.blockingCallback = (async _ => {
+ await noStorageAccessInitially();
+ }).toString();
+
+ msg.nonBlockingCallback = (async _ => {
+ /* import-globals-from storageAccessAPIHelpers.js */
+ await hasStorageAccessInitially();
+ }).toString();
+
+ msg.openWindowCallback = (async url => {
+ open(url);
+ }).toString();
+
+ info("Checking if storage access is denied");
+
+ let ifr = content.document.createElement("iframe");
+ let loading = new content.Promise(resolve => {
+ ifr.onload = resolve;
+ });
+ content.document.body.appendChild(ifr);
+ ifr.src = obj.page;
+ await loading;
+
+ info(
+ "The 3rd party content should not have access to first party storage."
+ );
+ await new content.Promise(resolve => {
+ content.addEventListener("message", function msg(event) {
+ if (event.data.type == "finish") {
+ content.removeEventListener("message", msg);
+ resolve();
+ return;
+ }
+
+ if (event.data.type == "ok") {
+ ok(event.data.what, event.data.msg);
+ return;
+ }
+
+ if (event.data.type == "info") {
+ info(event.data.msg);
+ return;
+ }
+
+ ok(false, "Unknown message");
+ });
+ ifr.contentWindow.postMessage({ callback: msg.blockingCallback }, "*");
+ });
+
+ let windowClosed = new content.Promise(resolve => {
+ Services.ww.registerNotification(function notification(
+ aSubject,
+ aTopic,
+ aData
+ ) {
+ if (aTopic == "domwindowclosed") {
+ Services.ww.unregisterNotification(notification);
+ resolve();
+ }
+ });
+ });
+
+ info("Opening a window from the iframe.");
+ ifr.contentWindow.postMessage(
+ { callback: msg.openWindowCallback, arg: obj.popup },
+ "*"
+ );
+
+ info("Let's wait for the window to be closed");
+ await windowClosed;
+
+ info("The 3rd party content should have access to first party storage.");
+ await new content.Promise(resolve => {
+ content.addEventListener("message", function msg(event) {
+ if (event.data.type == "finish") {
+ content.removeEventListener("message", msg);
+ resolve();
+ return;
+ }
+
+ if (event.data.type == "ok") {
+ ok(event.data.what, event.data.msg);
+ return;
+ }
+
+ if (event.data.type == "info") {
+ info(event.data.msg);
+ return;
+ }
+
+ ok(false, "Unknown message");
+ });
+ ifr.contentWindow.postMessage(
+ { callback: msg.nonBlockingCallback },
+ "*"
+ );
+ });
+ }
+ );
+
+ info("Removing the tab");
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function () {
+ info("Wait until the storage permission is ready before cleaning up.");
+ await waitStoragePermission();
+
+ info("Cleaning up.");
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+});
+
+async function runTestFirstPartyWindowOpenHeuristic(disableHeuristics) {
+ info(
+ `Starting first-party window.open() heuristic test with heuristic ${
+ disableHeuristics ? "disabled" : "enabled"
+ }.`
+ );
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["privacy.antitracking.enableWebcompat", !disableHeuristics]],
+ });
+
+ // Interact with the tracker first before testing window.open heuristic
+ await AntiTracking.interactWithTracker();
+
+ info("Creating a new tab");
+ let tab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE);
+ gBrowser.selectedTab = tab;
+
+ let browser = gBrowser.getBrowserForTab(tab);
+ await BrowserTestUtils.browserLoaded(browser);
+
+ info("Loading tracking scripts");
+ await SpecialPowers.spawn(
+ browser,
+ [
+ {
+ page: TEST_3RD_PARTY_PAGE,
+ },
+ ],
+ async obj => {
+ info("Tracker shouldn't have storage access initially");
+ let msg = {};
+ msg.blockingCallback = (async _ => {
+ await noStorageAccessInitially();
+ }).toString();
+
+ await new content.Promise(resolve => {
+ let ifr = content.document.createElement("iframe");
+ ifr.onload = function () {
+ info("Sending code to the 3rd party content");
+ ifr.contentWindow.postMessage(msg.blockingCallback, "*");
+ };
+
+ content.addEventListener("message", function msg(event) {
+ if (event.data.type == "finish") {
+ content.removeEventListener("message", msg);
+ resolve();
+ return;
+ }
+
+ if (event.data.type == "ok") {
+ ok(event.data.what, event.data.msg);
+ return;
+ }
+
+ if (event.data.type == "info") {
+ info(event.data.msg);
+ return;
+ }
+
+ ok(false, "Unknown message");
+ });
+
+ content.document.body.appendChild(ifr);
+ ifr.id = "ifr";
+ ifr.src = obj.page;
+ });
+ }
+ );
+
+ info("Calling window.open in a first-party iframe");
+ await SpecialPowers.spawn(
+ browser,
+ [
+ {
+ page: TEST_IFRAME_PAGE,
+ popup: TEST_3RD_PARTY_DOMAIN + TEST_PATH + "3rdPartyOpen.html",
+ },
+ ],
+ async obj => {
+ let ifr = content.document.createElement("iframe");
+ let loading = new content.Promise(resolve => {
+ ifr.onload = resolve;
+ });
+ content.document.body.appendChild(ifr);
+ ifr.src = obj.page;
+ await loading;
+
+ info("Opening a window from the iframe.");
+ await SpecialPowers.spawn(ifr, [obj.popup], async popup => {
+ await new content.Promise(resolve => {
+ content.open(popup);
+ content.addEventListener("message", function msg(event) {
+ if (event.data == "hello!") {
+ resolve();
+ }
+ });
+ });
+ });
+ }
+ );
+
+ await SpecialPowers.spawn(browser, [{ disableHeuristics }], async obj => {
+ info(
+ "If the heuristic is enabled, the tracker should have storage access now."
+ );
+ let msg = {};
+
+ // If the heuristic is disabled, we won't get storage access.
+ if (obj.disableHeuristics) {
+ msg.nonBlockingCallback = (async _ => {
+ /* import-globals-from storageAccessAPIHelpers.js */
+ await stillNoStorageAccess();
+ }).toString();
+ } else {
+ msg.nonBlockingCallback = (async _ => {
+ /* import-globals-from storageAccessAPIHelpers.js */
+ await hasStorageAccessInitially();
+ }).toString();
+ }
+
+ await new content.Promise(resolve => {
+ let ifr = content.document.getElementById("ifr");
+ info("Sending code to the 3rd party content");
+ ifr.contentWindow.postMessage(msg.nonBlockingCallback, "*");
+
+ content.addEventListener("message", function msg(event) {
+ if (event.data.type == "finish") {
+ content.removeEventListener("message", msg);
+ resolve();
+ return;
+ }
+
+ if (event.data.type == "ok") {
+ ok(event.data.what, event.data.msg);
+ return;
+ }
+
+ if (event.data.type == "info") {
+ info(event.data.msg);
+ return;
+ }
+
+ ok(false, "Unknown message");
+ });
+ });
+ });
+
+ info("Removing the tab");
+ BrowserTestUtils.removeTab(tab);
+
+ await SpecialPowers.popPrefEnv();
+
+ info("Cleaning up.");
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+}
+
+add_task(async function testFirstPartyWindowOpenHeuristic() {
+ await runTestFirstPartyWindowOpenHeuristic(false);
+});
+
+add_task(async function testFirstPartyWindowOpenHeuristicDisabled() {
+ await runTestFirstPartyWindowOpenHeuristic(true);
+});
+
+add_task(async function () {
+ UrlClassifierTestUtils.cleanupTestTrackers();
+});
diff --git a/toolkit/components/antitracking/test/browser/browser_storageAccess_TopLevel_Arguments.js b/toolkit/components/antitracking/test/browser/browser_storageAccess_TopLevel_Arguments.js
new file mode 100644
index 0000000000..a88fc8bcb3
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_storageAccess_TopLevel_Arguments.js
@@ -0,0 +1,117 @@
+add_task(async function testArgumentInRequestStorageAccessUnderSite() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.storage_access.enabled", true],
+ ["dom.storage_access.forward_declared.enabled", true],
+ [
+ "network.cookie.cookieBehavior",
+ BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN,
+ ],
+ ["dom.storage_access.auto_grants", false],
+ ["dom.storage_access.max_concurrent_auto_grants", 1],
+ ],
+ });
+ let tab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: TEST_4TH_PARTY_PAGE,
+ });
+ let browser = tab.linkedBrowser;
+ await SpecialPowers.spawn(browser, [], async _ => {
+ SpecialPowers.wrap(content.document).notifyUserGestureActivation();
+ var p = content.document.requestStorageAccessUnderSite("blob://test");
+ try {
+ await p;
+ ok(false, "Blob URLs must be rejected.");
+ } catch {
+ ok(true, "Must reject.");
+ }
+
+ p = content.document.requestStorageAccessUnderSite("about:config");
+ try {
+ await p;
+ ok(false, "about URLs must be rejected.");
+ } catch {
+ ok(true, "Must reject.");
+ }
+
+ p = content.document.requestStorageAccessUnderSite("qwertyuiop");
+ try {
+ await p;
+ ok(false, "Non URLs must be rejected.");
+ } catch {
+ ok(true, "Must reject.");
+ }
+
+ p = content.document.requestStorageAccessUnderSite("");
+ try {
+ await p;
+ ok(false, "Nullstring must be rejected.");
+ } catch {
+ ok(true, "Must reject.");
+ }
+ });
+
+ await BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function testArgumentInCompleteStorageAccessRequest() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.storage_access.enabled", true],
+ ["dom.storage_access.forward_declared.enabled", true],
+ ["network.cookie.cookieBehavior", BEHAVIOR_ACCEPT],
+ ["dom.storage_access.auto_grants", false],
+ ["dom.storage_access.max_concurrent_auto_grants", 1],
+ ],
+ });
+ let tab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: TEST_TOP_PAGE,
+ });
+ let browser = tab.linkedBrowser;
+ await SpecialPowers.spawn(browser, [], async _ => {
+ SpecialPowers.wrap(content.document).notifyUserGestureActivation();
+ var p =
+ content.document.completeStorageAccessRequestFromSite("blob://test");
+ try {
+ await p;
+ ok(false, "Blob URLs must be rejected.");
+ } catch {
+ ok(true, "Must reject.");
+ }
+
+ p = content.document.completeStorageAccessRequestFromSite("about:config");
+ try {
+ await p;
+ ok(false, "about URLs must be rejected.");
+ } catch {
+ ok(true, "Must reject.");
+ }
+
+ p = content.document.completeStorageAccessRequestFromSite("qwertyuiop");
+ try {
+ await p;
+ ok(false, "Non URLs must be rejected.");
+ } catch {
+ ok(true, "Must reject.");
+ }
+
+ p = content.document.completeStorageAccessRequestFromSite("");
+ try {
+ await p;
+ ok(false, "Nullstring must be rejected.");
+ } catch {
+ ok(true, "Must reject.");
+ }
+ });
+ await BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async () => {
+ Services.perms.removeAll();
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+});
diff --git a/toolkit/components/antitracking/test/browser/browser_storageAccess_TopLevel_CookieBehavior.js b/toolkit/components/antitracking/test/browser/browser_storageAccess_TopLevel_CookieBehavior.js
new file mode 100644
index 0000000000..ea44a34f24
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_storageAccess_TopLevel_CookieBehavior.js
@@ -0,0 +1,412 @@
+add_task(async function testBehaviorAcceptRequestStorageAccessUnderSite() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.storage_access.enabled", true],
+ ["dom.storage_access.forward_declared.enabled", true],
+ ["network.cookie.cookieBehavior", BEHAVIOR_ACCEPT],
+ ["dom.storage_access.auto_grants", false],
+ ["dom.storage_access.max_concurrent_auto_grants", 1],
+ ],
+ });
+ let tab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: TEST_3RD_PARTY_PAGE,
+ });
+ let browser = tab.linkedBrowser;
+ await SpecialPowers.spawn(browser, [TEST_DOMAIN], async tp => {
+ SpecialPowers.wrap(content.document).notifyUserGestureActivation();
+ var p = content.document.requestStorageAccessUnderSite(tp);
+ try {
+ await p;
+ ok(true, "Must resolve.");
+ } catch {
+ ok(false, "Must not reject.");
+ }
+ });
+
+ await BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function testBehaviorAcceptCompleteStorageAccessRequest() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.storage_access.enabled", true],
+ ["dom.storage_access.forward_declared.enabled", true],
+ ["network.cookie.cookieBehavior", BEHAVIOR_ACCEPT],
+ ["dom.storage_access.auto_grants", false],
+ ["dom.storage_access.max_concurrent_auto_grants", 1],
+ ],
+ });
+ let tab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: TEST_TOP_PAGE,
+ });
+ let browser = tab.linkedBrowser;
+ await SpecialPowers.spawn(browser, [TEST_3RD_PARTY_DOMAIN], async tp => {
+ await SpecialPowers.pushPermissions([
+ {
+ type: "AllowStorageAccessRequest^http://example.org",
+ allow: 1,
+ context: content.document,
+ },
+ ]);
+ SpecialPowers.wrap(content.document).notifyUserGestureActivation();
+ var p = content.document.completeStorageAccessRequestFromSite(tp);
+ try {
+ await p;
+ ok(true, "Must resolve.");
+ } catch {
+ ok(false, "Must not reject.");
+ }
+ });
+ await BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function testBehaviorRejectRequestStorageAccessUnderSite() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.storage_access.enabled", true],
+ ["dom.storage_access.forward_declared.enabled", true],
+ ["network.cookie.cookieBehavior", BEHAVIOR_REJECT],
+ ["dom.storage_access.auto_grants", false],
+ ["dom.storage_access.max_concurrent_auto_grants", 1],
+ ],
+ });
+ let tab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: TEST_3RD_PARTY_PAGE,
+ });
+ let browser = tab.linkedBrowser;
+ await SpecialPowers.spawn(browser, [TEST_DOMAIN], async tp => {
+ SpecialPowers.wrap(content.document).notifyUserGestureActivation();
+ var p = content.document.requestStorageAccessUnderSite(tp);
+ try {
+ await p;
+ ok(false, "Must not resolve.");
+ } catch {
+ ok(true, "Must reject.");
+ }
+ });
+
+ await BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function testBehaviorRejectCompleteStorageAccessRequest() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.storage_access.enabled", true],
+ ["dom.storage_access.forward_declared.enabled", true],
+ ["network.cookie.cookieBehavior", BEHAVIOR_REJECT],
+ ["dom.storage_access.auto_grants", false],
+ ["dom.storage_access.max_concurrent_auto_grants", 1],
+ ],
+ });
+ let tab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: TEST_TOP_PAGE,
+ });
+ let browser = tab.linkedBrowser;
+ await SpecialPowers.spawn(browser, [TEST_3RD_PARTY_DOMAIN], async tp => {
+ await SpecialPowers.pushPermissions([
+ {
+ type: "AllowStorageAccessRequest^http://example.org",
+ allow: 1,
+ context: content.document,
+ },
+ ]);
+ SpecialPowers.wrap(content.document).notifyUserGestureActivation();
+ var p = content.document.completeStorageAccessRequestFromSite(tp);
+ try {
+ await p;
+ ok(false, "Must not resolve.");
+ } catch {
+ ok(true, "Must reject.");
+ }
+ });
+ await BrowserTestUtils.removeTab(tab);
+});
+
+add_task(
+ async function testBehaviorLimitForeignRequestStorageAccessUnderSite() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.storage_access.enabled", true],
+ ["dom.storage_access.forward_declared.enabled", true],
+ ["network.cookie.cookieBehavior", BEHAVIOR_LIMIT_FOREIGN],
+ ["dom.storage_access.auto_grants", false],
+ ["dom.storage_access.max_concurrent_auto_grants", 1],
+ ],
+ });
+ let tab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: TEST_3RD_PARTY_PAGE,
+ });
+ let browser = tab.linkedBrowser;
+ await SpecialPowers.spawn(browser, [TEST_DOMAIN], async tp => {
+ SpecialPowers.wrap(content.document).notifyUserGestureActivation();
+ var p = content.document.requestStorageAccessUnderSite(tp);
+ try {
+ await p;
+ ok(false, "Must not resolve.");
+ } catch {
+ ok(true, "Must reject.");
+ }
+ });
+
+ await BrowserTestUtils.removeTab(tab);
+ }
+);
+
+add_task(async function testBehaviorLimitForeignCompleteStorageAccessRequest() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.storage_access.enabled", true],
+ ["dom.storage_access.forward_declared.enabled", true],
+ ["network.cookie.cookieBehavior", BEHAVIOR_LIMIT_FOREIGN],
+ ["dom.storage_access.auto_grants", false],
+ ["dom.storage_access.max_concurrent_auto_grants", 1],
+ ],
+ });
+ let tab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: TEST_TOP_PAGE,
+ });
+ let browser = tab.linkedBrowser;
+ await SpecialPowers.spawn(browser, [TEST_3RD_PARTY_DOMAIN], async tp => {
+ await SpecialPowers.pushPermissions([
+ {
+ type: "AllowStorageAccessRequest^http://example.org",
+ allow: 1,
+ context: content.document,
+ },
+ ]);
+ SpecialPowers.wrap(content.document).notifyUserGestureActivation();
+ var p = content.document.completeStorageAccessRequestFromSite(tp);
+ try {
+ await p;
+ ok(false, "Must not resolve.");
+ } catch {
+ ok(true, "Must reject.");
+ }
+ });
+ await BrowserTestUtils.removeTab(tab);
+});
+
+add_task(
+ async function testBehaviorRejectForeignRequestStorageAccessUnderSite() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.storage_access.enabled", true],
+ ["dom.storage_access.forward_declared.enabled", true],
+ ["network.cookie.cookieBehavior", BEHAVIOR_REJECT_FOREIGN],
+ ["dom.storage_access.auto_grants", false],
+ ["dom.storage_access.max_concurrent_auto_grants", 1],
+ ],
+ });
+ let tab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: TEST_3RD_PARTY_PAGE,
+ });
+ let browser = tab.linkedBrowser;
+ await SpecialPowers.spawn(browser, [TEST_DOMAIN], async tp => {
+ SpecialPowers.wrap(content.document).notifyUserGestureActivation();
+ var p = content.document.requestStorageAccessUnderSite(tp);
+ try {
+ await p;
+ ok(false, "Must not resolve.");
+ } catch {
+ ok(true, "Must reject.");
+ }
+ });
+
+ await BrowserTestUtils.removeTab(tab);
+ }
+);
+
+add_task(
+ async function testBehaviorRejectForeignCompleteStorageAccessRequest() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.storage_access.enabled", true],
+ ["dom.storage_access.forward_declared.enabled", true],
+ ["network.cookie.cookieBehavior", BEHAVIOR_REJECT_FOREIGN],
+ ["dom.storage_access.auto_grants", false],
+ ["dom.storage_access.max_concurrent_auto_grants", 1],
+ ],
+ });
+ let tab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: TEST_TOP_PAGE,
+ });
+ let browser = tab.linkedBrowser;
+ await SpecialPowers.spawn(browser, [TEST_3RD_PARTY_DOMAIN], async tp => {
+ await SpecialPowers.pushPermissions([
+ {
+ type: "AllowStorageAccessRequest^http://example.org",
+ allow: 1,
+ context: content.document,
+ },
+ ]);
+ SpecialPowers.wrap(content.document).notifyUserGestureActivation();
+ var p = content.document.completeStorageAccessRequestFromSite(tp);
+ try {
+ await p;
+ ok(false, "Must not resolve.");
+ } catch {
+ ok(true, "Must reject.");
+ }
+ });
+ await BrowserTestUtils.removeTab(tab);
+ }
+);
+
+add_task(
+ async function testBehaviorRejectTrackerRequestStorageAccessUnderSite() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.storage_access.enabled", true],
+ ["dom.storage_access.forward_declared.enabled", true],
+ ["network.cookie.cookieBehavior", BEHAVIOR_REJECT_TRACKER],
+ ["dom.storage_access.auto_grants", false],
+ ["dom.storage_access.max_concurrent_auto_grants", 1],
+ ],
+ });
+ let tab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: TEST_3RD_PARTY_DOMAIN,
+ });
+ let browser = tab.linkedBrowser;
+ await SpecialPowers.spawn(browser, [TEST_DOMAIN], async tp => {
+ SpecialPowers.wrap(content.document).notifyUserGestureActivation();
+ var p = content.document.requestStorageAccessUnderSite(tp);
+ try {
+ await p;
+ ok(true, "Must resolve.");
+ } catch {
+ ok(false, "Must not reject.");
+ }
+ });
+
+ await BrowserTestUtils.removeTab(tab);
+ }
+);
+
+add_task(
+ async function testBehaviorRejectTrackerCompleteStorageAccessRequest() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.storage_access.enabled", true],
+ ["dom.storage_access.forward_declared.enabled", true],
+ ["network.cookie.cookieBehavior", BEHAVIOR_REJECT_TRACKER],
+ ["dom.storage_access.auto_grants", false],
+ ["dom.storage_access.max_concurrent_auto_grants", 1],
+ ],
+ });
+ let tab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: TEST_TOP_PAGE,
+ });
+ let browser = tab.linkedBrowser;
+ await SpecialPowers.spawn(browser, [TEST_3RD_PARTY_DOMAIN], async tp => {
+ await SpecialPowers.pushPermissions([
+ {
+ type: "AllowStorageAccessRequest^http://example.com",
+ allow: 1,
+ context: content.document,
+ },
+ ]);
+ SpecialPowers.wrap(content.document).notifyUserGestureActivation();
+ var p = content.document.completeStorageAccessRequestFromSite(tp);
+ try {
+ await p;
+ ok(true, "Must resolve.");
+ } catch {
+ ok(false, "Must not reject.");
+ }
+ });
+ await BrowserTestUtils.removeTab(tab);
+ }
+);
+
+add_task(
+ async function testBehaviorRejectTrackerAndPartitionForeignRequestStorageAccessUnderSite() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.storage_access.enabled", true],
+ ["dom.storage_access.forward_declared.enabled", true],
+ [
+ "network.cookie.cookieBehavior",
+ BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN,
+ ],
+ ["dom.storage_access.auto_grants", false],
+ ["dom.storage_access.max_concurrent_auto_grants", 1],
+ ],
+ });
+ let tab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: TEST_3RD_PARTY_DOMAIN,
+ });
+ let browser = tab.linkedBrowser;
+ await SpecialPowers.spawn(browser, [TEST_DOMAIN], async tp => {
+ SpecialPowers.wrap(content.document).notifyUserGestureActivation();
+ var p = content.document.requestStorageAccessUnderSite(tp);
+ try {
+ await p;
+ ok(true, "Must resolve.");
+ } catch {
+ ok(false, "Must not reject.");
+ }
+ });
+
+ await BrowserTestUtils.removeTab(tab);
+ }
+);
+
+add_task(
+ async function testBehaviorRejectTrackerAndPartitionForeignCompleteStorageAccessRequest() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.storage_access.enabled", true],
+ ["dom.storage_access.forward_declared.enabled", true],
+ [
+ "network.cookie.cookieBehavior",
+ BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN,
+ ],
+ ["dom.storage_access.auto_grants", false],
+ ["dom.storage_access.max_concurrent_auto_grants", 1],
+ ],
+ });
+ let tab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: TEST_TOP_PAGE,
+ });
+ let browser = tab.linkedBrowser;
+ await SpecialPowers.spawn(browser, [TEST_3RD_PARTY_DOMAIN], async tp => {
+ await SpecialPowers.pushPermissions([
+ {
+ type: "AllowStorageAccessRequest^http://example.com",
+ allow: 1,
+ context: content.document,
+ },
+ ]);
+ SpecialPowers.wrap(content.document).notifyUserGestureActivation();
+ var p = content.document.completeStorageAccessRequestFromSite(tp);
+ try {
+ await p;
+ ok(true, "Must resolve.");
+ } catch {
+ ok(false, "Must not reject.");
+ }
+ });
+ await BrowserTestUtils.removeTab(tab);
+ }
+);
+
+add_task(async () => {
+ Services.perms.removeAll();
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+});
diff --git a/toolkit/components/antitracking/test/browser/browser_storageAccess_TopLevel_CookiePermission.js b/toolkit/components/antitracking/test/browser/browser_storageAccess_TopLevel_CookiePermission.js
new file mode 100644
index 0000000000..64ead20020
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_storageAccess_TopLevel_CookiePermission.js
@@ -0,0 +1,174 @@
+add_task(async _ => {
+ PermissionTestUtils.add(
+ TEST_4TH_PARTY_PAGE,
+ "cookie",
+ Services.perms.ALLOW_ACTION
+ );
+});
+
+add_task(async function testCookiePermissionRequestStorageAccessUnderSite() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.storage_access.enabled", true],
+ ["dom.storage_access.forward_declared.enabled", true],
+ [
+ "network.cookie.cookieBehavior",
+ BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN,
+ ],
+ ["dom.storage_access.auto_grants", false],
+ ["dom.storage_access.max_concurrent_auto_grants", 1],
+ ],
+ });
+ let tab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: TEST_4TH_PARTY_PAGE,
+ });
+ let browser = tab.linkedBrowser;
+ await SpecialPowers.spawn(browser, [TEST_DOMAIN], async tp => {
+ SpecialPowers.wrap(content.document).notifyUserGestureActivation();
+ var p = content.document.requestStorageAccessUnderSite(tp);
+ try {
+ await p;
+ ok(true, "Must resolve.");
+ } catch {
+ ok(false, "Must not reject.");
+ }
+ });
+ await BrowserTestUtils.removeTab(tab);
+ await SpecialPowers.popPermissions();
+});
+
+add_task(async function testCookiePermissionCompleteStorageAccessRequest() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.storage_access.enabled", true],
+ ["dom.storage_access.forward_declared.enabled", true],
+ [
+ "network.cookie.cookieBehavior",
+ BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN,
+ ],
+ ["dom.storage_access.auto_grants", false],
+ ["dom.storage_access.max_concurrent_auto_grants", 1],
+ ],
+ });
+ let tab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: TEST_TOP_PAGE,
+ });
+ let browser = tab.linkedBrowser;
+ await SpecialPowers.spawn(browser, [TEST_4TH_PARTY_DOMAIN], async tp => {
+ await SpecialPowers.pushPermissions([
+ {
+ type: "AllowStorageAccessRequest^http://example.com",
+ allow: Services.perms.ALLOW_ACTION,
+ context: content.document,
+ },
+ ]);
+ SpecialPowers.wrap(content.document).notifyUserGestureActivation();
+ var p = content.document.completeStorageAccessRequestFromSite(tp);
+ try {
+ await p;
+ ok(true, "Must resolve.");
+ } catch {
+ ok(false, "Must not reject.");
+ }
+ });
+ await BrowserTestUtils.removeTab(tab);
+ await SpecialPowers.popPermissions();
+});
+
+add_task(async _ => {
+ Services.perms.removeAll();
+});
+
+add_task(async _ => {
+ PermissionTestUtils.add(
+ TEST_4TH_PARTY_PAGE,
+ "cookie",
+ Services.perms.DENY_ACTION
+ );
+});
+
+add_task(
+ async function testCookiePermissionRejectRequestStorageAccessUnderSite() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.storage_access.enabled", true],
+ ["dom.storage_access.forward_declared.enabled", true],
+ [
+ "network.cookie.cookieBehavior",
+ BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN,
+ ],
+ ["dom.storage_access.auto_grants", false],
+ ["dom.storage_access.max_concurrent_auto_grants", 1],
+ ],
+ });
+ let tab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: TEST_4TH_PARTY_PAGE,
+ });
+ let browser = tab.linkedBrowser;
+ await SpecialPowers.spawn(browser, [TEST_DOMAIN], async tp => {
+ SpecialPowers.wrap(content.document).notifyUserGestureActivation();
+ var p = content.document.requestStorageAccessUnderSite(tp);
+ try {
+ await p;
+ ok(false, "Must not resolve.");
+ } catch {
+ ok(true, "Must reject.");
+ }
+ });
+ await BrowserTestUtils.removeTab(tab);
+ await SpecialPowers.popPermissions();
+ }
+);
+
+add_task(
+ async function testCookiePermissionRejectCompleteStorageAccessRequest() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.storage_access.enabled", true],
+ ["dom.storage_access.forward_declared.enabled", true],
+ [
+ "network.cookie.cookieBehavior",
+ BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN,
+ ],
+ ["dom.storage_access.auto_grants", false],
+ ["dom.storage_access.max_concurrent_auto_grants", 1],
+ ],
+ });
+ let tab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: TEST_TOP_PAGE,
+ });
+ let browser = tab.linkedBrowser;
+ await SpecialPowers.spawn(browser, [TEST_4TH_PARTY_DOMAIN], async tp => {
+ await SpecialPowers.pushPermissions([
+ {
+ type: "AllowStorageAccessRequest^http://example.com",
+ allow: Services.perms.ALLOW_ACTION,
+ context: content.document,
+ },
+ ]);
+ SpecialPowers.wrap(content.document).notifyUserGestureActivation();
+ var p = content.document.completeStorageAccessRequestFromSite(tp);
+ try {
+ await p;
+ ok(false, "Must not resolve.");
+ } catch {
+ ok(true, "Must reject.");
+ }
+ });
+ await BrowserTestUtils.removeTab(tab);
+ await SpecialPowers.popPermissions();
+ }
+);
+
+add_task(async () => {
+ Services.perms.removeAll();
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+});
diff --git a/toolkit/components/antitracking/test/browser/browser_storageAccess_TopLevel_CrossOriginSameSite.js b/toolkit/components/antitracking/test/browser/browser_storageAccess_TopLevel_CrossOriginSameSite.js
new file mode 100644
index 0000000000..ca3e47d8e7
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_storageAccess_TopLevel_CrossOriginSameSite.js
@@ -0,0 +1,162 @@
+add_task(async function testIntermediatePreferenceReadSameSite() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.storage_access.enabled", true],
+ ["dom.storage_access.forward_declared.enabled", true],
+ [
+ "network.cookie.cookieBehavior",
+ BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN,
+ ],
+ ["dom.storage_access.auto_grants", false],
+ ["dom.storage_access.max_concurrent_auto_grants", 1],
+ ],
+ });
+ let tab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: TEST_DOMAIN_7,
+ });
+ let browser = tab.linkedBrowser;
+ await SpecialPowers.spawn(browser, [TEST_3RD_PARTY_DOMAIN], async tp => {
+ SpecialPowers.wrap(content.document).notifyUserGestureActivation();
+ var p = content.document.completeStorageAccessRequestFromSite(tp);
+ try {
+ await p;
+ ok(false, "Must not resolve.");
+ } catch {
+ ok(true, "Must reject because we don't have the initial request.");
+ }
+ });
+
+ await SpecialPowers.pushPermissions([
+ {
+ type: "AllowStorageAccessRequest^https://example.com",
+ allow: 1,
+ context: TEST_DOMAIN_7,
+ },
+ ]);
+
+ await SpecialPowers.spawn(browser, [TEST_3RD_PARTY_DOMAIN], async tp => {
+ SpecialPowers.wrap(content.document).notifyUserGestureActivation();
+ var p = content.document.completeStorageAccessRequestFromSite(tp);
+ try {
+ await p;
+ ok(false, "Must not resolve.");
+ } catch {
+ ok(true, "Must reject because the permission is cross site.");
+ }
+ });
+
+ await SpecialPowers.pushPermissions([
+ {
+ type: "AllowStorageAccessRequest^https://example.org",
+ allow: 1,
+ context: TEST_DOMAIN_7,
+ },
+ ]);
+
+ await SpecialPowers.spawn(browser, [TEST_3RD_PARTY_DOMAIN], async tp => {
+ SpecialPowers.wrap(content.document).notifyUserGestureActivation();
+ var p = content.document.completeStorageAccessRequestFromSite(tp);
+ try {
+ await p;
+ ok(
+ true,
+ "Must resolve now that we have the permission from the embedee."
+ );
+ } catch {
+ ok(false, "Must not reject.");
+ }
+ });
+
+ await SpecialPowers.pushPermissions([
+ {
+ type: "AllowStorageAccessRequest^https://example.org",
+ allow: 1,
+ context: TEST_DOMAIN_8,
+ },
+ ]);
+
+ await SpecialPowers.spawn(browser, [TEST_3RD_PARTY_DOMAIN], async tp => {
+ SpecialPowers.wrap(content.document).notifyUserGestureActivation();
+ var p = content.document.completeStorageAccessRequestFromSite(tp);
+ try {
+ await p;
+ ok(
+ true,
+ "Must resolve now that we have the permission from the embedee."
+ );
+ } catch {
+ ok(false, "Must not reject.");
+ }
+ });
+
+ await BrowserTestUtils.removeTab(tab);
+});
+
+// Note: TEST_DOMAIN_7 and TEST_DOMAIN_8 are Same-Site
+add_task(async function testIntermediatePreferenceWriteCrossOrigin() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.storage_access.enabled", true],
+ ["dom.storage_access.forward_declared.enabled", true],
+ [
+ "network.cookie.cookieBehavior",
+ BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN,
+ ],
+ ["dom.storage_access.auto_grants", false],
+ ["dom.storage_access.max_concurrent_auto_grants", 1],
+ ],
+ });
+ let tab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: TEST_3RD_PARTY_PAGE,
+ });
+ let browser = tab.linkedBrowser;
+ await SpecialPowers.spawn(browser, [TEST_DOMAIN_8], async tp => {
+ SpecialPowers.wrap(content.document).notifyUserGestureActivation();
+ var p = content.document.requestStorageAccessUnderSite(tp);
+ try {
+ await p;
+ ok(
+ true,
+ "Must resolve- no funny business here, we just want to set the intermediate pref"
+ );
+ } catch {
+ ok(false, "Must not reject.");
+ }
+ });
+
+ let principal =
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(
+ TEST_DOMAIN_8
+ );
+ // Important to note that this is the site but not origin of TEST_3RD_PARTY_PAGE
+ var permission = Services.perms.testPermissionFromPrincipal(
+ principal,
+ "AllowStorageAccessRequest^https://example.org"
+ );
+ ok(permission == Services.perms.ALLOW_ACTION);
+
+ // Test that checking the permission across site works
+ principal =
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(
+ TEST_DOMAIN_7
+ );
+ // Important to note that this is the site but not origin of TEST_3RD_PARTY_PAGE
+ permission = Services.perms.testPermissionFromPrincipal(
+ principal,
+ "AllowStorageAccessRequest^https://example.org"
+ );
+ ok(permission == Services.perms.ALLOW_ACTION);
+
+ await BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async () => {
+ Services.perms.removeAll();
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+});
diff --git a/toolkit/components/antitracking/test/browser/browser_storageAccess_TopLevel_Doorhanger.js b/toolkit/components/antitracking/test/browser/browser_storageAccess_TopLevel_Doorhanger.js
new file mode 100644
index 0000000000..e8d6ce9bb1
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_storageAccess_TopLevel_Doorhanger.js
@@ -0,0 +1,122 @@
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/browser/modules/test/browser/head.js",
+ this
+);
+
+async function cleanUp() {
+ Services.perms.removeAll();
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+}
+
+add_task(async function testDoorhangerRequestStorageAccessUnderSite() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.storage_access.enabled", true],
+ ["dom.storage_access.forward_declared.enabled", true],
+ ["dom.storage_access.prompt.testing", false],
+ [
+ "network.cookie.cookieBehavior",
+ BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN,
+ ],
+ ["dom.storage_access.auto_grants", false],
+ ["dom.storage_access.max_concurrent_auto_grants", 1],
+ ],
+ });
+ let tab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: TEST_4TH_PARTY_PAGE,
+ });
+ let browser = tab.linkedBrowser;
+ let permChanged = TestUtils.topicObserved("perm-changed", (subject, data) => {
+ let result =
+ subject
+ .QueryInterface(Ci.nsIPermission)
+ .type.startsWith("AllowStorageAccessRequest^") &&
+ subject.principal.origin == new URL(TEST_TOP_PAGE).origin &&
+ data == "added";
+ return result;
+ }).then(() => {
+ ok(true, "Permission changed to add intermediate permission");
+ });
+ let shownPromise = BrowserTestUtils.waitForEvent(
+ PopupNotifications.panel,
+ "popupshown"
+ ).then(_ => {
+ ok(true, "Must display doorhanger from RequestStorageAccessUnderSite");
+ return clickMainAction();
+ });
+ let sp = SpecialPowers.spawn(browser, [TEST_DOMAIN], async tp => {
+ SpecialPowers.wrap(content.document).notifyUserGestureActivation();
+ let p = content.document.requestStorageAccessUnderSite(tp);
+ try {
+ await p;
+ ok(true, "Must resolve.");
+ } catch {
+ ok(false, "Must not reject.");
+ }
+ });
+
+ await Promise.all([sp, shownPromise, permChanged]);
+ await cleanUp();
+ await BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function testNoDoorhangerCompleteStorageAccessRequestFromSite() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.storage_access.enabled", true],
+ ["dom.storage_access.forward_declared.enabled", true],
+ [
+ "network.cookie.cookieBehavior",
+ BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN,
+ ],
+ ["dom.storage_access.prompt.testing", false],
+ ["dom.storage_access.auto_grants", false],
+ ["dom.storage_access.max_concurrent_auto_grants", 1],
+ ],
+ });
+ let tab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: TEST_TOP_PAGE,
+ });
+ let browser = tab.linkedBrowser;
+ let popupShown = false;
+ // This promise is used to the absence of a doorhanger showing up.
+ BrowserTestUtils.waitForEvent(PopupNotifications.panel, "popupshown")
+ .then(_ => {
+ // This will be called if a doorhanger is shown.
+ ok(
+ false,
+ "Must not display doorhanger from CompleteStorageAccessRequestFromSite"
+ );
+ popupShown = true;
+ })
+ .catch(_ => {
+ // This will be called when the test ends if a doorhanger is not shown
+ ok(true, "It is expected for this popup to not show up.");
+ });
+ await SpecialPowers.spawn(browser, [TEST_4TH_PARTY_DOMAIN], async tp => {
+ await SpecialPowers.pushPermissions([
+ {
+ type: "AllowStorageAccessRequest^http://example.com",
+ allow: 1,
+ context: content.document,
+ },
+ ]);
+ SpecialPowers.wrap(content.document).notifyUserGestureActivation();
+ let p = content.document.completeStorageAccessRequestFromSite(tp);
+ try {
+ await p;
+ ok(true, "Must resolve.");
+ } catch {
+ ok(false, "Must not reject.");
+ }
+ });
+ ok(!popupShown, "Must not have shown a popup during this test.");
+ await cleanUp();
+ await BrowserTestUtils.removeTab(tab);
+});
diff --git a/toolkit/components/antitracking/test/browser/browser_storageAccess_TopLevel_Embed.js b/toolkit/components/antitracking/test/browser/browser_storageAccess_TopLevel_Embed.js
new file mode 100644
index 0000000000..86f584763c
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_storageAccess_TopLevel_Embed.js
@@ -0,0 +1,155 @@
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/toolkit/components/antitracking/test/browser/storage_access_head.js",
+ this
+);
+
+async function requestStorageAccessUnderSiteAndExpectSuccess() {
+ SpecialPowers.wrap(document).notifyUserGestureActivation();
+ var p = document.requestStorageAccessUnderSite("http://example.org");
+ try {
+ await p;
+ ok(true, "Must resolve.");
+ } catch {
+ ok(false, "Must not reject.");
+ }
+}
+
+async function requestStorageAccessUnderSiteAndExpectFailure() {
+ SpecialPowers.wrap(document).notifyUserGestureActivation();
+ var p = document.requestStorageAccessUnderSite("http://example.org");
+ try {
+ await p;
+ ok(false, "Must not resolve.");
+ } catch {
+ ok(true, "Must reject.");
+ }
+}
+
+async function completeStorageAccessRequestFromSiteAndExpectSuccess() {
+ SpecialPowers.wrap(document).notifyUserGestureActivation();
+ var p = document.completeStorageAccessRequestFromSite("http://example.org");
+ try {
+ await p;
+ ok(true, "Must resolve.");
+ } catch {
+ ok(false, "Must not reject.");
+ }
+}
+
+async function completeStorageAccessRequestFromSiteAndExpectFailure() {
+ SpecialPowers.wrap(document).notifyUserGestureActivation();
+ var p = document.completeStorageAccessRequestFromSite("http://example.org");
+ try {
+ await p;
+ ok(false, "Must not resolve.");
+ } catch {
+ ok(true, "Must reject.");
+ }
+}
+
+async function setIntermediatePreference() {
+ await SpecialPowers.pushPermissions([
+ {
+ type: "AllowStorageAccessRequest^http://example.org",
+ allow: 1,
+ context: "http://example.com/",
+ },
+ ]);
+}
+
+async function configurePrefs() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.storage_access.enabled", true],
+ ["dom.storage_access.forward_declared.enabled", true],
+ [
+ "network.cookie.cookieBehavior",
+ BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN,
+ ],
+ ["dom.storage_access.auto_grants", false],
+ ["dom.storage_access.max_concurrent_auto_grants", 1],
+ ],
+ });
+}
+
+add_task(async function rSAUS_sameOriginIframe() {
+ await configurePrefs();
+ await openPageAndRunCode(
+ TEST_TOP_PAGE_7,
+ () => {},
+ TEST_DOMAIN_7 + TEST_PATH + "3rdParty.html",
+ requestStorageAccessUnderSiteAndExpectSuccess
+ );
+ await cleanUpData();
+ await SpecialPowers.flushPrefEnv();
+});
+
+add_task(async function rSAUS_sameSiteIframe() {
+ await configurePrefs();
+ await openPageAndRunCode(
+ TEST_TOP_PAGE_7,
+ () => {},
+ TEST_DOMAIN_8 + TEST_PATH + "3rdParty.html",
+ requestStorageAccessUnderSiteAndExpectSuccess
+ );
+ await cleanUpData();
+ await SpecialPowers.flushPrefEnv();
+});
+
+add_task(async function rSAUS_crossSiteIframe() {
+ await configurePrefs();
+ await openPageAndRunCode(
+ TEST_TOP_PAGE_7,
+ () => {},
+ TEST_DOMAIN + TEST_PATH + "3rdParty.html",
+ requestStorageAccessUnderSiteAndExpectFailure
+ );
+ await cleanUpData();
+ await SpecialPowers.flushPrefEnv();
+});
+
+add_task(async function cSAR_sameOriginIframe() {
+ await configurePrefs();
+ await openPageAndRunCode(
+ TEST_TOP_PAGE_7,
+ setIntermediatePreference,
+ TEST_DOMAIN_7 + TEST_PATH + "3rdParty.html",
+ completeStorageAccessRequestFromSiteAndExpectSuccess
+ );
+ await cleanUpData();
+ await SpecialPowers.flushPrefEnv();
+});
+
+add_task(async function cSAR_sameSiteIframe() {
+ await configurePrefs();
+ await setIntermediatePreference();
+ await openPageAndRunCode(
+ TEST_TOP_PAGE_7,
+ () => {},
+ TEST_DOMAIN_8 + TEST_PATH + "3rdParty.html",
+ completeStorageAccessRequestFromSiteAndExpectSuccess
+ );
+ await cleanUpData();
+ await SpecialPowers.flushPrefEnv();
+});
+
+add_task(async function cSAR_crossSiteIframe() {
+ await configurePrefs();
+ await openPageAndRunCode(
+ TEST_TOP_PAGE_7,
+ setIntermediatePreference,
+ TEST_DOMAIN + TEST_PATH + "3rdParty.html",
+ completeStorageAccessRequestFromSiteAndExpectFailure
+ );
+ await cleanUpData();
+ await SpecialPowers.flushPrefEnv();
+});
+
+add_task(async () => {
+ Services.perms.removeAll();
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+});
diff --git a/toolkit/components/antitracking/test/browser/browser_storageAccess_TopLevel_Enable.js b/toolkit/components/antitracking/test/browser/browser_storageAccess_TopLevel_Enable.js
new file mode 100644
index 0000000000..6ee61cc378
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_storageAccess_TopLevel_Enable.js
@@ -0,0 +1,86 @@
+add_task(async function testDefaultDisabled() {
+ let value = Services.prefs.getBoolPref(
+ "dom.storage_access.forward_declared.enabled"
+ );
+ ok(!value, "dom.storage_access.forward_declared.enabled should be false");
+ let tab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: TEST_TOP_PAGE,
+ });
+ let browser = tab.linkedBrowser;
+ await SpecialPowers.spawn(browser, [], async _ => {
+ ok(
+ content.window.requestStorageAccessUnderSite == undefined,
+ "API should not be on the window"
+ );
+ ok(
+ content.window.completeStorageAccessRequestFromSite == undefined,
+ "API should not be on the window"
+ );
+ });
+ await BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function testExplicitlyDisabled() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["dom.storage_access.forward_declared.enabled", false]],
+ });
+ let value = Services.prefs.getBoolPref(
+ "dom.storage_access.forward_declared.enabled"
+ );
+ ok(!value, "dom.storage_access.forward_declared.enabled should be false");
+ let tab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: TEST_TOP_PAGE,
+ });
+ let browser = tab.linkedBrowser;
+ await SpecialPowers.spawn(browser, [], async _ => {
+ ok(
+ content.window.requestStorageAccessUnderSite == undefined,
+ "API should not be on the window"
+ );
+ ok(
+ content.window.completeStorageAccessRequestFromSite == undefined,
+ "API should not be on the window"
+ );
+ });
+ await BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function testExplicitlyEnabled() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.storage_access.enabled", true],
+ ["dom.storage_access.forward_declared.enabled", true],
+ ],
+ });
+ let value = Services.prefs.getBoolPref(
+ "dom.storage_access.forward_declared.enabled"
+ );
+ ok(value, "dom.storage_access.forward_declared.enabled should be true");
+ let tab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: TEST_TOP_PAGE,
+ });
+ let browser = tab.linkedBrowser;
+ await SpecialPowers.spawn(browser, [], async _ => {
+ ok(
+ content.document.requestStorageAccessUnderSite != undefined,
+ "API should be on the window"
+ );
+ ok(
+ content.document.completeStorageAccessRequestFromSite != undefined,
+ "API should be on the window"
+ );
+ });
+ await BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async () => {
+ Services.perms.removeAll();
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+});
diff --git a/toolkit/components/antitracking/test/browser/browser_storageAccess_TopLevel_RequireIntermediatePermission.js b/toolkit/components/antitracking/test/browser/browser_storageAccess_TopLevel_RequireIntermediatePermission.js
new file mode 100644
index 0000000000..a5cc6f10b5
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_storageAccess_TopLevel_RequireIntermediatePermission.js
@@ -0,0 +1,61 @@
+add_task(async function testIntermediatePermissionRequired() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.storage_access.enabled", true],
+ ["dom.storage_access.forward_declared.enabled", true],
+ [
+ "network.cookie.cookieBehavior",
+ BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN,
+ ],
+ ["dom.storage_access.auto_grants", false],
+ ["dom.storage_access.max_concurrent_auto_grants", 1],
+ ],
+ });
+ let tab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: TEST_TOP_PAGE,
+ });
+ let browser = tab.linkedBrowser;
+ await SpecialPowers.spawn(browser, [TEST_3RD_PARTY_DOMAIN], async tp => {
+ SpecialPowers.wrap(content.document).notifyUserGestureActivation();
+ var p = content.document.completeStorageAccessRequestFromSite(tp);
+ try {
+ await p;
+ ok(false, "Must not resolve.");
+ } catch {
+ ok(true, "Must reject because we don't have the initial request.");
+ }
+ });
+
+ await SpecialPowers.pushPermissions([
+ {
+ type: "AllowStorageAccessRequest^https://example.org",
+ allow: 1,
+ context: TEST_TOP_PAGE,
+ },
+ ]);
+
+ await SpecialPowers.spawn(browser, [TEST_3RD_PARTY_DOMAIN], async tp => {
+ SpecialPowers.wrap(content.document).notifyUserGestureActivation();
+ var p = content.document.completeStorageAccessRequestFromSite(tp);
+ try {
+ await p;
+ ok(
+ true,
+ "Must resolve now that we have the permission from the embedee."
+ );
+ } catch {
+ ok(false, "Must not reject.");
+ }
+ });
+ await BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async () => {
+ Services.perms.removeAll();
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+});
diff --git a/toolkit/components/antitracking/test/browser/browser_storageAccess_TopLevel_StorageAccessPermission.js b/toolkit/components/antitracking/test/browser/browser_storageAccess_TopLevel_StorageAccessPermission.js
new file mode 100644
index 0000000000..0b4f3e7273
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_storageAccess_TopLevel_StorageAccessPermission.js
@@ -0,0 +1,94 @@
+add_task(
+ async function testStorageAccessPermissionRequestStorageAccessUnderSite() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.storage_access.enabled", true],
+ ["dom.storage_access.forward_declared.enabled", true],
+ [
+ "network.cookie.cookieBehavior",
+ BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN,
+ ],
+ ["dom.storage_access.auto_grants", false],
+ ["dom.storage_access.max_concurrent_auto_grants", 1],
+ ],
+ });
+ let tab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: TEST_4TH_PARTY_PAGE,
+ });
+ let browser = tab.linkedBrowser;
+ await SpecialPowers.spawn(browser, [TEST_DOMAIN], async tp => {
+ await SpecialPowers.pushPermissions([
+ {
+ type: "3rdPartyStorage^http://not-tracking.example.com",
+ allow: 1,
+ context: tp,
+ },
+ ]);
+ SpecialPowers.wrap(content.document).notifyUserGestureActivation();
+ var p = content.document.requestStorageAccessUnderSite(tp);
+ try {
+ await p;
+ ok(true, "Must resolve.");
+ } catch {
+ ok(false, "Must not reject.");
+ }
+ });
+
+ await BrowserTestUtils.removeTab(tab);
+ }
+);
+
+add_task(
+ async function testStorageAccessPermissionCompleteStorageAccessRequestFromSite() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.storage_access.enabled", true],
+ ["dom.storage_access.forward_declared.enabled", true],
+ [
+ "network.cookie.cookieBehavior",
+ BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN,
+ ],
+ ["dom.storage_access.auto_grants", false],
+ ["dom.storage_access.max_concurrent_auto_grants", 1],
+ ],
+ });
+ let tab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: TEST_TOP_PAGE,
+ });
+ let browser = tab.linkedBrowser;
+ await SpecialPowers.spawn(browser, [TEST_4TH_PARTY_DOMAIN], async tp => {
+ await SpecialPowers.pushPermissions([
+ {
+ type: "AllowStorageAccessRequest^http://example.com",
+ allow: 1,
+ context: content.document,
+ },
+ {
+ type: "3rdPartyStorage^http://not-tracking.example.com",
+ allow: 1,
+ context: content.document,
+ },
+ ]);
+ SpecialPowers.wrap(content.document).notifyUserGestureActivation();
+ var p = content.document.completeStorageAccessRequestFromSite(tp);
+ try {
+ await p;
+ ok(true, "Must resolve.");
+ } catch {
+ ok(false, "Must not reject.");
+ }
+ });
+ await BrowserTestUtils.removeTab(tab);
+ }
+);
+
+add_task(async () => {
+ Services.perms.removeAll();
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+});
diff --git a/toolkit/components/antitracking/test/browser/browser_storageAccess_TopLevel_UserActivation.js b/toolkit/components/antitracking/test/browser/browser_storageAccess_TopLevel_UserActivation.js
new file mode 100644
index 0000000000..49dd73fbf7
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_storageAccess_TopLevel_UserActivation.js
@@ -0,0 +1,63 @@
+add_task(async function testUserActivations() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.storage_access.enabled", true],
+ ["dom.storage_access.forward_declared.enabled", true],
+ ["network.cookie.cookieBehavior", BEHAVIOR_ACCEPT],
+ ["dom.storage_access.auto_grants", false],
+ ["dom.storage_access.max_concurrent_auto_grants", 1],
+ ],
+ });
+ // Part 1: open the embedded site as a top level
+ let tab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: TEST_3RD_PARTY_PAGE,
+ });
+ let browser = tab.linkedBrowser;
+ await SpecialPowers.spawn(browser, [TEST_DOMAIN], async tp => {
+ // Part 2: requestStorageAccessUnderSite without activation
+ var p = content.document.requestStorageAccessUnderSite(tp);
+ try {
+ await p;
+ ok(false, "Must not resolve without user activation.");
+ } catch {
+ ok(true, "Must reject without user activation.");
+ }
+ // Part 3: requestStorageAccessUnderSite with activation
+ SpecialPowers.wrap(content.document).notifyUserGestureActivation();
+ p = content.document.requestStorageAccessUnderSite(tp);
+ try {
+ await p;
+ ok(true, "Must resolve with user activation and autogrant.");
+ } catch {
+ ok(false, "Must not reject with user activation.");
+ }
+ });
+ // Part 4: open the embedding site as a top level
+ let tab2 = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: TEST_TOP_PAGE,
+ });
+ let browser2 = tab2.linkedBrowser;
+ await SpecialPowers.spawn(browser2, [TEST_3RD_PARTY_DOMAIN], async tp => {
+ // Part 5: completeStorageAccessRequestFromSite without activation
+ var p = content.document.completeStorageAccessRequestFromSite(tp);
+ try {
+ await p;
+ ok(true, "Must resolve without user activation.");
+ } catch {
+ ok(false, "Must not reject without user activation in this context.");
+ }
+ });
+ await BrowserTestUtils.removeTab(tab);
+ await BrowserTestUtils.removeTab(tab2);
+});
+
+add_task(async () => {
+ Services.perms.removeAll();
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+});
diff --git a/toolkit/components/antitracking/test/browser/browser_subResources.js b/toolkit/components/antitracking/test/browser/browser_subResources.js
new file mode 100644
index 0000000000..4841527d19
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_subResources.js
@@ -0,0 +1,277 @@
+add_task(async function () {
+ info("Starting subResources test");
+
+ await SpecialPowers.flushPrefEnv();
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [
+ "network.cookie.cookieBehavior",
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER,
+ ],
+ [
+ "network.cookie.cookieBehavior.pbmode",
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER,
+ ],
+ ["privacy.trackingprotection.enabled", false],
+ ["privacy.trackingprotection.pbmode.enabled", false],
+ ["privacy.trackingprotection.annotate_channels", true],
+ ["privacy.partition.network_state", false],
+ [
+ "privacy.restrict3rdpartystorage.userInteractionRequiredForHosts",
+ "tracking.example.com,tracking.example.org",
+ ],
+ // Bug 1617611: Fix all the tests broken by "cookies SameSite=lax by default"
+ ["network.cookie.sameSite.laxByDefault", false],
+ ],
+ });
+
+ await UrlClassifierTestUtils.addTestTrackers();
+
+ info("Creating a new tab");
+ let tab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE);
+ gBrowser.selectedTab = tab;
+
+ let browser = gBrowser.getBrowserForTab(tab);
+ await BrowserTestUtils.browserLoaded(browser);
+
+ info("Loading tracking scripts and tracking images");
+ await SpecialPowers.spawn(browser, [], async function () {
+ // Let's load the script twice here.
+ {
+ let src = content.document.createElement("script");
+ let p = new content.Promise(resolve => {
+ src.onload = resolve;
+ });
+ content.document.body.appendChild(src);
+ src.src =
+ "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/subResources.sjs?what=script";
+ await p;
+ }
+ {
+ let src = content.document.createElement("script");
+ let p = new content.Promise(resolve => {
+ src.onload = resolve;
+ });
+ content.document.body.appendChild(src);
+ src.src =
+ "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/subResources.sjs?what=script";
+ await p;
+ }
+
+ // Let's load an image twice here.
+ {
+ let img = content.document.createElement("img");
+ let p = new content.Promise(resolve => {
+ img.onload = resolve;
+ });
+ content.document.body.appendChild(img);
+ img.src =
+ "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/subResources.sjs?what=image";
+ await p;
+ }
+ {
+ let img = content.document.createElement("img");
+ let p = new content.Promise(resolve => {
+ img.onload = resolve;
+ });
+ content.document.body.appendChild(img);
+ img.src =
+ "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/subResources.sjs?what=image";
+ await p;
+ }
+ });
+
+ await fetch(
+ "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/subResources.sjs?result&what=image"
+ )
+ .then(r => r.text())
+ .then(text => {
+ is(text, "0", "Cookies received for images");
+ });
+
+ await fetch(
+ "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/subResources.sjs?result&what=script"
+ )
+ .then(r => r.text())
+ .then(text => {
+ is(text, "0", "Cookies received for scripts");
+ });
+
+ info("Creating a 3rd party content");
+ await SpecialPowers.spawn(
+ browser,
+ [
+ {
+ page: TEST_3RD_PARTY_PAGE_WO,
+ blockingCallback: (async _ => {}).toString(),
+ nonBlockingCallback: (async _ => {}).toString(),
+ },
+ ],
+ async function (obj) {
+ await new content.Promise(resolve => {
+ let ifr = content.document.createElement("iframe");
+ ifr.onload = function () {
+ info("Sending code to the 3rd party content");
+ ifr.contentWindow.postMessage(obj, "*");
+ };
+
+ content.addEventListener("message", function msg(event) {
+ if (event.data.type == "finish") {
+ content.removeEventListener("message", msg);
+ resolve();
+ return;
+ }
+
+ if (event.data.type == "ok") {
+ ok(event.data.what, event.data.msg);
+ return;
+ }
+
+ if (event.data.type == "info") {
+ info(event.data.msg);
+ return;
+ }
+
+ ok(false, "Unknown message");
+ });
+
+ content.document.body.appendChild(ifr);
+ ifr.src = obj.page;
+ });
+ }
+ );
+
+ info("Loading tracking scripts and tracking images again");
+ await SpecialPowers.spawn(browser, [], async function () {
+ // Let's load the script twice here.
+ {
+ let src = content.document.createElement("script");
+ let p = new content.Promise(resolve => {
+ src.onload = resolve;
+ });
+ content.document.body.appendChild(src);
+ src.src =
+ "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/subResources.sjs?what=script";
+ await p;
+ }
+ {
+ let src = content.document.createElement("script");
+ let p = new content.Promise(resolve => {
+ src.onload = resolve;
+ });
+ content.document.body.appendChild(src);
+ src.src =
+ "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/subResources.sjs?what=script";
+ await p;
+ }
+
+ // Let's load an image twice here.
+ {
+ let img = content.document.createElement("img");
+ let p = new content.Promise(resolve => {
+ img.onload = resolve;
+ });
+ content.document.body.appendChild(img);
+ img.src =
+ "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/subResources.sjs?what=image";
+ await p;
+ }
+ {
+ let img = content.document.createElement("img");
+ let p = new content.Promise(resolve => {
+ img.onload = resolve;
+ });
+ content.document.body.appendChild(img);
+ img.src =
+ "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/subResources.sjs?what=image";
+ await p;
+ }
+ });
+
+ await fetch(
+ "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/subResources.sjs?result&what=image"
+ )
+ .then(r => r.text())
+ .then(text => {
+ is(text, "1", "One cookie received for images.");
+ });
+
+ await fetch(
+ "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/subResources.sjs?result&what=script"
+ )
+ .then(r => r.text())
+ .then(text => {
+ is(text, "1", "One cookie received received for scripts.");
+ });
+
+ let expectTrackerBlocked = (item, blocked) => {
+ is(
+ item[0],
+ Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER,
+ "Correct blocking type reported"
+ );
+ is(item[1], blocked, "Correct blocking status reported");
+ ok(item[2] >= 1, "Correct repeat count reported");
+ };
+
+ let expectTrackerFound = item => {
+ is(
+ item[0],
+ Ci.nsIWebProgressListener.STATE_LOADED_LEVEL_1_TRACKING_CONTENT,
+ "Correct blocking type reported"
+ );
+ is(item[1], true, "Correct blocking status reported");
+ ok(item[2] >= 1, "Correct repeat count reported");
+ };
+
+ let expectCookiesLoaded = item => {
+ is(
+ item[0],
+ Ci.nsIWebProgressListener.STATE_COOKIES_LOADED,
+ "Correct blocking type reported"
+ );
+ is(item[1], true, "Correct blocking status reported");
+ ok(item[2] >= 1, "Correct repeat count reported");
+ };
+
+ let expectTrackerCookiesLoaded = item => {
+ is(
+ item[0],
+ Ci.nsIWebProgressListener.STATE_COOKIES_LOADED_TRACKER,
+ "Correct blocking type reported"
+ );
+ is(item[1], true, "Correct blocking status reported");
+ ok(item[2] >= 1, "Correct repeat count reported");
+ };
+
+ let log = JSON.parse(await browser.getContentBlockingLog());
+ for (let trackerOrigin in log) {
+ is(
+ trackerOrigin + "/",
+ TEST_3RD_PARTY_DOMAIN,
+ "Correct tracker origin must be reported"
+ );
+ let originLog = log[trackerOrigin];
+ is(originLog.length, 5, "We should have 4 entries in the compressed log");
+ expectTrackerFound(originLog[0]);
+ expectCookiesLoaded(originLog[1]);
+ expectTrackerCookiesLoaded(originLog[2]);
+ expectTrackerBlocked(originLog[3], true);
+ expectTrackerBlocked(originLog[4], false);
+ }
+
+ info("Removing the tab");
+ BrowserTestUtils.removeTab(tab);
+
+ UrlClassifierTestUtils.cleanupTestTrackers();
+});
+
+add_task(async function () {
+ info("Cleaning up.");
+ SpecialPowers.clearUserPref("network.cookie.sameSite.laxByDefault");
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+});
diff --git a/toolkit/components/antitracking/test/browser/browser_subResourcesPartitioned.js b/toolkit/components/antitracking/test/browser/browser_subResourcesPartitioned.js
new file mode 100644
index 0000000000..b2de150075
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_subResourcesPartitioned.js
@@ -0,0 +1,308 @@
+async function runTests(topPage, limitForeignContexts) {
+ info("Creating a new tab");
+ let tab = BrowserTestUtils.addTab(gBrowser, topPage);
+ gBrowser.selectedTab = tab;
+
+ let browser = gBrowser.getBrowserForTab(tab);
+ await BrowserTestUtils.browserLoaded(browser);
+
+ info("Loading scripts and images");
+ await SpecialPowers.spawn(browser, [], async function () {
+ // Let's load the script twice here.
+ {
+ let src = content.document.createElement("script");
+ let p = new content.Promise(resolve => {
+ src.onload = resolve;
+ });
+ content.document.body.appendChild(src);
+ src.src =
+ "https://example.org/browser/toolkit/components/antitracking/test/browser/subResources.sjs?what=script";
+ await p;
+ }
+ {
+ let src = content.document.createElement("script");
+ let p = new content.Promise(resolve => {
+ src.onload = resolve;
+ });
+ content.document.body.appendChild(src);
+ src.src =
+ "https://example.org/browser/toolkit/components/antitracking/test/browser/subResources.sjs?what=script";
+ await p;
+ }
+
+ // Let's load an image twice here.
+ {
+ let img = content.document.createElement("img");
+ let p = new content.Promise(resolve => {
+ img.onload = resolve;
+ });
+ content.document.body.appendChild(img);
+ img.src =
+ "https://example.org/browser/toolkit/components/antitracking/test/browser/subResources.sjs?what=image";
+ await p;
+ }
+ {
+ let img = content.document.createElement("img");
+ let p = new content.Promise(resolve => {
+ img.onload = resolve;
+ });
+ content.document.body.appendChild(img);
+ img.src =
+ "https://example.org/browser/toolkit/components/antitracking/test/browser/subResources.sjs?what=image";
+ await p;
+ }
+ });
+
+ await fetch(
+ "https://example.org/browser/toolkit/components/antitracking/test/browser/subResources.sjs?result&what=image"
+ )
+ .then(r => r.text())
+ .then(text => {
+ if (limitForeignContexts) {
+ is(text, "0", "No cookie received for images.");
+ } else {
+ is(text, "1", "One cookie received for images.");
+ }
+ });
+
+ await fetch(
+ "https://example.org/browser/toolkit/components/antitracking/test/browser/subResources.sjs?result&what=script"
+ )
+ .then(r => r.text())
+ .then(text => {
+ if (limitForeignContexts) {
+ is(text, "0", "No cookie received received for scripts.");
+ } else {
+ is(text, "1", "One cookie received received for scripts.");
+ }
+ });
+
+ info("Creating a 3rd party content");
+ await SpecialPowers.spawn(
+ browser,
+ [
+ {
+ page: TEST_3RD_PARTY_PAGE_WO,
+ blockingCallback: (async _ => {}).toString(),
+ nonBlockingCallback: (async _ => {}).toString(),
+ },
+ ],
+ async function (obj) {
+ await new content.Promise(resolve => {
+ let ifr = content.document.createElement("iframe");
+ ifr.onload = function () {
+ info("Sending code to the 3rd party content");
+ ifr.contentWindow.postMessage(obj, "*");
+ };
+
+ content.addEventListener("message", function msg(event) {
+ if (event.data.type == "finish") {
+ content.removeEventListener("message", msg);
+ resolve();
+ return;
+ }
+
+ if (event.data.type == "ok") {
+ ok(event.data.what, event.data.msg);
+ return;
+ }
+
+ if (event.data.type == "info") {
+ info(event.data.msg);
+ return;
+ }
+
+ ok(false, "Unknown message");
+ });
+
+ content.document.body.appendChild(ifr);
+ ifr.src = obj.page;
+ });
+ }
+ );
+
+ info("Loading scripts and images again");
+ await SpecialPowers.spawn(browser, [], async function () {
+ // Let's load the script twice here.
+ {
+ let src = content.document.createElement("script");
+ let p = new content.Promise(resolve => {
+ src.onload = resolve;
+ });
+ content.document.body.appendChild(src);
+ src.src =
+ "https://example.com/browser/toolkit/components/antitracking/test/browser/subResources.sjs?what=script";
+ await p;
+ }
+ {
+ let src = content.document.createElement("script");
+ let p = new content.Promise(resolve => {
+ src.onload = resolve;
+ });
+ content.document.body.appendChild(src);
+ src.src =
+ "https://example.com/browser/toolkit/components/antitracking/test/browser/subResources.sjs?what=script";
+ await p;
+ }
+
+ // Let's load an image twice here.
+ {
+ let img = content.document.createElement("img");
+ let p = new content.Promise(resolve => {
+ img.onload = resolve;
+ });
+ content.document.body.appendChild(img);
+ img.src =
+ "https://example.com/browser/toolkit/components/antitracking/test/browser/subResources.sjs?what=image";
+ await p;
+ }
+ {
+ let img = content.document.createElement("img");
+ let p = new content.Promise(resolve => {
+ img.onload = resolve;
+ });
+ content.document.body.appendChild(img);
+ img.src =
+ "https://example.com/browser/toolkit/components/antitracking/test/browser/subResources.sjs?what=image";
+ await p;
+ }
+ });
+
+ await fetch(
+ "https://example.com/browser/toolkit/components/antitracking/test/browser/subResources.sjs?result&what=image"
+ )
+ .then(r => r.text())
+ .then(text => {
+ if (limitForeignContexts) {
+ is(text, "0", "No cookie received for images.");
+ } else {
+ is(text, "1", "One cookie received for images.");
+ }
+ });
+
+ await fetch(
+ "https://example.com/browser/toolkit/components/antitracking/test/browser/subResources.sjs?result&what=script"
+ )
+ .then(r => r.text())
+ .then(text => {
+ if (limitForeignContexts) {
+ is(text, "0", "No cookie received received for scripts.");
+ } else {
+ is(text, "1", "One cookie received received for scripts.");
+ }
+ });
+
+ let expectTrackerBlocked = (item, blocked) => {
+ is(
+ item[0],
+ Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER,
+ "Correct blocking type reported"
+ );
+ is(item[1], blocked, "Correct blocking status reported");
+ ok(item[2] >= 1, "Correct repeat count reported");
+ };
+
+ let expectCookiesLoaded = item => {
+ is(
+ item[0],
+ Ci.nsIWebProgressListener.STATE_COOKIES_LOADED,
+ "Correct blocking type reported"
+ );
+ is(item[1], true, "Correct blocking status reported");
+ ok(item[2] >= 1, "Correct repeat count reported");
+ };
+
+ let expectCookiesBlockedForeign = item => {
+ is(
+ item[0],
+ Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_FOREIGN,
+ "Correct blocking type reported"
+ );
+ is(item[1], true, "Correct blocking status reported");
+ ok(item[2] >= 1, "Correct repeat count reported");
+ };
+
+ let log = JSON.parse(await browser.getContentBlockingLog());
+ for (let trackerOrigin in log) {
+ let originLog = log[trackerOrigin];
+ info(trackerOrigin);
+ switch (trackerOrigin) {
+ case "https://example.org":
+ case "https://example.com":
+ let numEntries = 1;
+ if (limitForeignContexts) {
+ ++numEntries;
+ }
+ is(
+ originLog.length,
+ numEntries,
+ `We should have ${numEntries} entries in the compressed log`
+ );
+ expectCookiesLoaded(originLog[0]);
+ if (limitForeignContexts) {
+ expectCookiesBlockedForeign(originLog[1]);
+ }
+ break;
+ case "https://tracking.example.org":
+ is(
+ originLog.length,
+ 1,
+ "We should have 1 entries in the compressed log"
+ );
+ expectTrackerBlocked(originLog[0], false);
+ break;
+ }
+ }
+
+ info("Removing the tab");
+ BrowserTestUtils.removeTab(tab);
+}
+
+add_task(async function () {
+ info("Starting subResources test");
+
+ await SpecialPowers.flushPrefEnv();
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [
+ "network.cookie.cookieBehavior",
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN,
+ ],
+ [
+ "network.cookie.cookieBehavior.pbmode",
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN,
+ ],
+ ["privacy.trackingprotection.enabled", false],
+ ["privacy.trackingprotection.pbmode.enabled", false],
+ ["privacy.trackingprotection.annotate_channels", true],
+ // Bug 1617611: Fix all the tests broken by "cookies SameSite=lax by default"
+ ["network.cookie.sameSite.laxByDefault", false],
+ [
+ "privacy.partition.always_partition_third_party_non_cookie_storage",
+ false,
+ ],
+ ],
+ });
+
+ for (let limitForeignContexts of [false, true]) {
+ SpecialPowers.setBoolPref(
+ "privacy.dynamic_firstparty.limitForeign",
+ limitForeignContexts
+ );
+ for (let page of [TEST_TOP_PAGE, TEST_TOP_PAGE_2, TEST_TOP_PAGE_3]) {
+ await runTests(page, limitForeignContexts);
+ }
+ }
+
+ SpecialPowers.clearUserPref("privacy.dynamic_firstparty.limitForeign");
+ SpecialPowers.clearUserPref("network.cookie.sameSite.laxByDefault");
+});
+
+add_task(async function () {
+ info("Cleaning up.");
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+});
diff --git a/toolkit/components/antitracking/test/browser/browser_subResourcesPartitioned_alwaysPartition.js b/toolkit/components/antitracking/test/browser/browser_subResourcesPartitioned_alwaysPartition.js
new file mode 100644
index 0000000000..53a90854b3
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_subResourcesPartitioned_alwaysPartition.js
@@ -0,0 +1,313 @@
+async function runTests(topPage, limitForeignContexts) {
+ info("Creating a new tab");
+ let tab = BrowserTestUtils.addTab(gBrowser, topPage);
+ gBrowser.selectedTab = tab;
+
+ let browser = gBrowser.getBrowserForTab(tab);
+ await BrowserTestUtils.browserLoaded(browser);
+
+ info("Loading scripts and images");
+ await SpecialPowers.spawn(browser, [], async function () {
+ // Let's load the script twice here.
+ {
+ let src = content.document.createElement("script");
+ let p = new content.Promise(resolve => {
+ src.onload = resolve;
+ });
+ content.document.body.appendChild(src);
+ src.src =
+ "https://example.org/browser/toolkit/components/antitracking/test/browser/subResources.sjs?what=script";
+ await p;
+ }
+ {
+ let src = content.document.createElement("script");
+ let p = new content.Promise(resolve => {
+ src.onload = resolve;
+ });
+ content.document.body.appendChild(src);
+ src.src =
+ "https://example.org/browser/toolkit/components/antitracking/test/browser/subResources.sjs?what=script";
+ await p;
+ }
+
+ // Let's load an image twice here.
+ {
+ let img = content.document.createElement("img");
+ let p = new content.Promise(resolve => {
+ img.onload = resolve;
+ });
+ content.document.body.appendChild(img);
+ img.src =
+ "https://example.org/browser/toolkit/components/antitracking/test/browser/subResources.sjs?what=image";
+ await p;
+ }
+ {
+ let img = content.document.createElement("img");
+ let p = new content.Promise(resolve => {
+ img.onload = resolve;
+ });
+ content.document.body.appendChild(img);
+ img.src =
+ "https://example.org/browser/toolkit/components/antitracking/test/browser/subResources.sjs?what=image";
+ await p;
+ }
+ });
+
+ await fetch(
+ "https://example.org/browser/toolkit/components/antitracking/test/browser/subResources.sjs?result&what=image"
+ )
+ .then(r => r.text())
+ .then(text => {
+ if (limitForeignContexts) {
+ is(text, "0", "No cookie received for images.");
+ } else {
+ is(text, "1", "One cookie received for images.");
+ }
+ });
+
+ await fetch(
+ "https://example.org/browser/toolkit/components/antitracking/test/browser/subResources.sjs?result&what=script"
+ )
+ .then(r => r.text())
+ .then(text => {
+ if (limitForeignContexts) {
+ is(text, "0", "No cookie received received for scripts.");
+ } else {
+ is(text, "1", "One cookie received received for scripts.");
+ }
+ });
+
+ info("Creating a 3rd party content");
+ await SpecialPowers.spawn(
+ browser,
+ [
+ {
+ page: TEST_3RD_PARTY_PAGE_WO,
+ blockingCallback: (async _ => {}).toString(),
+ nonBlockingCallback: (async _ => {}).toString(),
+ },
+ ],
+ async function (obj) {
+ await new content.Promise(resolve => {
+ let ifr = content.document.createElement("iframe");
+ ifr.onload = function () {
+ info("Sending code to the 3rd party content");
+ ifr.contentWindow.postMessage(obj, "*");
+ };
+
+ content.addEventListener("message", function msg(event) {
+ if (event.data.type == "finish") {
+ content.removeEventListener("message", msg);
+ resolve();
+ return;
+ }
+
+ if (event.data.type == "ok") {
+ ok(event.data.what, event.data.msg);
+ return;
+ }
+
+ if (event.data.type == "info") {
+ info(event.data.msg);
+ return;
+ }
+
+ ok(false, "Unknown message");
+ });
+
+ content.document.body.appendChild(ifr);
+ ifr.src = obj.page;
+ });
+ }
+ );
+
+ info("Loading scripts and images again");
+ await SpecialPowers.spawn(browser, [], async function () {
+ // Let's load the script twice here.
+ {
+ let src = content.document.createElement("script");
+ let p = new content.Promise(resolve => {
+ src.onload = resolve;
+ });
+ content.document.body.appendChild(src);
+ src.src =
+ "https://example.com/browser/toolkit/components/antitracking/test/browser/subResources.sjs?what=script";
+ await p;
+ }
+ {
+ let src = content.document.createElement("script");
+ let p = new content.Promise(resolve => {
+ src.onload = resolve;
+ });
+ content.document.body.appendChild(src);
+ src.src =
+ "https://example.com/browser/toolkit/components/antitracking/test/browser/subResources.sjs?what=script";
+ await p;
+ }
+
+ // Let's load an image twice here.
+ {
+ let img = content.document.createElement("img");
+ let p = new content.Promise(resolve => {
+ img.onload = resolve;
+ });
+ content.document.body.appendChild(img);
+ img.src =
+ "https://example.com/browser/toolkit/components/antitracking/test/browser/subResources.sjs?what=image";
+ await p;
+ }
+ {
+ let img = content.document.createElement("img");
+ let p = new content.Promise(resolve => {
+ img.onload = resolve;
+ });
+ content.document.body.appendChild(img);
+ img.src =
+ "https://example.com/browser/toolkit/components/antitracking/test/browser/subResources.sjs?what=image";
+ await p;
+ }
+ });
+
+ await fetch(
+ "https://example.com/browser/toolkit/components/antitracking/test/browser/subResources.sjs?result&what=image"
+ )
+ .then(r => r.text())
+ .then(text => {
+ if (limitForeignContexts) {
+ is(text, "0", "No cookie received for images.");
+ } else {
+ is(text, "1", "One cookie received for images.");
+ }
+ });
+
+ await fetch(
+ "https://example.com/browser/toolkit/components/antitracking/test/browser/subResources.sjs?result&what=script"
+ )
+ .then(r => r.text())
+ .then(text => {
+ if (limitForeignContexts) {
+ is(text, "0", "No cookie received received for scripts.");
+ } else {
+ is(text, "1", "One cookie received received for scripts.");
+ }
+ });
+
+ let expectTrackerBlocked = (item, blocked, type) => {
+ is(item[0], type, "Correct blocking type reported");
+ is(item[1], blocked, "Correct blocking status reported");
+ ok(item[2] >= 1, "Correct repeat count reported");
+ };
+
+ let expectCookiesLoaded = item => {
+ is(
+ item[0],
+ Ci.nsIWebProgressListener.STATE_COOKIES_LOADED,
+ "Correct blocking type reported"
+ );
+ is(item[1], true, "Correct blocking status reported");
+ ok(item[2] >= 1, "Correct repeat count reported");
+ };
+
+ let expectCookiesBlockedForeign = item => {
+ is(
+ item[0],
+ Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_FOREIGN,
+ "Correct blocking type reported"
+ );
+ is(item[1], true, "Correct blocking status reported");
+ ok(item[2] >= 1, "Correct repeat count reported");
+ };
+
+ let log = JSON.parse(await browser.getContentBlockingLog());
+ for (let trackerOrigin in log) {
+ let originLog = log[trackerOrigin];
+ info(trackerOrigin);
+ switch (trackerOrigin) {
+ case "https://example.org":
+ case "https://example.com":
+ let numEntries = 1;
+ if (limitForeignContexts) {
+ ++numEntries;
+ }
+ is(
+ originLog.length,
+ numEntries,
+ `We should have ${numEntries} entries in the compressed log`
+ );
+ expectCookiesLoaded(originLog[0]);
+ if (limitForeignContexts) {
+ expectCookiesBlockedForeign(originLog[1]);
+ }
+ break;
+ case "https://tracking.example.org":
+ is(
+ originLog.length,
+ 2,
+ "We should have 2 entries in the compressed log"
+ );
+ expectTrackerBlocked(
+ originLog[0],
+ true,
+ Ci.nsIWebProgressListener.STATE_COOKIES_LOADED
+ );
+ expectTrackerBlocked(
+ originLog[1],
+ false,
+ Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER
+ );
+ break;
+ }
+ }
+
+ info("Removing the tab");
+ BrowserTestUtils.removeTab(tab);
+}
+
+add_task(async function () {
+ info("Starting subResources test");
+
+ await SpecialPowers.flushPrefEnv();
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [
+ "network.cookie.cookieBehavior",
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN,
+ ],
+ [
+ "network.cookie.cookieBehavior.pbmode",
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN,
+ ],
+ ["privacy.trackingprotection.enabled", false],
+ ["privacy.trackingprotection.pbmode.enabled", false],
+ ["privacy.trackingprotection.annotate_channels", true],
+ // Bug 1617611: Fix all the tests broken by "cookies SameSite=lax by default"
+ ["network.cookie.sameSite.laxByDefault", false],
+ [
+ "privacy.partition.always_partition_third_party_non_cookie_storage",
+ true,
+ ],
+ ],
+ });
+
+ for (let limitForeignContexts of [false, true]) {
+ SpecialPowers.setBoolPref(
+ "privacy.dynamic_firstparty.limitForeign",
+ limitForeignContexts
+ );
+ for (let page of [TEST_TOP_PAGE, TEST_TOP_PAGE_2, TEST_TOP_PAGE_3]) {
+ await runTests(page, limitForeignContexts);
+ }
+ }
+
+ SpecialPowers.clearUserPref("privacy.dynamic_firstparty.limitForeign");
+ SpecialPowers.clearUserPref("network.cookie.sameSite.laxByDefault");
+});
+
+add_task(async function () {
+ info("Cleaning up.");
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+});
diff --git a/toolkit/components/antitracking/test/browser/browser_thirdPartyStorageRejectionForCORS.js b/toolkit/components/antitracking/test/browser/browser_thirdPartyStorageRejectionForCORS.js
new file mode 100644
index 0000000000..b415594662
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_thirdPartyStorageRejectionForCORS.js
@@ -0,0 +1,96 @@
+// This test works by setting up an exception for the tracker domain, which
+// disables all the anti-tracking tests.
+
+add_task(async _ => {
+ PermissionTestUtils.add(
+ "http://example.net",
+ "cookie",
+ Services.perms.ALLOW_ACTION
+ );
+
+ registerCleanupFunction(_ => {
+ Services.perms.removeAll();
+ });
+});
+
+AntiTracking._createTask({
+ name: "Test that we don't store 3P cookies from non-anonymous CORS XHR",
+ cookieBehavior: BEHAVIOR_REJECT_FOREIGN,
+ blockingByContentBlockingRTUI: false,
+ allowList: false,
+ thirdPartyPage: TEST_DOMAIN + TEST_PATH + "3rdParty.html",
+ callback: async _ => {
+ await new Promise(resolve => {
+ const xhr = new XMLHttpRequest();
+ xhr.open(
+ "GET",
+ "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/cookiesCORS.sjs?some;max-age=999999",
+ true
+ );
+ xhr.withCredentials = true;
+ xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
+ xhr.onreadystatechange = _ => {
+ if (4 === xhr.readyState && 200 === xhr.status) {
+ resolve();
+ }
+ };
+ xhr.send();
+ });
+ },
+ extraPrefs: [["network.cookie.rejectForeignWithExceptions.enabled", true]],
+ expectedBlockingNotifications:
+ Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_FOREIGN,
+ runInPrivateWindow: false,
+ iframeSandbox: null,
+ accessRemoval: null,
+ callbackAfterRemoval: null,
+});
+
+add_task(async _ => {
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+});
+
+AntiTracking._createTask({
+ name: "Test that we don't store 3P cookies from non-anonymous CORS XHR",
+ cookieBehavior: BEHAVIOR_REJECT_FOREIGN,
+ blockingByContentBlockingRTUI: false,
+ allowList: false,
+ thirdPartyPage: TEST_DOMAIN + TEST_PATH + "3rdParty.html",
+ callback: async _ => {
+ await new Promise(resolve => {
+ const xhr = new XMLHttpRequest();
+ xhr.open(
+ "GET",
+ "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/cookiesCORS.sjs?some;max-age=999999",
+ true
+ );
+ xhr.withCredentials = true;
+ xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
+ xhr.onreadystatechange = _ => {
+ if (4 === xhr.readyState && 200 === xhr.status) {
+ resolve();
+ }
+ };
+ xhr.send();
+ });
+ },
+ extraPrefs: [["network.cookie.rejectForeignWithExceptions.enabled", false]],
+ expectedBlockingNotifications:
+ Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_FOREIGN,
+ runInPrivateWindow: false,
+ iframeSandbox: null,
+ accessRemoval: null,
+ callbackAfterRemoval: null,
+});
+
+add_task(async _ => {
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+});
diff --git a/toolkit/components/antitracking/test/browser/browser_urlDecorationStripping.js b/toolkit/components/antitracking/test/browser/browser_urlDecorationStripping.js
new file mode 100644
index 0000000000..642b5d2cbd
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_urlDecorationStripping.js
@@ -0,0 +1,253 @@
+// This test ensures that the URL decoration annotations service works as
+// expected, and also we successfully downgrade document.referrer to the
+// eTLD+1 URL when tracking identifiers controlled by this service are
+// present in the referrer URI.
+
+"use strict";
+
+const { RemoteSettings } = ChromeUtils.importESModule(
+ "resource://services-settings/remote-settings.sys.mjs"
+);
+const { Preferences } = ChromeUtils.importESModule(
+ "resource://gre/modules/Preferences.sys.mjs"
+);
+
+const APS_PREF =
+ "privacy.partition.always_partition_third_party_non_cookie_storage";
+
+const COLLECTION_NAME = "anti-tracking-url-decoration";
+const PREF_NAME = "privacy.restrict3rdpartystorage.url_decorations";
+const TOKEN_1 = "fooBar";
+const TOKEN_2 = "foobaz";
+const TOKEN_3 = "fooqux";
+const TOKEN_4 = "bazqux";
+
+const token_1 = TOKEN_1.toLowerCase();
+
+const DOMAIN = TEST_DOMAIN_3;
+const SUB_DOMAIN = "https://sub1.xn--hxajbheg2az3al.xn--jxalpdlp/";
+const TOP_PAGE_WITHOUT_TRACKING_IDENTIFIER =
+ SUB_DOMAIN + TEST_PATH + "page.html";
+const TOP_PAGE_WITH_TRACKING_IDENTIFIER =
+ TOP_PAGE_WITHOUT_TRACKING_IDENTIFIER + "?" + TOKEN_1 + "=123";
+
+add_task(async _ => {
+ let uds = Cc["@mozilla.org/tracking-url-decoration-service;1"].getService(
+ Ci.nsIURLDecorationAnnotationsService
+ );
+
+ let records = [
+ {
+ id: "1",
+ last_modified: 1000000000000001,
+ schema: Date.now(),
+ token: TOKEN_1,
+ },
+ ];
+
+ // Add some initial data
+ async function emitSync() {
+ await RemoteSettings(COLLECTION_NAME).emit("sync", {
+ data: { current: records },
+ });
+ }
+ let db = RemoteSettings(COLLECTION_NAME).db;
+ await db.importChanges({}, Date.now(), [records[0]]);
+ await emitSync();
+
+ await uds.ensureUpdated();
+
+ let list = Preferences.get(PREF_NAME).split(" ");
+ ok(list.includes(TOKEN_1), "Token must now be available in " + PREF_NAME);
+ ok(Preferences.locked(PREF_NAME), PREF_NAME + " must be locked");
+
+ async function verifyList(array, not_array) {
+ await emitSync();
+
+ await uds.ensureUpdated();
+
+ list = Preferences.get(PREF_NAME).split(" ");
+ for (let token of array) {
+ ok(
+ list.includes(token),
+ token + " must now be available in " + PREF_NAME
+ );
+ }
+ for (let token of not_array) {
+ ok(
+ !list.includes(token),
+ token + " must not be available in " + PREF_NAME
+ );
+ }
+ ok(Preferences.locked(PREF_NAME), PREF_NAME + " must be locked");
+ }
+
+ records.push(
+ {
+ id: "2",
+ last_modified: 1000000000000002,
+ schema: Date.now(),
+ token: TOKEN_2,
+ },
+ {
+ id: "3",
+ last_modified: 1000000000000003,
+ schema: Date.now(),
+ token: TOKEN_3,
+ },
+ {
+ id: "4",
+ last_modified: 1000000000000005,
+ schema: Date.now(),
+ token: TOKEN_4,
+ }
+ );
+
+ await verifyList([TOKEN_1, TOKEN_2, TOKEN_3, TOKEN_4], []);
+
+ records.pop();
+
+ await verifyList([TOKEN_1, TOKEN_2, TOKEN_3], [TOKEN_4]);
+
+ is(
+ Services.eTLD.getBaseDomain(Services.io.newURI(DOMAIN)),
+ Services.eTLD.getBaseDomain(Services.io.newURI(SUB_DOMAIN)),
+ "Sanity check"
+ );
+
+ registerCleanupFunction(async _ => {
+ records = [];
+ await db.clear();
+ await emitSync();
+ });
+});
+
+AntiTracking._createTask({
+ name: "Test that we do not downgrade document.referrer when it does not contain a tracking identifier",
+ cookieBehavior: BEHAVIOR_REJECT_TRACKER,
+ blockingByContentBlockingRTUI: true,
+ allowList: false,
+ callback: async _ => {
+ let ref = new URL(document.referrer);
+ is(
+ ref.hostname,
+ "sub1.xn--hxajbheg2az3al.xn--jxalpdlp",
+ "Hostname shouldn't be stripped"
+ );
+ ok(ref.pathname.length > 1, "Path must not be trimmed");
+ // eslint-disable-next-line no-unused-vars
+ for (let entry of ref.searchParams.entries()) {
+ ok(false, "No query parameters should be found");
+ }
+ },
+ extraPrefs: [
+ ["network.http.referer.defaultPolicy", 3], // Ensure we don't downgrade because of the default policy.
+ ["network.http.referer.defaultPolicy.trackers", 3],
+ [APS_PREF, false],
+ ],
+ expectedBlockingNotifications: 0,
+ runInPrivateWindow: false,
+ iframeSandbox: null,
+ accessRemoval: null,
+ callbackAfterRemoval: null,
+ topPage: TOP_PAGE_WITHOUT_TRACKING_IDENTIFIER,
+});
+
+AntiTracking._createTask({
+ name: "Test that we do not downgrade document.referrer when it does not contain a tracking identifier even though it gets downgraded to origin only due to the default referrer policy",
+ cookieBehavior: BEHAVIOR_REJECT_TRACKER,
+ blockingByContentBlockingRTUI: true,
+ allowList: false,
+ callback: async _ => {
+ let ref = new URL(document.referrer);
+ is(
+ ref.hostname,
+ "sub1.xn--hxajbheg2az3al.xn--jxalpdlp",
+ "Hostname shouldn't be stripped"
+ );
+ is(ref.pathname.length, 1, "Path must be trimmed");
+ // eslint-disable-next-line no-unused-vars
+ for (let entry of ref.searchParams.entries()) {
+ ok(false, "No query parameters should be found");
+ }
+ },
+ extraPrefs: [
+ ["network.http.referer.defaultPolicy.trackers", 2],
+ [APS_PREF, false],
+ ],
+ expectedBlockingNotifications: 0,
+ runInPrivateWindow: false,
+ iframeSandbox: null,
+ accessRemoval: null,
+ callbackAfterRemoval: null,
+ topPage: TOP_PAGE_WITHOUT_TRACKING_IDENTIFIER,
+});
+
+AntiTracking._createTask({
+ name: "Test that we downgrade document.referrer when it contains a tracking identifier",
+ cookieBehavior: BEHAVIOR_REJECT_TRACKER,
+ blockingByContentBlockingRTUI: true,
+ allowList: false,
+ callback: async _ => {
+ let ref = new URL(document.referrer);
+ is(
+ ref.hostname,
+ "xn--hxajbheg2az3al.xn--jxalpdlp",
+ "Hostname should be stripped"
+ );
+ is(ref.pathname.length, 1, "Path must be trimmed");
+ // eslint-disable-next-line no-unused-vars
+ for (let entry of ref.searchParams.entries()) {
+ ok(false, "No query parameters should be found");
+ }
+ },
+ extraPrefs: [
+ ["network.http.referer.defaultPolicy", 3], // Ensure we don't downgrade because of the default policy.
+ ["network.http.referer.defaultPolicy.trackers", 3],
+ [APS_PREF, false],
+ ],
+ expectedBlockingNotifications: 0,
+ runInPrivateWindow: false,
+ iframeSandbox: null,
+ accessRemoval: null,
+ callbackAfterRemoval: null,
+ topPage: TOP_PAGE_WITH_TRACKING_IDENTIFIER,
+});
+
+AntiTracking._createTask({
+ name: "Test that we don't downgrade document.referrer when it contains a tracking identifier if it gets downgraded to origin only due to the default referrer policy because the tracking identifier wouldn't be present in the referrer any more",
+ cookieBehavior: BEHAVIOR_REJECT_TRACKER,
+ blockingByContentBlockingRTUI: true,
+ allowList: false,
+ callback: async _ => {
+ let ref = new URL(document.referrer);
+ is(
+ ref.hostname,
+ "sub1.xn--hxajbheg2az3al.xn--jxalpdlp",
+ "Hostname shouldn't be stripped"
+ );
+ is(ref.pathname.length, 1, "Path must be trimmed");
+ // eslint-disable-next-line no-unused-vars
+ for (let entry of ref.searchParams.entries()) {
+ ok(false, "No query parameters should be found");
+ }
+ },
+ extraPrefs: [
+ ["network.http.referer.defaultPolicy.trackers", 2],
+ [APS_PREF, false],
+ ],
+ expectedBlockingNotifications: 0,
+ runInPrivateWindow: false,
+ iframeSandbox: null,
+ accessRemoval: null,
+ callbackAfterRemoval: null,
+ topPage: TOP_PAGE_WITH_TRACKING_IDENTIFIER,
+});
+
+add_task(async _ => {
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+});
diff --git a/toolkit/components/antitracking/test/browser/browser_urlDecorationStripping_alwaysPartition.js b/toolkit/components/antitracking/test/browser/browser_urlDecorationStripping_alwaysPartition.js
new file mode 100644
index 0000000000..2fbac9811b
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_urlDecorationStripping_alwaysPartition.js
@@ -0,0 +1,255 @@
+// This test ensures that the URL decoration annotations service works as
+// expected, and also we successfully downgrade document.referrer to the
+// eTLD+1 URL when tracking identifiers controlled by this service are
+// present in the referrer URI.
+
+"use strict";
+
+const trackerBlocked = Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER;
+
+const { RemoteSettings } = ChromeUtils.importESModule(
+ "resource://services-settings/remote-settings.sys.mjs"
+);
+const { Preferences } = ChromeUtils.importESModule(
+ "resource://gre/modules/Preferences.sys.mjs"
+);
+
+const COLLECTION_NAME = "anti-tracking-url-decoration";
+const PREF_NAME = "privacy.restrict3rdpartystorage.url_decorations";
+const TOKEN_1 = "fooBar";
+const TOKEN_2 = "foobaz";
+const TOKEN_3 = "fooqux";
+const TOKEN_4 = "bazqux";
+
+const APS_PREF =
+ "privacy.partition.always_partition_third_party_non_cookie_storage";
+
+const token_1 = TOKEN_1.toLowerCase();
+
+const DOMAIN = TEST_DOMAIN_3;
+const SUB_DOMAIN = "https://sub1.xn--hxajbheg2az3al.xn--jxalpdlp/";
+const TOP_PAGE_WITHOUT_TRACKING_IDENTIFIER =
+ SUB_DOMAIN + TEST_PATH + "page.html";
+const TOP_PAGE_WITH_TRACKING_IDENTIFIER =
+ TOP_PAGE_WITHOUT_TRACKING_IDENTIFIER + "?" + TOKEN_1 + "=123";
+
+add_task(async _ => {
+ let uds = Cc["@mozilla.org/tracking-url-decoration-service;1"].getService(
+ Ci.nsIURLDecorationAnnotationsService
+ );
+
+ let records = [
+ {
+ id: "1",
+ last_modified: 1000000000000001,
+ schema: Date.now(),
+ token: TOKEN_1,
+ },
+ ];
+
+ // Add some initial data
+ async function emitSync() {
+ await RemoteSettings(COLLECTION_NAME).emit("sync", {
+ data: { current: records },
+ });
+ }
+ let db = RemoteSettings(COLLECTION_NAME).db;
+ await db.importChanges({}, Date.now(), [records[0]]);
+ await emitSync();
+
+ await uds.ensureUpdated();
+
+ let list = Preferences.get(PREF_NAME).split(" ");
+ ok(list.includes(TOKEN_1), "Token must now be available in " + PREF_NAME);
+ ok(Preferences.locked(PREF_NAME), PREF_NAME + " must be locked");
+
+ async function verifyList(array, not_array) {
+ await emitSync();
+
+ await uds.ensureUpdated();
+
+ list = Preferences.get(PREF_NAME).split(" ");
+ for (let token of array) {
+ ok(
+ list.includes(token),
+ token + " must now be available in " + PREF_NAME
+ );
+ }
+ for (let token of not_array) {
+ ok(
+ !list.includes(token),
+ token + " must not be available in " + PREF_NAME
+ );
+ }
+ ok(Preferences.locked(PREF_NAME), PREF_NAME + " must be locked");
+ }
+
+ records.push(
+ {
+ id: "2",
+ last_modified: 1000000000000002,
+ schema: Date.now(),
+ token: TOKEN_2,
+ },
+ {
+ id: "3",
+ last_modified: 1000000000000003,
+ schema: Date.now(),
+ token: TOKEN_3,
+ },
+ {
+ id: "4",
+ last_modified: 1000000000000005,
+ schema: Date.now(),
+ token: TOKEN_4,
+ }
+ );
+
+ await verifyList([TOKEN_1, TOKEN_2, TOKEN_3, TOKEN_4], []);
+
+ records.pop();
+
+ await verifyList([TOKEN_1, TOKEN_2, TOKEN_3], [TOKEN_4]);
+
+ is(
+ Services.eTLD.getBaseDomain(Services.io.newURI(DOMAIN)),
+ Services.eTLD.getBaseDomain(Services.io.newURI(SUB_DOMAIN)),
+ "Sanity check"
+ );
+
+ registerCleanupFunction(async _ => {
+ records = [];
+ await db.clear();
+ await emitSync();
+ });
+});
+
+AntiTracking._createTask({
+ name: "Test that we do not downgrade document.referrer when it does not contain a tracking identifier",
+ cookieBehavior: BEHAVIOR_REJECT_TRACKER,
+ blockingByContentBlockingRTUI: true,
+ allowList: false,
+ callback: async _ => {
+ let ref = new URL(document.referrer);
+ is(
+ ref.hostname,
+ "sub1.xn--hxajbheg2az3al.xn--jxalpdlp",
+ "Hostname shouldn't be stripped"
+ );
+ ok(ref.pathname.length > 1, "Path must not be trimmed");
+ // eslint-disable-next-line no-unused-vars
+ for (let entry of ref.searchParams.entries()) {
+ ok(false, "No query parameters should be found");
+ }
+ },
+ extraPrefs: [
+ ["network.http.referer.defaultPolicy", 3], // Ensure we don't downgrade because of the default policy.
+ ["network.http.referer.defaultPolicy.trackers", 3],
+ [APS_PREF, true],
+ ],
+ expectedBlockingNotifications: trackerBlocked,
+ runInPrivateWindow: false,
+ iframeSandbox: null,
+ accessRemoval: null,
+ callbackAfterRemoval: null,
+ topPage: TOP_PAGE_WITHOUT_TRACKING_IDENTIFIER,
+});
+
+AntiTracking._createTask({
+ name: "Test that we do not downgrade document.referrer when it does not contain a tracking identifier even though it gets downgraded to origin only due to the default referrer policy",
+ cookieBehavior: BEHAVIOR_REJECT_TRACKER,
+ blockingByContentBlockingRTUI: true,
+ allowList: false,
+ callback: async _ => {
+ let ref = new URL(document.referrer);
+ is(
+ ref.hostname,
+ "sub1.xn--hxajbheg2az3al.xn--jxalpdlp",
+ "Hostname shouldn't be stripped"
+ );
+ is(ref.pathname.length, 1, "Path must be trimmed");
+ // eslint-disable-next-line no-unused-vars
+ for (let entry of ref.searchParams.entries()) {
+ ok(false, "No query parameters should be found");
+ }
+ },
+ extraPrefs: [
+ ["network.http.referer.defaultPolicy.trackers", 2],
+ [APS_PREF, true],
+ ],
+ expectedBlockingNotifications: trackerBlocked,
+ runInPrivateWindow: false,
+ iframeSandbox: null,
+ accessRemoval: null,
+ callbackAfterRemoval: null,
+ topPage: TOP_PAGE_WITHOUT_TRACKING_IDENTIFIER,
+});
+
+AntiTracking._createTask({
+ name: "Test that we downgrade document.referrer when it contains a tracking identifier",
+ cookieBehavior: BEHAVIOR_REJECT_TRACKER,
+ blockingByContentBlockingRTUI: true,
+ allowList: false,
+ callback: async _ => {
+ let ref = new URL(document.referrer);
+ is(
+ ref.hostname,
+ "xn--hxajbheg2az3al.xn--jxalpdlp",
+ "Hostname should be stripped"
+ );
+ is(ref.pathname.length, 1, "Path must be trimmed");
+ // eslint-disable-next-line no-unused-vars
+ for (let entry of ref.searchParams.entries()) {
+ ok(false, "No query parameters should be found");
+ }
+ },
+ extraPrefs: [
+ ["network.http.referer.defaultPolicy", 3], // Ensure we don't downgrade because of the default policy.
+ ["network.http.referer.defaultPolicy.trackers", 3],
+ [APS_PREF, true],
+ ],
+ expectedBlockingNotifications: trackerBlocked,
+ runInPrivateWindow: false,
+ iframeSandbox: null,
+ accessRemoval: null,
+ callbackAfterRemoval: null,
+ topPage: TOP_PAGE_WITH_TRACKING_IDENTIFIER,
+});
+
+AntiTracking._createTask({
+ name: "Test that we don't downgrade document.referrer when it contains a tracking identifier if it gets downgraded to origin only due to the default referrer policy because the tracking identifier wouldn't be present in the referrer any more",
+ cookieBehavior: BEHAVIOR_REJECT_TRACKER,
+ blockingByContentBlockingRTUI: true,
+ allowList: false,
+ callback: async _ => {
+ let ref = new URL(document.referrer);
+ is(
+ ref.hostname,
+ "sub1.xn--hxajbheg2az3al.xn--jxalpdlp",
+ "Hostname shouldn't be stripped"
+ );
+ is(ref.pathname.length, 1, "Path must be trimmed");
+ // eslint-disable-next-line no-unused-vars
+ for (let entry of ref.searchParams.entries()) {
+ ok(false, "No query parameters should be found");
+ }
+ },
+ extraPrefs: [
+ ["network.http.referer.defaultPolicy.trackers", 2],
+ [APS_PREF, true],
+ ],
+ expectedBlockingNotifications: trackerBlocked,
+ runInPrivateWindow: false,
+ iframeSandbox: null,
+ accessRemoval: null,
+ callbackAfterRemoval: null,
+ topPage: TOP_PAGE_WITH_TRACKING_IDENTIFIER,
+});
+
+add_task(async _ => {
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+});
diff --git a/toolkit/components/antitracking/test/browser/browser_urlQueryStringStripping.js b/toolkit/components/antitracking/test/browser/browser_urlQueryStringStripping.js
new file mode 100644
index 0000000000..6395110f41
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_urlQueryStringStripping.js
@@ -0,0 +1,855 @@
+/* vim: set ts=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/. */
+
+"use strict";
+
+requestLongerTimeout(6);
+
+const TEST_THIRD_PARTY_DOMAIN = TEST_DOMAIN_2;
+const TEST_THIRD_PARTY_SUB_DOMAIN = "http://sub1.xn--exmple-cua.test/";
+
+const TEST_URI = TEST_DOMAIN + TEST_PATH + "file_stripping.html";
+const TEST_THIRD_PARTY_URI =
+ TEST_THIRD_PARTY_DOMAIN + TEST_PATH + "file_stripping.html";
+const TEST_REDIRECT_URI = TEST_DOMAIN + TEST_PATH + "redirect.sjs";
+
+const TEST_CASES = [
+ { testQueryString: "paramToStrip1=123", strippedQueryString: "" },
+ {
+ testQueryString: "PARAMTOSTRIP1=123&paramToStrip2=456",
+ strippedQueryString: "",
+ },
+ {
+ testQueryString: "paramToStrip1=123&paramToKeep=456",
+ strippedQueryString: "paramToKeep=456",
+ },
+ {
+ testQueryString: "paramToStrip1=123&paramToKeep=456&paramToStrip2=abc",
+ strippedQueryString: "paramToKeep=456",
+ },
+ {
+ testQueryString: "paramToKeep=123",
+ strippedQueryString: "paramToKeep=123",
+ },
+ // Test to make sure we don't encode the unstripped parameters.
+ {
+ testQueryString: "paramToStrip1=123&paramToKeep=?$!%",
+ strippedQueryString: "paramToKeep=?$!%",
+ },
+];
+
+let listService;
+
+function observeChannel(uri, expected) {
+ return TestUtils.topicObserved("http-on-modify-request", (subject, data) => {
+ let channel = subject.QueryInterface(Ci.nsIHttpChannel);
+ let channelURI = channel.URI;
+
+ if (channelURI.spec.startsWith(uri)) {
+ is(
+ channelURI.query,
+ expected,
+ "The loading channel has the expected query string."
+ );
+ return true;
+ }
+
+ return false;
+ });
+}
+
+async function verifyQueryString(browser, expected) {
+ await SpecialPowers.spawn(browser, [expected], expected => {
+ // Strip the first question mark.
+ let search = content.location.search.slice(1);
+
+ is(search, expected, "The query string is correct.");
+ });
+}
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["privacy.query_stripping.strip_list", "paramToStrip1 paramToStrip2"],
+ ["privacy.query_stripping.redirect", true],
+ ["privacy.query_stripping.listService.logLevel", "Debug"],
+ ["privacy.query_stripping.strip_on_share.enabled", false],
+ ],
+ });
+
+ // Get the list service so we can wait for it to be fully initialized before running tests.
+ listService = Cc["@mozilla.org/query-stripping-list-service;1"].getService(
+ Ci.nsIURLQueryStrippingListService
+ );
+ // Here we don't care about the actual enabled state, we just want any init to be done so we get reliable starting conditions.
+ await listService.testWaitForInit();
+});
+
+async function waitForListServiceInit(strippingEnabled) {
+ info("Waiting for nsIURLQueryStrippingListService to be initialized.");
+ let isInitialized = await listService.testWaitForInit();
+ is(
+ isInitialized,
+ strippingEnabled,
+ "nsIURLQueryStrippingListService should be initialized when the feature is enabled."
+ );
+}
+
+add_task(async function doTestsForTabOpen() {
+ info("Start testing query stripping for tab open.");
+ for (const strippingEnabled of [false, true]) {
+ await SpecialPowers.pushPrefEnv({
+ set: [["privacy.query_stripping.enabled", strippingEnabled]],
+ });
+ await waitForListServiceInit(strippingEnabled);
+
+ for (const test of TEST_CASES) {
+ let testURI = TEST_URI + "?" + test.testQueryString;
+
+ let expected = strippingEnabled
+ ? test.strippedQueryString
+ : test.testQueryString;
+
+ // Observe the channel and check if the query string is expected.
+ let networkPromise = observeChannel(TEST_URI, expected);
+
+ // Open a new tab.
+ await BrowserTestUtils.withNewTab(testURI, async browser => {
+ // Verify if the query string is expected in the new tab.
+ await verifyQueryString(browser, expected);
+ });
+
+ await networkPromise;
+ }
+
+ await SpecialPowers.popPrefEnv();
+ }
+});
+
+add_task(async function doTestsForWindowOpen() {
+ info("Start testing query stripping for window.open().");
+ for (const strippingEnabled of [false, true]) {
+ await SpecialPowers.pushPrefEnv({
+ set: [["privacy.query_stripping.enabled", strippingEnabled]],
+ });
+ await waitForListServiceInit(strippingEnabled);
+
+ for (const test of TEST_CASES) {
+ let testFirstPartyURI = TEST_URI + "?" + test.testQueryString;
+ let testThirdPartyURI = TEST_THIRD_PARTY_URI + "?" + test.testQueryString;
+
+ let originalQueryString = test.testQueryString;
+ let expectedQueryString = strippingEnabled
+ ? test.strippedQueryString
+ : test.testQueryString;
+
+ await BrowserTestUtils.withNewTab(TEST_URI, async browser => {
+ // Observe the channel and check if the query string is intact when open
+ // a same-origin URI.
+ let networkPromise = observeChannel(TEST_URI, originalQueryString);
+
+ // Create the promise to wait for the opened tab.
+ let newTabPromise = BrowserTestUtils.waitForNewTab(gBrowser, url => {
+ return url.startsWith(TEST_URI);
+ });
+
+ // Call window.open() to open the same-origin URI where the query string
+ // won't be stripped.
+ await SpecialPowers.spawn(browser, [testFirstPartyURI], async url => {
+ content.postMessage({ type: "window-open", url }, "*");
+ });
+
+ await networkPromise;
+ let newTab = await newTabPromise;
+
+ // Verify if the query string is expected in the new opened tab.
+ await verifyQueryString(newTab.linkedBrowser, originalQueryString);
+
+ BrowserTestUtils.removeTab(newTab);
+
+ // Observe the channel and check if the query string is expected for
+ // cross-origin URI.
+ networkPromise = observeChannel(
+ TEST_THIRD_PARTY_URI,
+ expectedQueryString
+ );
+
+ newTabPromise = BrowserTestUtils.waitForNewTab(gBrowser, url => {
+ return url.startsWith(TEST_THIRD_PARTY_URI);
+ });
+
+ // Call window.open() to open the cross-site URI where the query string
+ // could be stripped.
+ await SpecialPowers.spawn(browser, [testThirdPartyURI], async url => {
+ content.postMessage({ type: "window-open", url }, "*");
+ });
+
+ await networkPromise;
+ newTab = await newTabPromise;
+
+ // Verify if the query string is expected in the new opened tab.
+ await verifyQueryString(newTab.linkedBrowser, expectedQueryString);
+
+ BrowserTestUtils.removeTab(newTab);
+ });
+ }
+
+ await SpecialPowers.popPrefEnv();
+ }
+});
+
+add_task(async function doTestsForLinkClick() {
+ info("Start testing query stripping for link navigation.");
+ for (const strippingEnabled of [false, true]) {
+ await SpecialPowers.pushPrefEnv({
+ set: [["privacy.query_stripping.enabled", strippingEnabled]],
+ });
+ await waitForListServiceInit(strippingEnabled);
+
+ for (const test of TEST_CASES) {
+ let testFirstPartyURI = TEST_URI + "?" + test.testQueryString;
+ let testThirdPartyURI = TEST_THIRD_PARTY_URI + "?" + test.testQueryString;
+
+ let originalQueryString = test.testQueryString;
+ let expectedQueryString = strippingEnabled
+ ? test.strippedQueryString
+ : test.testQueryString;
+
+ await BrowserTestUtils.withNewTab(TEST_URI, async browser => {
+ // Observe the channel and check if the query string is intact when
+ // click a same-origin link.
+ let networkPromise = observeChannel(TEST_URI, originalQueryString);
+
+ // Create the promise to wait for the location change.
+ let locationChangePromise = BrowserTestUtils.waitForLocationChange(
+ gBrowser,
+ testFirstPartyURI
+ );
+
+ // Create a link and click it to navigate.
+ await SpecialPowers.spawn(browser, [testFirstPartyURI], async uri => {
+ let link = content.document.createElement("a");
+ link.setAttribute("href", uri);
+ link.textContent = "Link";
+ content.document.body.appendChild(link);
+ link.click();
+ });
+
+ await networkPromise;
+ await locationChangePromise;
+
+ // Verify the query string in the content window.
+ await verifyQueryString(browser, originalQueryString);
+
+ // Second, create a link to a cross-origin site to see if the query
+ // string is stripped as expected.
+
+ // Observe the channel and check if the query string is expected when
+ // click a cross-origin link.
+ networkPromise = observeChannel(
+ TEST_THIRD_PARTY_URI,
+ expectedQueryString
+ );
+
+ let targetURI = expectedQueryString
+ ? `${TEST_THIRD_PARTY_URI}?${expectedQueryString}`
+ : TEST_THIRD_PARTY_URI;
+ // Create the promise to wait for the location change.
+ locationChangePromise = BrowserTestUtils.waitForLocationChange(
+ gBrowser,
+ targetURI
+ );
+
+ // Create a cross-origin link and click it to navigate.
+ await SpecialPowers.spawn(browser, [testThirdPartyURI], async uri => {
+ let link = content.document.createElement("a");
+ link.setAttribute("href", uri);
+ link.textContent = "Link";
+ content.document.body.appendChild(link);
+ link.click();
+ });
+
+ await networkPromise;
+ await locationChangePromise;
+
+ // Verify the query string in the content window.
+ await verifyQueryString(browser, expectedQueryString);
+ });
+ }
+
+ await SpecialPowers.popPrefEnv();
+ }
+});
+
+add_task(async function doTestsForLinkClickInIframe() {
+ info("Start testing query stripping for link navigation in iframe.");
+ for (const strippingEnabled of [false, true]) {
+ await SpecialPowers.pushPrefEnv({
+ set: [["privacy.query_stripping.enabled", strippingEnabled]],
+ });
+ await waitForListServiceInit(strippingEnabled);
+
+ for (const test of TEST_CASES) {
+ let testFirstPartyURI = TEST_URI + "?" + test.testQueryString;
+ let testThirdPartyURI = TEST_THIRD_PARTY_URI + "?" + test.testQueryString;
+
+ let originalQueryString = test.testQueryString;
+ let expectedQueryString = strippingEnabled
+ ? test.strippedQueryString
+ : test.testQueryString;
+
+ await BrowserTestUtils.withNewTab(TEST_URI, async browser => {
+ // Create an iframe and wait until it has been loaded.
+ let iframeBC = await SpecialPowers.spawn(
+ browser,
+ [TEST_URI],
+ async url => {
+ let frame = content.document.createElement("iframe");
+ content.document.body.appendChild(frame);
+
+ await new Promise(done => {
+ frame.addEventListener(
+ "load",
+ function () {
+ done();
+ },
+ { capture: true, once: true }
+ );
+
+ frame.setAttribute("src", url);
+ });
+
+ return frame.browsingContext;
+ }
+ );
+
+ // Observe the channel and check if the query string is intact when
+ // click a same-origin link.
+ let networkPromise = observeChannel(TEST_URI, originalQueryString);
+
+ // Create the promise to wait for the new tab.
+ let newTabPromise = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ testFirstPartyURI
+ );
+
+ // Create a same-site link which has '_blank' as target in the iframe
+ // and click it to navigate.
+ await SpecialPowers.spawn(iframeBC, [testFirstPartyURI], async uri => {
+ let link = content.document.createElement("a");
+ link.setAttribute("href", uri);
+ link.setAttribute("target", "_blank");
+ link.textContent = "Link";
+ content.document.body.appendChild(link);
+ link.click();
+ });
+
+ await networkPromise;
+ let newOpenedTab = await newTabPromise;
+
+ // Verify the query string in the content window.
+ await verifyQueryString(
+ newOpenedTab.linkedBrowser,
+ originalQueryString
+ );
+ BrowserTestUtils.removeTab(newOpenedTab);
+
+ // Second, create a link to a cross-origin site in the iframe to see if
+ // the query string is stripped as expected.
+
+ // Observe the channel and check if the query string is expected when
+ // click a cross-origin link.
+ networkPromise = observeChannel(
+ TEST_THIRD_PARTY_URI,
+ expectedQueryString
+ );
+
+ let targetURI = expectedQueryString
+ ? `${TEST_THIRD_PARTY_URI}?${expectedQueryString}`
+ : TEST_THIRD_PARTY_URI;
+ // Create the promise to wait for the new tab.
+ newTabPromise = BrowserTestUtils.waitForNewTab(gBrowser, targetURI);
+
+ // Create a cross-origin link which has '_blank' as target in the iframe
+ // and click it to navigate.
+ await SpecialPowers.spawn(iframeBC, [testThirdPartyURI], async uri => {
+ let link = content.document.createElement("a");
+ link.setAttribute("href", uri);
+ link.setAttribute("target", "_blank");
+ link.textContent = "Link";
+ content.document.body.appendChild(link);
+ link.click();
+ });
+
+ await networkPromise;
+ newOpenedTab = await newTabPromise;
+
+ // Verify the query string in the content window.
+ await verifyQueryString(
+ newOpenedTab.linkedBrowser,
+ expectedQueryString
+ );
+ BrowserTestUtils.removeTab(newOpenedTab);
+ });
+ }
+
+ await SpecialPowers.popPrefEnv();
+ }
+});
+
+add_task(async function doTestsForScriptNavigation() {
+ info("Start testing query stripping for script navigation.");
+ for (const strippingEnabled of [false, true]) {
+ await SpecialPowers.pushPrefEnv({
+ set: [["privacy.query_stripping.enabled", strippingEnabled]],
+ });
+ await waitForListServiceInit(strippingEnabled);
+
+ for (const test of TEST_CASES) {
+ let testFirstPartyURI = TEST_URI + "?" + test.testQueryString;
+ let testThirdPartyURI = TEST_THIRD_PARTY_URI + "?" + test.testQueryString;
+
+ let originalQueryString = test.testQueryString;
+ let expectedQueryString = strippingEnabled
+ ? test.strippedQueryString
+ : test.testQueryString;
+
+ await BrowserTestUtils.withNewTab(TEST_URI, async browser => {
+ // Observe the channel and check if the query string is intact when
+ // navigating to a same-origin URI via script.
+ let networkPromise = observeChannel(TEST_URI, originalQueryString);
+
+ // Create the promise to wait for the location change.
+ let locationChangePromise = BrowserTestUtils.waitForLocationChange(
+ gBrowser,
+ testFirstPartyURI
+ );
+
+ // Trigger the navigation by script.
+ await SpecialPowers.spawn(browser, [testFirstPartyURI], async url => {
+ content.postMessage({ type: "script", url }, "*");
+ });
+
+ await networkPromise;
+ await locationChangePromise;
+
+ // Verify the query string in the content window.
+ await verifyQueryString(browser, originalQueryString);
+
+ // Second, trigger a cross-origin navigation through script to see if
+ // the query string is stripped as expected.
+
+ let targetURI = expectedQueryString
+ ? `${TEST_THIRD_PARTY_URI}?${expectedQueryString}`
+ : TEST_THIRD_PARTY_URI;
+
+ // Observe the channel and check if the query string is expected.
+ networkPromise = observeChannel(
+ TEST_THIRD_PARTY_URI,
+ expectedQueryString
+ );
+
+ locationChangePromise = BrowserTestUtils.waitForLocationChange(
+ gBrowser,
+ targetURI
+ );
+
+ // Trigger the cross-origin navigation by script.
+ await SpecialPowers.spawn(browser, [testThirdPartyURI], async url => {
+ content.postMessage({ type: "script", url }, "*");
+ });
+
+ await networkPromise;
+ await locationChangePromise;
+
+ // Verify the query string in the content window.
+ await verifyQueryString(browser, expectedQueryString);
+ });
+ }
+
+ await SpecialPowers.popPrefEnv();
+ }
+});
+
+add_task(async function doTestsForNoStrippingForIframeNavigation() {
+ info("Start testing no query stripping for iframe navigation.");
+
+ for (const strippingEnabled of [false, true]) {
+ await SpecialPowers.pushPrefEnv({
+ set: [["privacy.query_stripping.enabled", strippingEnabled]],
+ });
+ await waitForListServiceInit(strippingEnabled);
+
+ for (const test of TEST_CASES) {
+ let testFirstPartyURI = TEST_URI + "?" + test.testQueryString;
+ let testThirdPartyURI = TEST_THIRD_PARTY_URI + "?" + test.testQueryString;
+
+ // There should be no query stripping for the iframe navigation.
+ let originalQueryString = test.testQueryString;
+ let expectedQueryString = test.testQueryString;
+
+ await BrowserTestUtils.withNewTab(TEST_URI, async browser => {
+ // Create an iframe and wait until it has been loaded.
+ let iframeBC = await SpecialPowers.spawn(
+ browser,
+ [TEST_URI],
+ async url => {
+ let frame = content.document.createElement("iframe");
+ content.document.body.appendChild(frame);
+
+ await new Promise(done => {
+ frame.addEventListener(
+ "load",
+ function () {
+ done();
+ },
+ { capture: true, once: true }
+ );
+
+ frame.setAttribute("src", url);
+ });
+
+ return frame.browsingContext;
+ }
+ );
+
+ // Observe the channel and check if the query string is intact when
+ // navigating an iframe.
+ let networkPromise = observeChannel(TEST_URI, originalQueryString);
+
+ // Create the promise to wait for the location change.
+ let locationChangePromise = BrowserTestUtils.waitForLocationChange(
+ gBrowser,
+ testFirstPartyURI
+ );
+
+ // Trigger the iframe navigation by script.
+ await SpecialPowers.spawn(iframeBC, [testFirstPartyURI], async url => {
+ content.postMessage({ type: "script", url }, "*");
+ });
+
+ await networkPromise;
+ await locationChangePromise;
+
+ // Verify the query string in the iframe.
+ await verifyQueryString(iframeBC, originalQueryString);
+
+ // Second, trigger a cross-origin navigation through script to see if
+ // the query string is still the same.
+
+ let targetURI = expectedQueryString
+ ? `${TEST_THIRD_PARTY_URI}?${expectedQueryString}`
+ : TEST_THIRD_PARTY_URI;
+
+ // Observe the channel and check if the query string is not stripped.
+ networkPromise = observeChannel(
+ TEST_THIRD_PARTY_URI,
+ expectedQueryString
+ );
+
+ locationChangePromise = BrowserTestUtils.waitForLocationChange(
+ gBrowser,
+ targetURI
+ );
+
+ // Trigger the cross-origin iframe navigation by script.
+ await SpecialPowers.spawn(iframeBC, [testThirdPartyURI], async url => {
+ content.postMessage({ type: "script", url }, "*");
+ });
+
+ await networkPromise;
+ await locationChangePromise;
+
+ // Verify the query string in the content window.
+ await verifyQueryString(iframeBC, expectedQueryString);
+ });
+ }
+
+ await SpecialPowers.popPrefEnv();
+ }
+});
+
+add_task(async function doTestsForRedirect() {
+ info("Start testing query stripping for redirects.");
+
+ for (const strippingEnabled of [false, true]) {
+ await SpecialPowers.pushPrefEnv({
+ set: [["privacy.query_stripping.enabled", strippingEnabled]],
+ });
+ await waitForListServiceInit(strippingEnabled);
+
+ for (const test of TEST_CASES) {
+ let testFirstPartyURI =
+ TEST_REDIRECT_URI + "?" + TEST_URI + "?" + test.testQueryString;
+ let testThirdPartyURI = `${TEST_REDIRECT_URI}?${TEST_THIRD_PARTY_URI}?${test.testQueryString}`;
+
+ let originalQueryString = test.testQueryString;
+ let expectedQueryString = strippingEnabled
+ ? test.strippedQueryString
+ : test.testQueryString;
+
+ await BrowserTestUtils.withNewTab(TEST_URI, async browser => {
+ // Observe the channel and check if the query string is intact when
+ // redirecting to a same-origin URI .
+ let networkPromise = observeChannel(TEST_URI, originalQueryString);
+
+ let targetURI = `${TEST_URI}?${originalQueryString}`;
+
+ // Create the promise to wait for the location change.
+ let locationChangePromise = BrowserTestUtils.waitForLocationChange(
+ gBrowser,
+ targetURI
+ );
+
+ // Trigger the redirect.
+ await SpecialPowers.spawn(browser, [testFirstPartyURI], async url => {
+ content.postMessage({ type: "script", url }, "*");
+ });
+
+ await networkPromise;
+ await locationChangePromise;
+
+ // Verify the query string in the content window.
+ await verifyQueryString(browser, originalQueryString);
+
+ // Second, trigger a redirect to a cross-origin site where the query
+ // string should be stripped.
+
+ targetURI = expectedQueryString
+ ? `${TEST_THIRD_PARTY_URI}?${expectedQueryString}`
+ : TEST_THIRD_PARTY_URI;
+
+ // Observe the channel and check if the query string is expected.
+ networkPromise = observeChannel(
+ TEST_THIRD_PARTY_URI,
+ expectedQueryString
+ );
+
+ locationChangePromise = BrowserTestUtils.waitForLocationChange(
+ gBrowser,
+ targetURI
+ );
+
+ // Trigger the cross-origin redirect.
+ await SpecialPowers.spawn(browser, [testThirdPartyURI], async url => {
+ content.postMessage({ type: "script", url }, "*");
+ });
+
+ await networkPromise;
+ await locationChangePromise;
+
+ // Verify the query string in the content window.
+ await verifyQueryString(browser, expectedQueryString);
+ });
+ }
+
+ await SpecialPowers.popPrefEnv();
+ }
+});
+
+add_task(async function doTestForAllowList() {
+ info("Start testing query stripping allow list.");
+
+ // Enable the query stripping and set the allow list.
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["privacy.query_stripping.enabled", true],
+ ["privacy.query_stripping.allow_list", "xn--exmple-cua.test"],
+ ],
+ });
+ await waitForListServiceInit(true);
+
+ const expected = "paramToStrip1=123";
+
+ // Make sure the allow list works for sites, so we will test both the domain
+ // and the sub domain.
+ for (const domain of [TEST_THIRD_PARTY_DOMAIN, TEST_THIRD_PARTY_SUB_DOMAIN]) {
+ let testURI = `${domain}${TEST_PATH}file_stripping.html`;
+ let testURIWithQueryString = `${testURI}?${expected}`;
+
+ // 1. Test the allow list for tab open.
+ info("Run tab open test.");
+
+ // Observe the channel and check if the query string is not stripped.
+ let networkPromise = observeChannel(testURI, expected);
+
+ await BrowserTestUtils.withNewTab(testURIWithQueryString, async browser => {
+ // Verify if the query string is not stripped in the new tab.
+ await verifyQueryString(browser, expected);
+ });
+
+ await networkPromise;
+
+ // 2. Test the allow list for window open
+ info("Run window open test.");
+ await BrowserTestUtils.withNewTab(TEST_URI, async browser => {
+ // Observe the channel and check if the query string is not stripped.
+ let networkPromise = observeChannel(testURI, expected);
+
+ let newTabPromise = BrowserTestUtils.waitForNewTab(gBrowser, url => {
+ return url.startsWith(testURI);
+ });
+
+ await SpecialPowers.spawn(
+ browser,
+ [testURIWithQueryString],
+ async url => {
+ content.postMessage({ type: "window-open", url }, "*");
+ }
+ );
+
+ await networkPromise;
+ let newTab = await newTabPromise;
+
+ // Verify if the query string is not stripped in the new opened tab.
+ await verifyQueryString(newTab.linkedBrowser, expected);
+
+ BrowserTestUtils.removeTab(newTab);
+ });
+
+ // 3. Test the allow list for link click
+ info("Run link click test.");
+ await BrowserTestUtils.withNewTab(TEST_URI, async browser => {
+ // Observe the channel and check if the query string is not stripped.
+ let networkPromise = observeChannel(testURI, expected);
+
+ // Create the promise to wait for the location change.
+ let locationChangePromise = BrowserTestUtils.waitForLocationChange(
+ gBrowser,
+ testURIWithQueryString
+ );
+
+ await SpecialPowers.spawn(
+ browser,
+ [testURIWithQueryString],
+ async url => {
+ let link = content.document.createElement("a");
+ link.setAttribute("href", url);
+ link.textContent = "Link";
+ content.document.body.appendChild(link);
+ link.click();
+ }
+ );
+
+ await networkPromise;
+ await locationChangePromise;
+
+ // Verify the query string is not stripped in the content window.
+ await verifyQueryString(browser, expected);
+ });
+
+ // 4. Test the allow list for clicking link in an iframe.
+ info("Run link click in iframe test.");
+ await BrowserTestUtils.withNewTab(TEST_URI, async browser => {
+ // Create an iframe and wait until it has been loaded.
+ let iframeBC = await SpecialPowers.spawn(
+ browser,
+ [TEST_URI],
+ async url => {
+ let frame = content.document.createElement("iframe");
+ content.document.body.appendChild(frame);
+
+ await new Promise(done => {
+ frame.addEventListener(
+ "load",
+ function () {
+ done();
+ },
+ { capture: true, once: true }
+ );
+
+ frame.setAttribute("src", url);
+ });
+
+ return frame.browsingContext;
+ }
+ );
+
+ // Observe the channel and check if the query string is not stripped.
+ let networkPromise = observeChannel(testURI, expected);
+
+ // Create the promise to wait for the new tab.
+ let newTabPromise = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ testURIWithQueryString
+ );
+
+ // Create a same-site link which has '_blank' as target in the iframe
+ // and click it to navigate.
+ await SpecialPowers.spawn(
+ iframeBC,
+ [testURIWithQueryString],
+ async uri => {
+ let link = content.document.createElement("a");
+ link.setAttribute("href", uri);
+ link.setAttribute("target", "_blank");
+ link.textContent = "Link";
+ content.document.body.appendChild(link);
+ link.click();
+ }
+ );
+
+ await networkPromise;
+ let newOpenedTab = await newTabPromise;
+
+ // Verify the query string is not stripped in the content window.
+ await verifyQueryString(newOpenedTab.linkedBrowser, expected);
+ BrowserTestUtils.removeTab(newOpenedTab);
+
+ // 5. Test the allow list for script navigation.
+ info("Run script navigation test.");
+ await BrowserTestUtils.withNewTab(TEST_URI, async browser => {
+ // Observe the channel and check if the query string is not stripped.
+ let networkPromise = observeChannel(testURI, expected);
+
+ // Create the promise to wait for the location change.
+ let locationChangePromise = BrowserTestUtils.waitForLocationChange(
+ gBrowser,
+ testURIWithQueryString
+ );
+
+ await SpecialPowers.spawn(
+ browser,
+ [testURIWithQueryString],
+ async url => {
+ content.postMessage({ type: "script", url }, "*");
+ }
+ );
+
+ await networkPromise;
+ await locationChangePromise;
+
+ // Verify the query string is not stripped in the content window.
+ await verifyQueryString(browser, expected);
+ });
+
+ // 6. Test the allow list for redirect.
+ info("Run redirect test.");
+ await BrowserTestUtils.withNewTab(TEST_URI, async browser => {
+ // Observe the channel and check if the query string is not stripped.
+ let networkPromise = observeChannel(testURI, expected);
+
+ // Create the promise to wait for the location change.
+ let locationChangePromise = BrowserTestUtils.waitForLocationChange(
+ gBrowser,
+ testURIWithQueryString
+ );
+
+ let testRedirectURI = `${TEST_REDIRECT_URI}?${testURI}?${expected}`;
+
+ // Trigger the redirect.
+ await SpecialPowers.spawn(browser, [testRedirectURI], async url => {
+ content.postMessage({ type: "script", url }, "*");
+ });
+
+ await networkPromise;
+ await locationChangePromise;
+
+ // Verify the query string in the content window.
+ await verifyQueryString(browser, expected);
+ });
+ });
+ }
+});
diff --git a/toolkit/components/antitracking/test/browser/browser_urlQueryStringStripping_allowList.js b/toolkit/components/antitracking/test/browser/browser_urlQueryStringStripping_allowList.js
new file mode 100644
index 0000000000..6dee6cede0
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_urlQueryStringStripping_allowList.js
@@ -0,0 +1,442 @@
+/* vim: set ts=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/. */
+
+"use strict";
+
+const TEST_THIRD_PARTY_DOMAIN = TEST_DOMAIN_2;
+
+const TEST_URI = TEST_DOMAIN + TEST_PATH + "file_stripping.html";
+const TEST_THIRD_PARTY_URI =
+ TEST_THIRD_PARTY_DOMAIN + TEST_PATH + "file_stripping.html";
+const TEST_REDIRECT_URI = TEST_DOMAIN + TEST_PATH + "redirect.sjs";
+
+const TEST_QUERY_STRING = "paramToStrip=1";
+
+function observeChannel(uri, expected) {
+ return TestUtils.topicObserved("http-on-before-connect", (subject, data) => {
+ let channel = subject.QueryInterface(Ci.nsIHttpChannel);
+ let channelURI = channel.URI;
+
+ if (channelURI.spec.startsWith(uri)) {
+ is(
+ channelURI.query,
+ expected,
+ "The loading channel has the expected query string."
+ );
+ return true;
+ }
+
+ return false;
+ });
+}
+
+async function verifyQueryString(browser, expected) {
+ await SpecialPowers.spawn(browser, [expected], expected => {
+ // Strip the first question mark.
+ let search = content.location.search.slice(1);
+
+ is(search, expected, "The query string is correct.");
+ });
+}
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["privacy.query_stripping.strip_list", "paramToStrip"],
+ ["privacy.query_stripping.redirect", true],
+ ["privacy.query_stripping.enabled", true],
+ ],
+ });
+
+ let listService = Cc[
+ "@mozilla.org/query-stripping-list-service;1"
+ ].getService(Ci.nsIURLQueryStrippingListService);
+ await listService.testWaitForInit();
+});
+
+add_task(async function doTestsForTabOpen() {
+ let testURI = TEST_URI + "?" + TEST_QUERY_STRING;
+
+ // Observe the channel and check if the query string is stripped.
+ let networkPromise = observeChannel(TEST_URI, "");
+
+ // Open a new tab.
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, testURI);
+
+ // Verify if the query string is stripped.
+ await verifyQueryString(tab.linkedBrowser, "");
+ await networkPromise;
+
+ // Toggle ETP off and verify if the query string is restored.
+ let browserLoadedPromise = BrowserTestUtils.browserLoaded(
+ tab.linkedBrowser,
+ false,
+ testURI
+ );
+ // Observe the channel and check if the query string is not stripped.
+ networkPromise = observeChannel(TEST_URI, TEST_QUERY_STRING);
+
+ gProtectionsHandler.disableForCurrentPage();
+ await browserLoadedPromise;
+ await networkPromise;
+
+ await verifyQueryString(tab.linkedBrowser, TEST_QUERY_STRING);
+
+ BrowserTestUtils.removeTab(tab);
+
+ // Open the tab again and check if the query string is not stripped.
+ networkPromise = observeChannel(TEST_URI, TEST_QUERY_STRING);
+ tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, testURI);
+ await networkPromise;
+
+ // Verify if the query string is not stripped because it's in the content
+ // blocking allow list.
+ await verifyQueryString(tab.linkedBrowser, TEST_QUERY_STRING);
+
+ // Toggle ETP on and verify if the query string is stripped again.
+ networkPromise = observeChannel(TEST_URI, "");
+ browserLoadedPromise = BrowserTestUtils.browserLoaded(
+ tab.linkedBrowser,
+ false,
+ TEST_URI
+ );
+ gProtectionsHandler.enableForCurrentPage();
+ await browserLoadedPromise;
+ await networkPromise;
+
+ await verifyQueryString(tab.linkedBrowser, "");
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function doTestsForWindowOpen() {
+ let testURI = TEST_THIRD_PARTY_URI + "?" + TEST_QUERY_STRING;
+
+ await BrowserTestUtils.withNewTab(TEST_URI, async browser => {
+ // Observe the channel and check if the query string is stripped.
+ let networkPromise = observeChannel(TEST_THIRD_PARTY_URI, "");
+
+ // Create the promise to wait for the opened tab.
+ let newTabPromise = BrowserTestUtils.waitForNewTab(gBrowser, url => {
+ return url.startsWith(TEST_THIRD_PARTY_URI);
+ });
+
+ // Call window.open() to open the third-party URI.
+ await SpecialPowers.spawn(browser, [testURI], async url => {
+ content.postMessage({ type: "window-open", url }, "*");
+ });
+
+ await networkPromise;
+ let newTab = await newTabPromise;
+
+ // Verify if the query string is stripped in the new opened tab.
+ await verifyQueryString(newTab.linkedBrowser, "");
+
+ // Toggle ETP off and verify if the query string is restored.
+ let browserLoadedPromise = BrowserTestUtils.browserLoaded(
+ newTab.linkedBrowser,
+ false,
+ testURI
+ );
+ // Observe the channel and check if the query string is not stripped.
+ networkPromise = observeChannel(TEST_THIRD_PARTY_URI, TEST_QUERY_STRING);
+
+ gProtectionsHandler.disableForCurrentPage();
+ await browserLoadedPromise;
+ await networkPromise;
+
+ await verifyQueryString(newTab.linkedBrowser, TEST_QUERY_STRING);
+
+ BrowserTestUtils.removeTab(newTab);
+
+ // Call window.open() again to check if the query string is not stripped if
+ // it's in the content blocking allow list.
+ networkPromise = observeChannel(TEST_THIRD_PARTY_URI, TEST_QUERY_STRING);
+ newTabPromise = BrowserTestUtils.waitForNewTab(gBrowser, url => {
+ return url.startsWith(TEST_THIRD_PARTY_URI);
+ });
+
+ await SpecialPowers.spawn(browser, [testURI], async url => {
+ content.postMessage({ type: "window-open", url }, "*");
+ });
+
+ await networkPromise;
+ newTab = await newTabPromise;
+
+ // Verify if the query string is not stripped in the new opened tab.
+ await verifyQueryString(newTab.linkedBrowser, TEST_QUERY_STRING);
+
+ // Toggle ETP on and verify if the query string is stripped again.
+ networkPromise = observeChannel(TEST_THIRD_PARTY_URI, "");
+ browserLoadedPromise = BrowserTestUtils.browserLoaded(
+ newTab.linkedBrowser,
+ false,
+ TEST_THIRD_PARTY_URI
+ );
+ gProtectionsHandler.enableForCurrentPage();
+ await browserLoadedPromise;
+ await networkPromise;
+
+ await verifyQueryString(newTab.linkedBrowser, "");
+ BrowserTestUtils.removeTab(newTab);
+ });
+});
+
+add_task(async function doTestsForLinkClick() {
+ let testURI = TEST_THIRD_PARTY_URI + "?" + TEST_QUERY_STRING;
+
+ await BrowserTestUtils.withNewTab(TEST_URI, async browser => {
+ // Observe the channel and check if the query string is stripped.
+ let networkPromise = observeChannel(TEST_THIRD_PARTY_URI, "");
+
+ // Create the promise to wait for the location change.
+ let locationChangePromise = BrowserTestUtils.waitForLocationChange(
+ gBrowser,
+ TEST_THIRD_PARTY_URI
+ );
+
+ // Create a link and click it to navigate.
+ await SpecialPowers.spawn(browser, [testURI], async uri => {
+ let link = content.document.createElement("a");
+ link.setAttribute("href", uri);
+ link.textContent = "Link";
+ content.document.body.appendChild(link);
+ link.click();
+ });
+
+ await networkPromise;
+ await locationChangePromise;
+
+ // Verify the query string in the content window.
+ await verifyQueryString(browser, "");
+
+ // Toggle ETP off and verify if the query string is restored.
+ let browserLoadedPromise = BrowserTestUtils.browserLoaded(
+ browser,
+ false,
+ testURI
+ );
+ // Observe the channel and check if the query string is not stripped.
+ networkPromise = observeChannel(TEST_THIRD_PARTY_URI, TEST_QUERY_STRING);
+
+ gProtectionsHandler.disableForCurrentPage();
+ await browserLoadedPromise;
+ await networkPromise;
+
+ // Verify the query string in the content window.
+ await verifyQueryString(browser, TEST_QUERY_STRING);
+ });
+
+ // Repeat the test again to see if the query string is not stripped if it's in
+ // the content blocking allow list.
+ await BrowserTestUtils.withNewTab(TEST_URI, async browser => {
+ // Observe the channel and check if the query string is not stripped.
+ let networkPromise = observeChannel(
+ TEST_THIRD_PARTY_URI,
+ TEST_QUERY_STRING
+ );
+
+ // Create the promise to wait for the location change.
+ let locationChangePromise = BrowserTestUtils.waitForLocationChange(
+ gBrowser,
+ testURI
+ );
+
+ // Create a link and click it to navigate.
+ await SpecialPowers.spawn(browser, [testURI], async uri => {
+ let link = content.document.createElement("a");
+ link.setAttribute("href", uri);
+ link.textContent = "Link";
+ content.document.body.appendChild(link);
+ link.click();
+ });
+
+ await networkPromise;
+ await locationChangePromise;
+
+ // Verify the query string in the content window.
+ await verifyQueryString(browser, TEST_QUERY_STRING);
+
+ // Toggle ETP on and verify if the query string is stripped again.
+ let browserLoadedPromise = BrowserTestUtils.browserLoaded(
+ browser,
+ false,
+ TEST_THIRD_PARTY_URI
+ );
+ // Observe the channel and check if the query string is not stripped.
+ networkPromise = observeChannel(TEST_THIRD_PARTY_URI, "");
+
+ gProtectionsHandler.enableForCurrentPage();
+ await browserLoadedPromise;
+ await networkPromise;
+
+ // Verify the query string in the content window.
+ await verifyQueryString(browser, "");
+ });
+});
+
+add_task(async function doTestsForScriptNavigation() {
+ let testURI = TEST_THIRD_PARTY_URI + "?" + TEST_QUERY_STRING;
+
+ await BrowserTestUtils.withNewTab(TEST_URI, async browser => {
+ // Observe the channel and check if the query string is stripped.
+ let networkPromise = observeChannel(TEST_THIRD_PARTY_URI, "");
+
+ // Create the promise to wait for the location change.
+ let locationChangePromise = BrowserTestUtils.waitForLocationChange(
+ gBrowser,
+ TEST_THIRD_PARTY_URI
+ );
+
+ // Trigger the navigation by script.
+ await SpecialPowers.spawn(browser, [testURI], async url => {
+ content.postMessage({ type: "script", url }, "*");
+ });
+
+ await networkPromise;
+ await locationChangePromise;
+
+ // Verify the query string in the content window.
+ await verifyQueryString(browser, "");
+
+ // Toggle ETP off and verify if the query string is restored.
+ let browserLoadedPromise = BrowserTestUtils.browserLoaded(
+ browser,
+ false,
+ testURI
+ );
+ // Observe the channel and check if the query string is not stripped.
+ networkPromise = observeChannel(TEST_THIRD_PARTY_URI, TEST_QUERY_STRING);
+
+ gProtectionsHandler.disableForCurrentPage();
+ await browserLoadedPromise;
+ await networkPromise;
+
+ // Verify the query string in the content window.
+ await verifyQueryString(browser, TEST_QUERY_STRING);
+ });
+
+ // Repeat the test again to see if the query string is not stripped if it's in
+ // the content blocking allow list.
+ await BrowserTestUtils.withNewTab(TEST_URI, async browser => {
+ // Observe the channel and check if the query string is not stripped.
+ let networkPromise = observeChannel(
+ TEST_THIRD_PARTY_URI,
+ TEST_QUERY_STRING
+ );
+
+ // Create the promise to wait for the location change.
+ let locationChangePromise = BrowserTestUtils.waitForLocationChange(
+ gBrowser,
+ testURI
+ );
+
+ // Trigger the navigation by script.
+ await SpecialPowers.spawn(browser, [testURI], async url => {
+ content.postMessage({ type: "script", url }, "*");
+ });
+
+ await networkPromise;
+ await locationChangePromise;
+
+ // Verify the query string in the content window.
+ await verifyQueryString(browser, TEST_QUERY_STRING);
+
+ // Toggle ETP on and verify if the query string is stripped again.
+ let browserLoadedPromise = BrowserTestUtils.browserLoaded(
+ browser,
+ false,
+ TEST_THIRD_PARTY_URI
+ );
+ // Observe the channel and check if the query string is stripped.
+ networkPromise = observeChannel(TEST_THIRD_PARTY_URI, "");
+
+ gProtectionsHandler.enableForCurrentPage();
+ await browserLoadedPromise;
+ await networkPromise;
+
+ // Verify the query string in the content window.
+ await verifyQueryString(browser, "");
+ });
+});
+
+add_task(async function doTestsForRedirect() {
+ let testURI = `${TEST_REDIRECT_URI}?${TEST_THIRD_PARTY_URI}?${TEST_QUERY_STRING}`;
+ let resultURI = TEST_THIRD_PARTY_URI;
+ let resultURIWithQuery = `${TEST_THIRD_PARTY_URI}?${TEST_QUERY_STRING}`;
+
+ // Open a new tab.
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URI);
+
+ // Observe the channel and check if the query string is stripped.
+ let networkPromise = observeChannel(TEST_THIRD_PARTY_URI, "");
+
+ // Create the promise to wait for the location change.
+ let locationChangePromise = BrowserTestUtils.waitForLocationChange(
+ gBrowser,
+ resultURI
+ );
+
+ // Trigger the redirect.
+ await SpecialPowers.spawn(tab.linkedBrowser, [testURI], async url => {
+ content.postMessage({ type: "script", url }, "*");
+ });
+
+ await networkPromise;
+ await locationChangePromise;
+
+ // Verify the query string in the content window.
+ await verifyQueryString(tab.linkedBrowser, "");
+
+ // Toggle ETP off and verify if the query string is restored.
+ let browserLoadedPromise = BrowserTestUtils.browserLoaded(
+ tab.linkedBrowser,
+ false,
+ resultURIWithQuery
+ );
+ // Observe the channel and check if the query string is not stripped.
+ networkPromise = observeChannel(TEST_THIRD_PARTY_URI, TEST_QUERY_STRING);
+
+ gProtectionsHandler.disableForCurrentPage();
+ await browserLoadedPromise;
+ await networkPromise;
+
+ BrowserTestUtils.removeTab(tab);
+
+ // Open the tab again to check if the query string is not stripped.
+ networkPromise = observeChannel(TEST_THIRD_PARTY_URI, TEST_QUERY_STRING);
+ tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URI);
+
+ locationChangePromise = BrowserTestUtils.waitForLocationChange(
+ gBrowser,
+ resultURIWithQuery
+ );
+
+ // Trigger the redirect.
+ await SpecialPowers.spawn(tab.linkedBrowser, [testURI], async url => {
+ content.postMessage({ type: "script", url }, "*");
+ });
+
+ await networkPromise;
+ await locationChangePromise;
+
+ // Verify the query string in the content window.
+ await verifyQueryString(tab.linkedBrowser, TEST_QUERY_STRING);
+
+ // Toggle ETP on and verify if the query string is stripped again.
+ networkPromise = observeChannel(TEST_THIRD_PARTY_URI, "");
+ browserLoadedPromise = BrowserTestUtils.browserLoaded(
+ tab.linkedBrowser,
+ false,
+ resultURI
+ );
+ gProtectionsHandler.enableForCurrentPage();
+ await browserLoadedPromise;
+ await networkPromise;
+
+ await verifyQueryString(tab.linkedBrowser, "");
+
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/toolkit/components/antitracking/test/browser/browser_urlQueryStringStripping_nimbus.js b/toolkit/components/antitracking/test/browser/browser_urlQueryStringStripping_nimbus.js
new file mode 100644
index 0000000000..e5f256b870
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_urlQueryStringStripping_nimbus.js
@@ -0,0 +1,145 @@
+/* vim: set ts=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/. */
+
+"use strict";
+
+/**
+ * Simplified version of browser_urlQueryStripping.js to test that the Nimbus
+ * integration works correctly in both normal and private browsing.
+ */
+
+const { ExperimentFakes } = ChromeUtils.importESModule(
+ "resource://testing-common/NimbusTestUtils.sys.mjs"
+);
+
+const TEST_URI = TEST_DOMAIN + TEST_PATH + "file_stripping.html";
+const TEST_QUERY_STRING = "paramToStrip1=123&paramToKeep=456";
+const TEST_QUERY_STRING_STRIPPED = "paramToKeep=456";
+const TEST_URI_WITH_QUERY = TEST_URI + "?" + TEST_QUERY_STRING;
+
+let listService;
+
+async function waitForListServiceInit(strippingEnabled) {
+ info("Waiting for nsIURLQueryStrippingListService to be initialized.");
+ let isInitialized = await listService.testWaitForInit();
+ is(
+ isInitialized,
+ strippingEnabled,
+ "nsIURLQueryStrippingListService should be initialized when the feature is enabled."
+ );
+}
+
+/**
+ * Set a list of prefs on the default branch and restore the original values on test end.
+ * @param {*} prefs - Key value pairs in an array.
+ */
+function setDefaultPrefs(prefs) {
+ let originalValues = new Map();
+ let defaultPrefs = Services.prefs.getDefaultBranch("");
+
+ let prefValueToSetter = prefValue => {
+ let type = typeof prefValue;
+ if (type == "string") {
+ return defaultPrefs.setStringPref;
+ }
+ if (type == "boolean") {
+ return defaultPrefs.setBoolPref;
+ }
+ throw new Error("unexpected pref type");
+ };
+
+ prefs.forEach(([key, value]) => {
+ prefValueToSetter(value)(key, value);
+ originalValues.set(key, value);
+ });
+
+ registerCleanupFunction(function () {
+ prefs.forEach(([key, value]) => {
+ prefValueToSetter(value)(key, originalValues.get(key));
+ });
+ });
+}
+
+add_setup(async function () {
+ // Disable the feature via the default pref. This is required so we can set
+ // user values via Nimbus.
+ setDefaultPrefs([
+ ["privacy.query_stripping.enabled", false],
+ ["privacy.query_stripping.enabled.pbmode", false],
+ ["privacy.query_stripping.strip_list", ""],
+ ["privacy.query_stripping.strip_on_share.enabled", false],
+ ]);
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["privacy.query_stripping.listService.logLevel", "Debug"]],
+ });
+
+ // Get the list service so we can wait for it to be fully initialized before running tests.
+ listService = Cc["@mozilla.org/query-stripping-list-service;1"].getService(
+ Ci.nsIURLQueryStrippingListService
+ );
+ // Here we don't care about the actual enabled state, we just want any init to be done so we get reliable starting conditions.
+ await listService.testWaitForInit();
+});
+
+add_task(async function test() {
+ let [normalWindow, pbWindow] = await Promise.all([
+ BrowserTestUtils.openNewBrowserWindow(),
+ BrowserTestUtils.openNewBrowserWindow({ private: true }),
+ ]);
+
+ for (let enableStripPBM of [false, true]) {
+ for (let enableStrip of [false, true]) {
+ let doExperimentCleanup = await ExperimentFakes.enrollWithFeatureConfig({
+ featureId: "queryStripping",
+ value: {
+ enabledNormalBrowsing: enableStrip,
+ enabledPrivateBrowsing: enableStripPBM,
+ stripList: "paramToStrip1 paramToStrip2",
+ },
+ });
+
+ for (let testPBM of [false, true]) {
+ let shouldStrip =
+ (testPBM && enableStripPBM) || (!testPBM && enableStrip);
+ let expectedQueryString = shouldStrip
+ ? TEST_QUERY_STRING_STRIPPED
+ : TEST_QUERY_STRING;
+
+ info(
+ "Test stripping " +
+ JSON.stringify({
+ enableStripPBM,
+ enableStrip,
+ testPBM,
+ expectedQueryString,
+ })
+ );
+
+ await waitForListServiceInit(enableStripPBM || enableStrip);
+
+ let tabBrowser = testPBM ? pbWindow.gBrowser : normalWindow.gBrowser;
+ await BrowserTestUtils.withNewTab(
+ { gBrowser: tabBrowser, url: TEST_URI_WITH_QUERY },
+ async browser => {
+ is(
+ browser.currentURI.query,
+ expectedQueryString,
+ "Correct query string"
+ );
+ }
+ );
+ }
+
+ await doExperimentCleanup();
+ }
+ }
+
+ // Cleanup
+ await Promise.all([
+ BrowserTestUtils.closeWindow(normalWindow),
+ BrowserTestUtils.closeWindow(pbWindow),
+ ]);
+});
diff --git a/toolkit/components/antitracking/test/browser/browser_urlQueryStringStripping_pbmode.js b/toolkit/components/antitracking/test/browser/browser_urlQueryStringStripping_pbmode.js
new file mode 100644
index 0000000000..fd37f94765
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_urlQueryStringStripping_pbmode.js
@@ -0,0 +1,105 @@
+/* vim: set ts=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/. */
+
+"use strict";
+
+/**
+ * Simplified version of browser_urlQueryStripping.js to test that the feature
+ * prefs work correctly in both normal and private browsing.
+ */
+
+const TEST_URI = TEST_DOMAIN + TEST_PATH + "file_stripping.html";
+const TEST_QUERY_STRING = "paramToStrip1=123&paramToKeep=456";
+const TEST_QUERY_STRING_STRIPPED = "paramToKeep=456";
+const TEST_URI_WITH_QUERY = TEST_URI + "?" + TEST_QUERY_STRING;
+
+let listService;
+
+async function waitForListServiceInit(strippingEnabled) {
+ info("Waiting for nsIURLQueryStrippingListService to be initialized.");
+ let isInitialized = await listService.testWaitForInit();
+ is(
+ isInitialized,
+ strippingEnabled,
+ "nsIURLQueryStrippingListService should be initialized when the feature is enabled."
+ );
+}
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["privacy.query_stripping.strip_list", "paramToStrip1 paramToStrip2"],
+ ["privacy.query_stripping.listService.logLevel", "Debug"],
+ ["privacy.query_stripping.strip_on_share.enabled", false],
+ ],
+ });
+
+ // Get the list service so we can wait for it to be fully initialized before running tests.
+ listService = Cc["@mozilla.org/query-stripping-list-service;1"].getService(
+ Ci.nsIURLQueryStrippingListService
+ );
+ // Here we don't care about the actual enabled state, we just want any init to be done so we get reliable starting conditions.
+ await listService.testWaitForInit();
+});
+
+add_task(async function test() {
+ let [normalWindow, pbWindow] = await Promise.all([
+ BrowserTestUtils.openNewBrowserWindow(),
+ BrowserTestUtils.openNewBrowserWindow({ private: true }),
+ ]);
+
+ for (let enableStripPBM of [false, true]) {
+ Services.prefs.setBoolPref(
+ "privacy.query_stripping.enabled.pbmode",
+ enableStripPBM
+ );
+ for (let enableStrip of [false, true]) {
+ Services.prefs.setBoolPref(
+ "privacy.query_stripping.enabled",
+ enableStrip
+ );
+ for (let testPBM of [false, true]) {
+ let shouldStrip =
+ (testPBM && enableStripPBM) || (!testPBM && enableStrip);
+ let expectedQueryString = shouldStrip
+ ? TEST_QUERY_STRING_STRIPPED
+ : TEST_QUERY_STRING;
+
+ info(
+ "Test stripping " +
+ JSON.stringify({
+ enableStripPBM,
+ enableStrip,
+ testPBM,
+ expectedQueryString,
+ })
+ );
+
+ await waitForListServiceInit(enableStripPBM || enableStrip);
+
+ let tabBrowser = testPBM ? pbWindow.gBrowser : normalWindow.gBrowser;
+ await BrowserTestUtils.withNewTab(
+ { gBrowser: tabBrowser, url: TEST_URI_WITH_QUERY },
+ async browser => {
+ is(
+ browser.currentURI.query,
+ expectedQueryString,
+ "Correct query string"
+ );
+ }
+ );
+ }
+ }
+ }
+
+ // Cleanup
+ await Promise.all([
+ BrowserTestUtils.closeWindow(normalWindow),
+ BrowserTestUtils.closeWindow(pbWindow),
+ ]);
+
+ Services.prefs.clearUserPref("privacy.query_stripping.enabled");
+ Services.prefs.clearUserPref("privacy.query_stripping.enabled.pbmode");
+});
diff --git a/toolkit/components/antitracking/test/browser/browser_urlQueryStringStripping_telemetry.js b/toolkit/components/antitracking/test/browser/browser_urlQueryStringStripping_telemetry.js
new file mode 100644
index 0000000000..20c830113c
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_urlQueryStringStripping_telemetry.js
@@ -0,0 +1,355 @@
+/**
+ * Bug 1706616 - Testing the URL query string stripping telemetry.
+ */
+
+"use strict";
+
+const TEST_URI = TEST_DOMAIN + TEST_PATH + "file_stripping.html";
+const TEST_THIRD_PARTY_URI = TEST_DOMAIN_2 + TEST_PATH + "file_stripping.html";
+const TEST_REDIRECT_URI = TEST_DOMAIN + TEST_PATH + "redirect.sjs";
+
+const { TelemetryTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TelemetryTestUtils.sys.mjs"
+);
+
+const LABEL_NAVIGATION = 0;
+const LABEL_REDIRECT = 1;
+const LABEL_STRIP_FOR_NAVIGATION = 2;
+const LABEL_STRIP_FOR_REDIRECT = 3;
+
+const QUERY_STRIPPING_COUNT = "QUERY_STRIPPING_COUNT";
+const QUERY_STRIPPING_PARAM_COUNT = "QUERY_STRIPPING_PARAM_COUNT";
+
+async function clearTelemetry() {
+ // There's an arbitrary interval of 2 seconds in which the content
+ // processes sync their data with the parent process, we wait
+ // this out to ensure that we clear everything.
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(resolve => setTimeout(resolve, 2000));
+
+ Services.telemetry.getSnapshotForHistograms("main", true /* clear */);
+ Services.telemetry.getHistogramById(QUERY_STRIPPING_COUNT).clear();
+ Services.telemetry.getHistogramById(QUERY_STRIPPING_PARAM_COUNT).clear();
+
+ let isCleared = () => {
+ let histograms = Services.telemetry.getSnapshotForHistograms(
+ "main",
+ false /* clear */
+ ).content;
+
+ return (
+ !histograms ||
+ (!histograms[QUERY_STRIPPING_COUNT] &&
+ !histograms[QUERY_STRIPPING_PARAM_COUNT])
+ );
+ };
+
+ // Check that the telemetry probes have been cleared properly. Do this check
+ // sync first to avoid any race conditions where telemetry arrives after
+ // clearing.
+ if (!isCleared()) {
+ await TestUtils.waitForCondition(
+ isCleared,
+ "waiting for query stripping probes to be cleared"
+ );
+ }
+
+ ok(true, "Telemetry has been cleared.");
+}
+
+async function verifyQueryString(browser, expected) {
+ await SpecialPowers.spawn(browser, [expected], expected => {
+ // Strip the first question mark.
+ let search = content.location.search.slice(1);
+
+ is(search, expected, "The query string is correct.");
+ });
+}
+
+async function getTelemetryProbe(key, label, checkCntFn) {
+ let histogram;
+
+ // Wait until the telemetry probe appears.
+ await TestUtils.waitForCondition(() => {
+ let histograms = Services.telemetry.getSnapshotForHistograms(
+ "main",
+ false /* clear */
+ ).parent;
+ histogram = histograms[key];
+
+ let checkRes = false;
+
+ if (histogram) {
+ checkRes = checkCntFn ? checkCntFn(histogram.values[label]) : true;
+ }
+
+ return checkRes;
+ }, `waiting for telemetry probe (key=${key}, label=${label}) to appear`);
+
+ return histogram.values[label];
+}
+
+async function checkTelemetryProbe(key, expectedCnt, label) {
+ let cnt = await getTelemetryProbe(key, label, cnt => cnt == expectedCnt);
+
+ is(cnt, expectedCnt, "There should be expected count in telemetry.");
+}
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["privacy.query_stripping.enabled", true],
+ [
+ "privacy.query_stripping.strip_list",
+ "paramToStrip paramToStripB paramToStripC paramToStripD",
+ ],
+ ],
+ });
+
+ // Clear Telemetry probes before testing.
+ await clearTelemetry();
+});
+
+add_task(async function testQueryStrippingNavigationInParent() {
+ let testURI = TEST_URI + "?paramToStrip=value";
+
+ // Open a new tab and trigger the query stripping.
+ await BrowserTestUtils.withNewTab(testURI, async browser => {
+ // Verify if the query string was happened.
+ await verifyQueryString(browser, "");
+ });
+
+ // Verify the telemetry probe.
+ await checkTelemetryProbe(
+ QUERY_STRIPPING_COUNT,
+ 1,
+ LABEL_STRIP_FOR_NAVIGATION
+ );
+ await checkTelemetryProbe(QUERY_STRIPPING_PARAM_COUNT, 1, "1");
+
+ // Because there would be some loading happening during the test and they
+ // could interfere the count here. So, we only verify if the counter is
+ // increased, but not the exact count.
+ let newNavigationCnt = await getTelemetryProbe(
+ QUERY_STRIPPING_COUNT,
+ LABEL_NAVIGATION,
+ cnt => cnt > 0
+ );
+ ok(newNavigationCnt > 0, "There is navigation count added.");
+
+ await clearTelemetry();
+});
+
+add_task(async function testQueryStrippingNavigationInContent() {
+ let testThirdPartyURI = TEST_THIRD_PARTY_URI + "?paramToStrip=value";
+
+ await BrowserTestUtils.withNewTab(TEST_URI, async browser => {
+ // Create the promise to wait for the location change.
+ let locationChangePromise = BrowserTestUtils.waitForLocationChange(
+ gBrowser,
+ TEST_THIRD_PARTY_URI
+ );
+
+ // Trigger the navigation by script.
+ await SpecialPowers.spawn(browser, [testThirdPartyURI], async url => {
+ content.postMessage({ type: "script", url }, "*");
+ });
+
+ await locationChangePromise;
+
+ // Verify if the query string was happened.
+ await verifyQueryString(browser, "");
+ });
+
+ // Verify the telemetry probe.
+ await checkTelemetryProbe(
+ QUERY_STRIPPING_COUNT,
+ 1,
+ LABEL_STRIP_FOR_NAVIGATION
+ );
+ await checkTelemetryProbe(QUERY_STRIPPING_PARAM_COUNT, 1, "1");
+
+ // Check if the navigation count is increased.
+ let newNavigationCnt = await getTelemetryProbe(
+ QUERY_STRIPPING_COUNT,
+ LABEL_NAVIGATION,
+ cnt => cnt > 0
+ );
+ ok(newNavigationCnt > 0, "There is navigation count added.");
+
+ await clearTelemetry();
+});
+
+add_task(async function testQueryStrippingNavigationInContentQueryCount() {
+ let testThirdPartyURI =
+ TEST_THIRD_PARTY_URI +
+ "?paramToStrip=value&paramToStripB=valueB&paramToStripC=valueC&paramToStripD=valueD";
+
+ await BrowserTestUtils.withNewTab(TEST_URI, async browser => {
+ // Create the promise to wait for the location change.
+ let locationChangePromise = BrowserTestUtils.waitForLocationChange(
+ gBrowser,
+ TEST_THIRD_PARTY_URI
+ );
+
+ // Trigger the navigation by script.
+ await SpecialPowers.spawn(browser, [testThirdPartyURI], async url => {
+ content.postMessage({ type: "script", url }, "*");
+ });
+
+ await locationChangePromise;
+
+ // Verify if the query string was happened.
+ await verifyQueryString(browser, "");
+ });
+
+ // Verify the telemetry probe.
+ await checkTelemetryProbe(
+ QUERY_STRIPPING_COUNT,
+ 1,
+ LABEL_STRIP_FOR_NAVIGATION
+ );
+
+ await getTelemetryProbe(QUERY_STRIPPING_PARAM_COUNT, "0", cnt => !cnt);
+ await getTelemetryProbe(QUERY_STRIPPING_PARAM_COUNT, "1", cnt => !cnt);
+ await getTelemetryProbe(QUERY_STRIPPING_PARAM_COUNT, "2", cnt => !cnt);
+ await getTelemetryProbe(QUERY_STRIPPING_PARAM_COUNT, "3", cnt => !cnt);
+ await getTelemetryProbe(QUERY_STRIPPING_PARAM_COUNT, "4", cnt => cnt == 1);
+ await getTelemetryProbe(QUERY_STRIPPING_PARAM_COUNT, "5", cnt => !cnt);
+
+ // Check if the navigation count is increased.
+ let newNavigationCnt = await getTelemetryProbe(
+ QUERY_STRIPPING_COUNT,
+ LABEL_NAVIGATION,
+ cnt => cnt > 0
+ );
+ ok(newNavigationCnt > 0, "There is navigation count added.");
+
+ await clearTelemetry();
+});
+
+add_task(async function testQueryStrippingRedirect() {
+ let testThirdPartyURI = `${TEST_REDIRECT_URI}?${TEST_THIRD_PARTY_URI}?paramToStrip=value`;
+
+ await BrowserTestUtils.withNewTab(TEST_URI, async browser => {
+ // Create the promise to wait for the location change.
+ let locationChangePromise = BrowserTestUtils.waitForLocationChange(
+ gBrowser,
+ TEST_THIRD_PARTY_URI
+ );
+
+ // Trigger the redirect.
+ await SpecialPowers.spawn(browser, [testThirdPartyURI], async url => {
+ content.postMessage({ type: "script", url }, "*");
+ });
+
+ await locationChangePromise;
+
+ // Verify if the query string was happened.
+ await verifyQueryString(browser, "");
+ });
+
+ // Verify the telemetry probe in parent process. Note that there is no
+ // non-test loading is using redirect. So, we can check the exact count here.
+ await checkTelemetryProbe(QUERY_STRIPPING_COUNT, 1, LABEL_STRIP_FOR_REDIRECT);
+ await checkTelemetryProbe(QUERY_STRIPPING_COUNT, 1, LABEL_REDIRECT);
+ await checkTelemetryProbe(QUERY_STRIPPING_PARAM_COUNT, 1, "1");
+
+ await clearTelemetry();
+});
+
+add_task(async function testQueryStrippingDisabled() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["privacy.query_stripping.enabled", false]],
+ });
+
+ // First, test the navigation in parent process.
+ let testURI = TEST_URI + "?paramToStrip=value";
+
+ // Open a new tab and trigger the query stripping.
+ await BrowserTestUtils.withNewTab(testURI, async browser => {
+ // Verify if the query string was not happened.
+ await verifyQueryString(browser, "paramToStrip=value");
+ });
+
+ // Verify the telemetry probe. There should be no stripped navigation count.
+ await checkTelemetryProbe(
+ QUERY_STRIPPING_COUNT,
+ undefined,
+ LABEL_STRIP_FOR_NAVIGATION
+ );
+ // Check if the navigation count is increased.
+ let newNavigationCnt = await getTelemetryProbe(
+ QUERY_STRIPPING_COUNT,
+ LABEL_NAVIGATION,
+ cnt => cnt > 0
+ );
+ ok(newNavigationCnt > 0, "There is navigation count added.");
+
+ // Second, test the navigation in content.
+ let testThirdPartyURI = TEST_THIRD_PARTY_URI + "?paramToStrip=value";
+
+ await BrowserTestUtils.withNewTab(TEST_URI, async browser => {
+ // Create the promise to wait for the location change.
+ let locationChangePromise = BrowserTestUtils.waitForLocationChange(
+ gBrowser,
+ testThirdPartyURI
+ );
+
+ // Trigger the navigation by script.
+ await SpecialPowers.spawn(browser, [testThirdPartyURI], async url => {
+ content.postMessage({ type: "script", url }, "*");
+ });
+
+ await locationChangePromise;
+
+ // Verify if the query string was happened.
+ await verifyQueryString(browser, "paramToStrip=value");
+ });
+
+ // Verify the telemetry probe in content process. There should be no stripped
+ // navigation count.
+ await checkTelemetryProbe(
+ QUERY_STRIPPING_COUNT,
+ undefined,
+ LABEL_STRIP_FOR_NAVIGATION
+ );
+ // Check if the navigation count is increased.
+ newNavigationCnt = await getTelemetryProbe(
+ QUERY_STRIPPING_COUNT,
+ LABEL_NAVIGATION,
+ cnt => cnt > 0
+ );
+ ok(newNavigationCnt > 0, "There is navigation count added.");
+
+ // Third, test the redirect.
+ testThirdPartyURI = `${TEST_REDIRECT_URI}?${TEST_THIRD_PARTY_URI}?paramToStrip=value`;
+
+ await BrowserTestUtils.withNewTab(TEST_URI, async browser => {
+ // Create the promise to wait for the location change.
+ let locationChangePromise = BrowserTestUtils.waitForLocationChange(
+ gBrowser,
+ `${TEST_THIRD_PARTY_URI}?paramToStrip=value`
+ );
+
+ // Trigger the redirect.
+ await SpecialPowers.spawn(browser, [testThirdPartyURI], async url => {
+ content.postMessage({ type: "script", url }, "*");
+ });
+
+ await locationChangePromise;
+
+ // Verify if the query string was happened.
+ await verifyQueryString(browser, "paramToStrip=value");
+ });
+
+ // Verify the telemetry probe. The stripped redirect count should not exist.
+ await checkTelemetryProbe(
+ QUERY_STRIPPING_COUNT,
+ undefined,
+ LABEL_STRIP_FOR_REDIRECT
+ );
+ await checkTelemetryProbe(QUERY_STRIPPING_COUNT, 1, LABEL_REDIRECT);
+
+ await clearTelemetry();
+});
diff --git a/toolkit/components/antitracking/test/browser/browser_urlQueryStringStripping_telemetry_2.js b/toolkit/components/antitracking/test/browser/browser_urlQueryStringStripping_telemetry_2.js
new file mode 100644
index 0000000000..6874e4bc62
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_urlQueryStringStripping_telemetry_2.js
@@ -0,0 +1,159 @@
+"use strict";
+
+const TEST_URI = TEST_DOMAIN + TEST_PATH + "file_stripping.html";
+
+const QUERY_STRIPPING_COUNT = "QUERY_STRIPPING_COUNT";
+const QUERY_STRIPPING_PARAM_COUNT = "QUERY_STRIPPING_PARAM_COUNT";
+const QUERY_STRIPPING_COUNT_BY_PARAM = "QUERY_STRIPPING_COUNT_BY_PARAM";
+
+const histogramLabels =
+ Services.telemetry.getCategoricalLabels().QUERY_STRIPPING_COUNT_BY_PARAM;
+
+async function clearTelemetry() {
+ // There's an arbitrary interval of 2 seconds in which the content
+ // processes sync their data with the parent process, we wait
+ // this out to ensure that we clear everything.
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(resolve => setTimeout(resolve, 2000));
+
+ Services.telemetry.getSnapshotForHistograms("main", true /* clear */);
+ Services.telemetry.getHistogramById(QUERY_STRIPPING_COUNT).clear();
+ Services.telemetry.getHistogramById(QUERY_STRIPPING_PARAM_COUNT).clear();
+ Services.telemetry.getHistogramById(QUERY_STRIPPING_COUNT_BY_PARAM).clear();
+
+ let isCleared = () => {
+ let histograms = Services.telemetry.getSnapshotForHistograms(
+ "main",
+ false /* clear */
+ ).content;
+
+ return (
+ !histograms ||
+ (!histograms[QUERY_STRIPPING_COUNT] &&
+ !histograms[QUERY_STRIPPING_PARAM_COUNT] &&
+ !histograms.QUERY_STRIPPING_COUNT_BY_PARAM)
+ );
+ };
+
+ // Check that the telemetry probes have been cleared properly. Do this check
+ // sync first to avoid any race conditions where telemetry arrives after
+ // clearing.
+ if (!isCleared()) {
+ await TestUtils.waitForCondition(isCleared);
+ }
+
+ ok(true, "Telemetry has been cleared.");
+}
+
+async function verifyQueryString(browser, expected) {
+ await SpecialPowers.spawn(browser, [expected], expected => {
+ // Strip the first question mark.
+ let search = content.location.search.slice(1);
+
+ is(search, expected, "The query string is correct.");
+ });
+}
+
+function testTelemetry(queryParamToCount) {
+ const histogram = Services.telemetry.getHistogramById(
+ QUERY_STRIPPING_COUNT_BY_PARAM
+ );
+
+ let snapshot = histogram.snapshot();
+
+ let indexToCount = {};
+ Object.entries(queryParamToCount).forEach(([key, value]) => {
+ let index = histogramLabels.indexOf(`param_${key}`);
+
+ // In debug builds we perform additional stripping for testing, which
+ // results in telemetry being recorded twice. This does not impact
+ // production builds.
+ if (SpecialPowers.isDebugBuild) {
+ indexToCount[index] = value * 2;
+ } else {
+ indexToCount[index] = value;
+ }
+ });
+
+ for (let [i, val] of Object.entries(snapshot.values)) {
+ let expectedCount = indexToCount[i] || 0;
+
+ is(
+ val,
+ expectedCount,
+ `Histogram ${QUERY_STRIPPING_COUNT_BY_PARAM} should have expected value for label ${histogramLabels[i]}.`
+ );
+ }
+}
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["privacy.query_stripping.enabled", true],
+ [
+ "privacy.query_stripping.strip_list",
+ "foo mc_eid oly_anon_id oly_enc_id __s vero_id _hsenc mkt_tok fbclid",
+ ],
+ ],
+ });
+
+ // Clear Telemetry probes before testing.
+ await clearTelemetry();
+});
+
+/**
+ * Tests the QUERY_STRIPPING_COUNT_BY_PARAM histogram telemetry which counts how
+ * often query params from a predefined lists are stripped.
+ */
+add_task(async function test_queryParamCountTelemetry() {
+ info("Test with a query params to be stripped and recoded in telemetry.");
+ let url = new URL(TEST_URI);
+ url.searchParams.set("mc_eid", "myValue");
+
+ // Open a new tab and trigger the query stripping.
+ await BrowserTestUtils.withNewTab(url.href, async browser => {
+ // Verify that the tracking query param has been stripped.
+ await verifyQueryString(browser, "");
+ });
+
+ testTelemetry({ mc_eid: 1 });
+
+ // Repeat this with the same query parameter, the respective histogram bucket
+ // should be incremented.
+ await BrowserTestUtils.withNewTab(url.href, async browser => {
+ await verifyQueryString(browser, "");
+ });
+
+ testTelemetry({ mc_eid: 2 });
+
+ url = new URL(TEST_URI);
+ url.searchParams.set("fbclid", "myValue2");
+ url.searchParams.set("mkt_tok", "myValue3");
+ url.searchParams.set("bar", "foo");
+
+ info("Test with multiple query params to be stripped.");
+ await BrowserTestUtils.withNewTab(url.href, async browser => {
+ await verifyQueryString(browser, "bar=foo");
+ });
+ testTelemetry({ mc_eid: 2, fbclid: 1, mkt_tok: 1 });
+
+ info(
+ "Test with query param on the strip-list, which should not be recoded in telemetry."
+ );
+ url = new URL(TEST_URI);
+ url.searchParams.set("foo", "bar");
+ url.searchParams.set("__s", "myValue4");
+ await BrowserTestUtils.withNewTab(url.href, async browser => {
+ await verifyQueryString(browser, "");
+ });
+ testTelemetry({ mc_eid: 2, fbclid: 1, mkt_tok: 1, __s: 1 });
+
+ url = new URL(TEST_URI);
+ url.searchParams.set("foo", "bar");
+ await BrowserTestUtils.withNewTab(url.href, async browser => {
+ await verifyQueryString(browser, "");
+ });
+ testTelemetry({ mc_eid: 2, fbclid: 1, mkt_tok: 1, __s: 1 });
+
+ await clearTelemetry();
+});
diff --git a/toolkit/components/antitracking/test/browser/browser_urlQueryStrippingListService.js b/toolkit/components/antitracking/test/browser/browser_urlQueryStrippingListService.js
new file mode 100644
index 0000000000..1d46986ff1
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_urlQueryStrippingListService.js
@@ -0,0 +1,249 @@
+/* vim: set ts=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/. */
+
+"use strict";
+
+const { RemoteSettings } = ChromeUtils.importESModule(
+ "resource://services-settings/remote-settings.sys.mjs"
+);
+
+XPCOMUtils.defineLazyServiceGetter(
+ this,
+ "urlQueryStrippingListService",
+ "@mozilla.org/query-stripping-list-service;1",
+ "nsIURLQueryStrippingListService"
+);
+
+const COLLECTION_NAME = "query-stripping";
+
+const TEST_URI = TEST_DOMAIN + TEST_PATH + "empty.html";
+const TEST_THIRD_PARTY_URI = TEST_DOMAIN_2 + TEST_PATH + "empty.html";
+
+// The Update Event here is used to listen the observer from the
+// URLQueryStrippingListService. We need to use the event here so that the same
+// observer can be called multiple times.
+class UpdateEvent extends EventTarget {}
+function waitForEvent(element, eventName) {
+ return BrowserTestUtils.waitForEvent(element, eventName).then(e => e.detail);
+}
+
+async function verifyQueryString(browser, expected) {
+ await SpecialPowers.spawn(browser, [expected], expected => {
+ // Strip the first question mark.
+ let search = content.location.search.slice(1);
+
+ is(search, expected, "The query string is correct.");
+ });
+}
+
+async function check(query, expected) {
+ // Open a tab with the query string.
+ let testURI = TEST_URI + "?" + query;
+
+ // Test for stripping in parent process.
+ await BrowserTestUtils.withNewTab(testURI, async browser => {
+ // Verify if the query string is expected in the new tab.
+ await verifyQueryString(browser, expected);
+ });
+
+ testURI = TEST_URI + "?" + query;
+ let expectedURI;
+ if (expected != "") {
+ expectedURI = TEST_URI + "?" + expected;
+ } else {
+ expectedURI = TEST_URI;
+ }
+
+ // Test for stripping in content processes. This will first open a third-party
+ // page and create a link to the test uri. And then, click the link to
+ // navigate the page, which will trigger the stripping in content processes.
+ await BrowserTestUtils.withNewTab(TEST_THIRD_PARTY_URI, async browser => {
+ // Create the promise to wait for the location change.
+ let locationChangePromise = BrowserTestUtils.waitForLocationChange(
+ gBrowser,
+ expectedURI
+ );
+
+ // Create a link and click it to navigate.
+ await SpecialPowers.spawn(browser, [testURI], async uri => {
+ let link = content.document.createElement("a");
+ link.setAttribute("href", uri);
+ link.textContent = "Link";
+ content.document.body.appendChild(link);
+ link.click();
+ });
+
+ await locationChangePromise;
+
+ // Verify the query string in the content window.
+ await verifyQueryString(browser, expected);
+ });
+}
+
+registerCleanupFunction(() => {
+ Cc["@mozilla.org/query-stripping-list-service;1"]
+ .getService(Ci.nsIURLQueryStrippingListService)
+ .clearLists();
+});
+
+add_task(async function testPrefSettings() {
+ // Enable query stripping and clear the prefs at the beginning.
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["privacy.query_stripping.enabled", true],
+ ["privacy.query_stripping.strip_list", ""],
+ ["privacy.query_stripping.allow_list", ""],
+ ["privacy.query_stripping.testing", true],
+ ],
+ });
+
+ // Test if the observer been called when adding to the service.
+ let updateEvent = new UpdateEvent();
+ let obs = (stripList, allowList) => {
+ let event = new CustomEvent("update", { detail: { stripList, allowList } });
+ updateEvent.dispatchEvent(event);
+ };
+ let promise = waitForEvent(updateEvent, "update");
+ urlQueryStrippingListService.registerAndRunObserver(obs);
+ let lists = await promise;
+ is(lists.stripList, "", "No strip list at the beginning.");
+ is(lists.allowList, "", "No allow list at the beginning.");
+
+ // Verify that no query stripping happens.
+ await check("pref_query1=123", "pref_query1=123");
+ await check("pref_query2=456", "pref_query2=456");
+
+ // Set pref for strip list
+ promise = waitForEvent(updateEvent, "update");
+ await SpecialPowers.pushPrefEnv({
+ set: [["privacy.query_stripping.strip_list", "pref_query1 pref_query2"]],
+ });
+ lists = await promise;
+
+ is(
+ lists.stripList,
+ "pref_query1 pref_query2",
+ "There should be strip list entries."
+ );
+ is(lists.allowList, "", "There should be no allow list entries.");
+
+ // The query string should be stripped.
+ await check("pref_query1=123", "");
+ await check("pref_query2=456", "");
+
+ // Set the pref for allow list.
+ promise = waitForEvent(updateEvent, "update");
+ await SpecialPowers.pushPrefEnv({
+ set: [["privacy.query_stripping.allow_list", "example.net"]],
+ });
+ lists = await promise;
+
+ is(
+ lists.stripList,
+ "pref_query1 pref_query2",
+ "There should be strip list entires."
+ );
+ is(lists.allowList, "example.net", "There should be one allow list entry.");
+
+ // The query string shouldn't be stripped because this host is in allow list.
+ await check("pref_query1=123", "pref_query1=123");
+ await check("pref_query2=123", "pref_query2=123");
+
+ urlQueryStrippingListService.unregisterObserver(obs);
+
+ // Clear prefs.
+ SpecialPowers.flushPrefEnv();
+});
+
+add_task(async function testRemoteSettings() {
+ // Enable query stripping and clear the prefs at the beginning.
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["privacy.query_stripping.enabled", true],
+ ["privacy.query_stripping.strip_list", ""],
+ ["privacy.query_stripping.allow_list", ""],
+ ["privacy.query_stripping.testing", true],
+ ],
+ });
+
+ // Add initial empty record.
+ let db = RemoteSettings(COLLECTION_NAME).db;
+ await db.importChanges({}, Date.now(), []);
+
+ // Test if the observer been called when adding to the service.
+ let updateEvent = new UpdateEvent();
+ let obs = (stripList, allowList) => {
+ let event = new CustomEvent("update", { detail: { stripList, allowList } });
+ updateEvent.dispatchEvent(event);
+ };
+ let promise = waitForEvent(updateEvent, "update");
+ urlQueryStrippingListService.registerAndRunObserver(obs);
+ let lists = await promise;
+ is(lists.stripList, "", "No strip list at the beginning.");
+ is(lists.allowList, "", "No allow list at the beginning.");
+
+ // Verify that no query stripping happens.
+ await check("remote_query1=123", "remote_query1=123");
+ await check("remote_query2=456", "remote_query2=456");
+
+ // Set record for strip list.
+ promise = waitForEvent(updateEvent, "update");
+ await RemoteSettings(COLLECTION_NAME).emit("sync", {
+ data: {
+ current: [
+ {
+ id: "1",
+ last_modified: 1000000000000001,
+ stripList: ["remote_query1", "remote_query2"],
+ allowList: [],
+ },
+ ],
+ },
+ });
+ lists = await promise;
+
+ is(
+ lists.stripList,
+ "remote_query1 remote_query2",
+ "There should be strip list entries."
+ );
+ is(lists.allowList, "", "There should be no allow list entries.");
+
+ // The query string should be stripped.
+ await check("remote_query1=123", "");
+ await check("remote_query2=456", "");
+
+ // Set record for strip list and allow list.
+ promise = waitForEvent(updateEvent, "update");
+ await RemoteSettings(COLLECTION_NAME).emit("sync", {
+ data: {
+ current: [
+ {
+ id: "2",
+ last_modified: 1000000000000002,
+ stripList: ["remote_query1", "remote_query2"],
+ allowList: ["example.net"],
+ },
+ ],
+ },
+ });
+ lists = await promise;
+
+ is(
+ lists.stripList,
+ "remote_query1 remote_query2",
+ "There should be strip list entries."
+ );
+ is(lists.allowList, "example.net", "There should be one allow list entry.");
+
+ // The query string shouldn't be stripped because this host is in allow list.
+ await check("remote_query1=123", "remote_query1=123");
+ await check("remote_query2=123", "remote_query2=123");
+
+ urlQueryStrippingListService.unregisterObserver(obs);
+
+ // Clear the remote settings.
+ await db.clear();
+});
diff --git a/toolkit/components/antitracking/test/browser/browser_userInteraction.js b/toolkit/components/antitracking/test/browser/browser_userInteraction.js
new file mode 100644
index 0000000000..d343a56731
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_userInteraction.js
@@ -0,0 +1,124 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* eslint-disable mozilla/no-arbitrary-setTimeout */
+
+add_task(async function () {
+ info("Starting subResources test");
+
+ await SpecialPowers.flushPrefEnv();
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["privacy.userInteraction.document.interval", 1],
+ [
+ "network.cookie.cookieBehavior",
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER,
+ ],
+ [
+ "network.cookie.cookieBehavior.pbmode",
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER,
+ ],
+ ["privacy.trackingprotection.enabled", false],
+ ["privacy.trackingprotection.pbmode.enabled", false],
+ ["privacy.trackingprotection.annotate_channels", true],
+ ],
+ });
+
+ await UrlClassifierTestUtils.addTestTrackers();
+
+ info("Creating a new tab");
+ let tab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE);
+ gBrowser.selectedTab = tab;
+
+ let browser = gBrowser.getBrowserForTab(tab);
+ await BrowserTestUtils.browserLoaded(browser);
+
+ let uri = Services.io.newURI(TEST_DOMAIN);
+ is(
+ PermissionTestUtils.testPermission(uri, "storageAccessAPI"),
+ Services.perms.UNKNOWN_ACTION,
+ "Before user-interaction we don't have a permission"
+ );
+
+ let promise = TestUtils.topicObserved("perm-changed", (aSubject, aData) => {
+ let permission = aSubject.QueryInterface(Ci.nsIPermission);
+ return (
+ permission.type == "storageAccessAPI" &&
+ permission.principal.equalsURI(uri)
+ );
+ });
+
+ info("Simulating user-interaction.");
+ await SpecialPowers.spawn(browser, [], async function () {
+ content.document.userInteractionForTesting();
+ });
+
+ info("Waiting to have a permissions set.");
+ await promise;
+
+ // Let's see if the document is able to update the permission correctly.
+ for (var i = 0; i < 3; ++i) {
+ // Another perm-changed event should be triggered by the timer.
+ promise = TestUtils.topicObserved("perm-changed", (aSubject, aData) => {
+ let permission = aSubject.QueryInterface(Ci.nsIPermission);
+ return (
+ permission.type == "storageAccessAPI" &&
+ permission.principal.equalsURI(uri)
+ );
+ });
+
+ info("Simulating another user-interaction.");
+ await SpecialPowers.spawn(browser, [], async function () {
+ content.document.userInteractionForTesting();
+ });
+
+ info("Waiting to have a permissions set.");
+ await promise;
+ }
+
+ // Let's disable the document.interval.
+ await SpecialPowers.pushPrefEnv({
+ set: [["privacy.userInteraction.document.interval", 0]],
+ });
+
+ promise = new Promise(resolve => {
+ let id;
+
+ function observer(subject, topic, data) {
+ ok(false, "Notification received!");
+ Services.obs.removeObserver(observer, "perm-changed");
+ clearTimeout(id);
+ resolve();
+ }
+
+ Services.obs.addObserver(observer, "perm-changed");
+
+ id = setTimeout(() => {
+ ok(true, "No notification received!");
+ Services.obs.removeObserver(observer, "perm-changed");
+ resolve();
+ }, 2000);
+ });
+
+ info("Simulating another user-interaction.");
+ await SpecialPowers.spawn(browser, [], async function () {
+ content.document.userInteractionForTesting();
+ });
+
+ info("Waiting to have a permissions set.");
+ await promise;
+
+ info("Removing the tab");
+ BrowserTestUtils.removeTab(tab);
+
+ UrlClassifierTestUtils.cleanupTestTrackers();
+});
+
+add_task(async function () {
+ info("Cleaning up.");
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+});
diff --git a/toolkit/components/antitracking/test/browser/browser_workerPropagation.js b/toolkit/components/antitracking/test/browser/browser_workerPropagation.js
new file mode 100644
index 0000000000..54ec0c1bf6
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_workerPropagation.js
@@ -0,0 +1,87 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* eslint-disable mozilla/no-arbitrary-setTimeout */
+
+add_task(async function () {
+ info("Starting subResources test");
+
+ await SpecialPowers.flushPrefEnv();
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.storage_access.auto_grants", true],
+ ["dom.storage_access.auto_grants.delayed", false],
+ ["dom.storage_access.enabled", true],
+ ["dom.storage_access.prompt.testing", false],
+ [
+ "network.cookie.cookieBehavior",
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER,
+ ],
+ [
+ "network.cookie.cookieBehavior.pbmode",
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER,
+ ],
+ ["privacy.trackingprotection.enabled", false],
+ ["privacy.trackingprotection.pbmode.enabled", false],
+ ["privacy.trackingprotection.annotate_channels", true],
+ [
+ "privacy.restrict3rdpartystorage.userInteractionRequiredForHosts",
+ "tracking.example.com,tracking.example.org",
+ ],
+ ],
+ });
+
+ await UrlClassifierTestUtils.addTestTrackers();
+
+ info("Creating a new tab");
+ let tab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE);
+ gBrowser.selectedTab = tab;
+
+ let browser = gBrowser.getBrowserForTab(tab);
+ await BrowserTestUtils.browserLoaded(browser);
+
+ // Let's create an iframe and run the test there.
+ let page = TEST_3RD_PARTY_DOMAIN + TEST_PATH + "workerIframe.html";
+ await SpecialPowers.spawn(browser, [page], async function (page) {
+ await new content.Promise(resolve => {
+ let ifr = content.document.createElement("iframe");
+ ifr.id = "test";
+
+ content.addEventListener("message", e => {
+ if (e.data.type == "finish") {
+ resolve();
+ return;
+ }
+
+ if (e.data.type == "info") {
+ info(e.data.msg);
+ return;
+ }
+
+ if (e.data.type == "ok") {
+ ok(e.data.what, e.data.msg);
+ return;
+ }
+
+ ok(false, "Unknown message");
+ });
+
+ content.document.body.appendChild(ifr);
+ ifr.src = page;
+ });
+ });
+
+ info("Removing the tab");
+ BrowserTestUtils.removeTab(tab);
+
+ UrlClassifierTestUtils.cleanupTestTrackers();
+});
+
+add_task(async function () {
+ info("Cleaning up.");
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+});
diff --git a/toolkit/components/antitracking/test/browser/clearSiteData.sjs b/toolkit/components/antitracking/test/browser/clearSiteData.sjs
new file mode 100644
index 0000000000..374f03a474
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/clearSiteData.sjs
@@ -0,0 +1,6 @@
+function handleRequest(aRequest, aResponse) {
+ aResponse.setStatusLine(aRequest.httpVersion, 200);
+ aResponse.setHeader("Clear-Site-Data", '"*"');
+ aResponse.setHeader("Content-Type", "text/plain");
+ aResponse.write("Clear-Site-Data");
+}
diff --git a/toolkit/components/antitracking/test/browser/container.html b/toolkit/components/antitracking/test/browser/container.html
new file mode 100644
index 0000000000..24daa80113
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/container.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+<body>
+<iframe src="embedder.html"></iframe>
+</body>
+</html>
diff --git a/toolkit/components/antitracking/test/browser/container2.html b/toolkit/components/antitracking/test/browser/container2.html
new file mode 100644
index 0000000000..c2591ad7fc
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/container2.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<body>
+<script>
+ onmessage = function(e) {
+ parent.postMessage(e.data, "*");
+ };
+</script>
+<iframe src="embedder2.html"></iframe>
+</body>
+</html>
diff --git a/toolkit/components/antitracking/test/browser/cookies.sjs b/toolkit/components/antitracking/test/browser/cookies.sjs
new file mode 100644
index 0000000000..1267a69d8c
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/cookies.sjs
@@ -0,0 +1,12 @@
+function handleRequest(aRequest, aResponse) {
+ aResponse.setStatusLine(aRequest.httpVersion, 200);
+ let cookie = "";
+ if (aRequest.hasHeader("Cookie")) {
+ cookie = aRequest.getHeader("Cookie");
+ }
+ aResponse.write("cookie:" + cookie);
+
+ if (aRequest.queryString) {
+ aResponse.setHeader("Set-Cookie", "foopy=" + aRequest.queryString);
+ }
+}
diff --git a/toolkit/components/antitracking/test/browser/cookiesCORS.sjs b/toolkit/components/antitracking/test/browser/cookiesCORS.sjs
new file mode 100644
index 0000000000..2cfdca2700
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/cookiesCORS.sjs
@@ -0,0 +1,9 @@
+function handleRequest(aRequest, aResponse) {
+ aResponse.setStatusLine(aRequest.httpVersion, 200);
+ aResponse.setHeader("Access-Control-Allow-Origin", "http://example.net");
+ aResponse.setHeader("Access-Control-Allow-Credentials", "true");
+
+ if (aRequest.queryString) {
+ aResponse.setHeader("Set-Cookie", "foopy=" + aRequest.queryString);
+ }
+}
diff --git a/toolkit/components/antitracking/test/browser/dedicatedWorker.js b/toolkit/components/antitracking/test/browser/dedicatedWorker.js
new file mode 100644
index 0000000000..72fd4ad850
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/dedicatedWorker.js
@@ -0,0 +1,3 @@
+self.onmessage = msg => {
+ self.postMessage(msg.data);
+};
diff --git a/toolkit/components/antitracking/test/browser/dynamicfpi_head.js b/toolkit/components/antitracking/test/browser/dynamicfpi_head.js
new file mode 100644
index 0000000000..6eaa620508
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/dynamicfpi_head.js
@@ -0,0 +1,180 @@
+/* vim: set ts=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/. */
+
+/* import-globals-from head.js */
+
+"use strict";
+
+this.DynamicFPIHelper = {
+ getTestPageConfig(runInSecureContext) {
+ if (runInSecureContext) {
+ return {
+ topPage: TEST_TOP_PAGE_HTTPS,
+ thirdPartyPage: TEST_4TH_PARTY_STORAGE_PAGE_HTTPS,
+ partitionKey: "(https,example.net)",
+ };
+ }
+ return {
+ topPage: TEST_TOP_PAGE,
+ thirdPartyPage: TEST_4TH_PARTY_STORAGE_PAGE,
+ partitionKey: "(http,example.net)",
+ };
+ },
+
+ runTest(
+ name,
+ callback,
+ cleanupFunction,
+ extraPrefs,
+ runInPrivateWindow,
+ { runInSecureContext = false } = {}
+ ) {
+ add_task(async _ => {
+ info(
+ "Starting test `" +
+ name +
+ "' with dynamic FPI running in a " +
+ (runInPrivateWindow ? "private" : "normal") +
+ " window..."
+ );
+
+ await SpecialPowers.flushPrefEnv();
+ await setCookieBehaviorPref(
+ BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN,
+ runInPrivateWindow
+ );
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.storage_access.enabled", true],
+ [
+ "privacy.partition.always_partition_third_party_non_cookie_storage",
+ true,
+ ],
+ ["privacy.trackingprotection.enabled", false],
+ ["privacy.trackingprotection.pbmode.enabled", false],
+ ["privacy.trackingprotection.annotate_channels", true],
+ ["privacy.dynamic_firstparty.use_site", true],
+ ["dom.security.https_first_pbm", false],
+ [
+ "privacy.restrict3rdpartystorage.userInteractionRequiredForHosts",
+ "not-tracking.example.com",
+ ],
+ ],
+ });
+
+ if (extraPrefs && Array.isArray(extraPrefs) && extraPrefs.length) {
+ await SpecialPowers.pushPrefEnv({ set: extraPrefs });
+ }
+
+ let win = window;
+ if (runInPrivateWindow) {
+ win = OpenBrowserWindow({ private: true });
+ await TestUtils.topicObserved("browser-delayed-startup-finished");
+ }
+
+ const { topPage, thirdPartyPage, partitionKey } =
+ this.getTestPageConfig(runInSecureContext);
+
+ info("Creating a new tab");
+ let tab = BrowserTestUtils.addTab(win.gBrowser, topPage);
+ win.gBrowser.selectedTab = tab;
+
+ let browser = win.gBrowser.getBrowserForTab(tab);
+ await BrowserTestUtils.browserLoaded(browser);
+
+ info("Check the cookieJarSettings of the browser object");
+ ok(
+ browser.cookieJarSettings,
+ "The browser object has the cookieJarSettings."
+ );
+ is(
+ browser.cookieJarSettings.cookieBehavior,
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN,
+ "The cookieJarSettings has the correct cookieBehavior"
+ );
+ is(
+ browser.cookieJarSettings.partitionKey,
+ partitionKey,
+ "The cookieJarSettings has the correct partitionKey"
+ );
+
+ info("Creating a 3rd party content");
+ await SpecialPowers.spawn(
+ browser,
+ [
+ {
+ page: thirdPartyPage,
+ callback: callback.toString(),
+ partitionKey,
+ },
+ ],
+ async obj => {
+ await new content.Promise(resolve => {
+ let ifr = content.document.createElement("iframe");
+ ifr.onload = async _ => {
+ await SpecialPowers.spawn(ifr, [obj], async obj => {
+ is(
+ content.document.nodePrincipal.originAttributes.partitionKey,
+ "",
+ "We don't have first-party set on nodePrincipal"
+ );
+ is(
+ content.document.effectiveStoragePrincipal.originAttributes
+ .partitionKey,
+ obj.partitionKey,
+ "We have first-party set on storagePrincipal"
+ );
+ });
+ info("Sending code to the 3rd party content");
+ ifr.contentWindow.postMessage(obj.callback, "*");
+ };
+
+ content.addEventListener("message", function msg(event) {
+ if (event.data.type == "finish") {
+ content.removeEventListener("message", msg);
+ resolve();
+ return;
+ }
+
+ if (event.data.type == "ok") {
+ ok(event.data.what, event.data.msg);
+ return;
+ }
+
+ if (event.data.type == "info") {
+ info(event.data.msg);
+ return;
+ }
+
+ ok(false, "Unknown message");
+ });
+
+ content.document.body.appendChild(ifr);
+ ifr.src = obj.page;
+ });
+ }
+ );
+
+ info("Removing the tab");
+ BrowserTestUtils.removeTab(tab);
+
+ if (runInPrivateWindow) {
+ win.close();
+ }
+ });
+
+ add_task(async _ => {
+ info("Cleaning up.");
+ if (cleanupFunction) {
+ await cleanupFunction();
+ }
+
+ // While running these tests we typically do not have enough idle time to do
+ // GC reliably, so force it here.
+ /* import-globals-from antitracking_head.js */
+ forceGC();
+ });
+ },
+};
diff --git a/toolkit/components/antitracking/test/browser/embedder.html b/toolkit/components/antitracking/test/browser/embedder.html
new file mode 100644
index 0000000000..1a517079e0
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/embedder.html
@@ -0,0 +1,4 @@
+<!DOCTYPE html>
+<script src="https://tracking.example.com/browser/toolkit/components/antitracking/test/browser/empty.js"></script>
+<script src="https://tracking.example.com/browser/toolkit/components/antitracking/test/browser/empty.js?redirect"></script>
+<script src="https://tracking.example.com/browser/toolkit/components/antitracking/test/browser/empty.js?redirect2"></script>
diff --git a/toolkit/components/antitracking/test/browser/embedder2.html b/toolkit/components/antitracking/test/browser/embedder2.html
new file mode 100644
index 0000000000..b1441894a4
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/embedder2.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script src="https://tracking.example.com/browser/toolkit/components/antitracking/test/browser/empty.js"></script>
+<script src="https://tracking.example.com/browser/toolkit/components/antitracking/test/browser/empty.js?redirect"></script>
+<script src="https://tracking.example.com/browser/toolkit/components/antitracking/test/browser/empty.js?redirect2"></script>
+</head>
+<body onload="parent.postMessage({data:document.querySelectorAll('script').length}, '*');"></body>
+</html>
diff --git a/toolkit/components/antitracking/test/browser/empty-altsvc.js b/toolkit/components/antitracking/test/browser/empty-altsvc.js
new file mode 100644
index 0000000000..3053583c76
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/empty-altsvc.js
@@ -0,0 +1 @@
+/* nothing here */
diff --git a/toolkit/components/antitracking/test/browser/empty-altsvc.js^headers^ b/toolkit/components/antitracking/test/browser/empty-altsvc.js^headers^
new file mode 100644
index 0000000000..70592d2f93
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/empty-altsvc.js^headers^
@@ -0,0 +1 @@
+Alt-Svc: h2=":12345"; ma=60
diff --git a/toolkit/components/antitracking/test/browser/empty.html b/toolkit/components/antitracking/test/browser/empty.html
new file mode 100644
index 0000000000..e20d67db57
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/empty.html
@@ -0,0 +1 @@
+<h1>Empty</h1>
diff --git a/toolkit/components/antitracking/test/browser/empty.js b/toolkit/components/antitracking/test/browser/empty.js
new file mode 100644
index 0000000000..3053583c76
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/empty.js
@@ -0,0 +1 @@
+/* nothing here */
diff --git a/toolkit/components/antitracking/test/browser/file_iframe_document_open.html b/toolkit/components/antitracking/test/browser/file_iframe_document_open.html
new file mode 100644
index 0000000000..fd2969f270
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/file_iframe_document_open.html
@@ -0,0 +1,19 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<meta charset="utf-8">
+<script>
+function run() {
+ let ifr = document.createElement("iframe");
+ document.body.appendChild(ifr);
+
+ let doc = ifr.contentWindow.document;
+ doc.open();
+ doc.write(`<script>document.cookie = "foo=bar"<\/script>`);
+ doc.close();
+}
+</script>
+</head>
+<body onLoad="run();">
+</body>
+</html>
diff --git a/toolkit/components/antitracking/test/browser/file_localStorage.html b/toolkit/components/antitracking/test/browser/file_localStorage.html
new file mode 100644
index 0000000000..54bad94bc9
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/file_localStorage.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Bug 1663192 - Accessing localStorage in a file urls</title>
+</head>
+<script>
+ window.addEventListener("DOMContentLoaded", () => {
+ let result = document.getElementById("result");
+
+ try {
+ window.localStorage.setItem("foo", "bar");
+ result.textContent = "PASS";
+ } catch (e) {
+ result.textContent = "FAIL";
+ }
+ }, { once: true });
+</script>
+<body>
+<a id="result"></a>
+</body>
+</html>
diff --git a/toolkit/components/antitracking/test/browser/file_saveAsImage.sjs b/toolkit/components/antitracking/test/browser/file_saveAsImage.sjs
new file mode 100644
index 0000000000..2afb7d435f
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/file_saveAsImage.sjs
@@ -0,0 +1,20 @@
+// small red image
+const IMAGE = atob(
+ "iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12" +
+ "P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=="
+);
+
+function handleRequest(aRequest, aResponse) {
+ aResponse.setStatusLine(aRequest.httpVersion, 200);
+
+ if (aRequest.queryString.includes("result")) {
+ aResponse.write(getState("hints") || 0);
+ setState("hints", "0");
+ } else {
+ let hints = parseInt(getState("hints") || 0) + 1;
+ setState("hints", hints.toString());
+
+ aResponse.setHeader("Content-Type", "image/png", false);
+ aResponse.write(IMAGE);
+ }
+}
diff --git a/toolkit/components/antitracking/test/browser/file_saveAsPageInfo.html b/toolkit/components/antitracking/test/browser/file_saveAsPageInfo.html
new file mode 100644
index 0000000000..aa3de2a555
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/file_saveAsPageInfo.html
@@ -0,0 +1,6 @@
+<html>
+<body>
+ <img src="http://example.net/browser/toolkit/components/antitracking/test/browser/raptor.jpg" id="image1">
+ <video src="http://example.net/browser/toolkit/components/antitracking/test/browser/file_video.ogv" id="video1"> </video>
+</body>
+</html>
diff --git a/toolkit/components/antitracking/test/browser/file_saveAsVideo.sjs b/toolkit/components/antitracking/test/browser/file_saveAsVideo.sjs
new file mode 100644
index 0000000000..10bf246f62
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/file_saveAsVideo.sjs
@@ -0,0 +1,38 @@
+const VIDEO = atob(
+ "GkXfo49CgoR3ZWJtQoeBAkKFgQIYU4BnI0nOEU2bdKpNu" +
+ "4tTq4QVSalmU6yBL027i1OrhBZUrmtTrIGmTbuLU6uEHF" +
+ "O7a1OsgdEVSalm8k2ApWxpYmVibWwyIHYwLjkuNyArIGx" +
+ "pYm1hdHJvc2thMiB2MC45LjhXQZ5ta2NsZWFuIDAuMi41" +
+ "IGZyb20gTGF2ZjUyLjU3LjFzpJC/CoyJjSbckmOytS9Se" +
+ "y3cRImIQK9AAAAAAABEYYgEHiZDUAHwABZUrmumrqTXgQ" +
+ "FzxYEBnIEAIrWcg3VuZIaFVl9WUDiDgQHgh7CCAUC6gfA" +
+ "cU7trzbuMs4EAt4f3gQHxggEju42zggMgt4f3gQHxgrZd" +
+ "u46zggZAt4j3gQHxgwFba7uOs4IJYLeI94EB8YMCAR+7j" +
+ "rOCDIC3iPeBAfGDAqggH0O2dSBibueBAKNbiYEAAIDQdw" +
+ "CdASpAAfAAAAcIhYWIhYSIAIIb347n/c/Z0BPBfjv7f+I" +
+ "/6df275Wbh/XPuZ+qv9k58KrftA9tvkP+efiN/ovmd/if" +
+ "9L/ef2z+Xv+H/rv+39xD/N/zL+nfid71363e9X+pf6/+q" +
+ "+x7+W/0P/H/233Zf6n/Wv696APuDf0b+YdcL+2HsUfxr+" +
+ "gfP/91P+F/yH9K+H79Z/7j/Xvhj/VjVsvwHXt/n/qz9U/" +
+ "w749+c/g="
+);
+
+function handleRequest(aRequest, aResponse) {
+ aResponse.setStatusLine(aRequest.httpVersion, 200);
+
+ if (aRequest.queryString.includes("result")) {
+ aResponse.write(getState("hints") || 0);
+ setState("hints", "0");
+ } else {
+ let hints = parseInt(getState("hints") || 0) + 1;
+ setState("hints", hints.toString());
+
+ aResponse.setHeader("Content-Type", "video/webm", false);
+ aResponse.setHeader(
+ "Cache-Control",
+ "public, max-age=604800, immutable",
+ false
+ );
+ aResponse.write(VIDEO);
+ }
+}
diff --git a/toolkit/components/antitracking/test/browser/file_stripping.html b/toolkit/components/antitracking/test/browser/file_stripping.html
new file mode 100644
index 0000000000..60d9840a0f
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/file_stripping.html
@@ -0,0 +1,20 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<meta charset="utf8">
+<script>
+ onmessage = event => {
+ switch (event.data.type) {
+ case "window-open":
+ window.open(event.data.url);
+ break;
+ case "script":
+ window.location.href = event.data.url;
+ break
+ }
+ };
+</script>
+</head>
+<body>
+</body>
+</html>
diff --git a/toolkit/components/antitracking/test/browser/file_video.ogv b/toolkit/components/antitracking/test/browser/file_video.ogv
new file mode 100644
index 0000000000..68dee3cf2b
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/file_video.ogv
Binary files differ
diff --git a/toolkit/components/antitracking/test/browser/file_ws_handshake_delay_wsh.py b/toolkit/components/antitracking/test/browser/file_ws_handshake_delay_wsh.py
new file mode 100644
index 0000000000..412ac2e24c
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/file_ws_handshake_delay_wsh.py
@@ -0,0 +1,28 @@
+import time
+
+from mod_pywebsocket import msgutil
+
+
+def web_socket_do_extra_handshake(request):
+ # # must set request.ws_protocol to the selected version from ws_requested_protocols
+ for x in request.ws_requested_protocols:
+ if x != "test-does-not-exist":
+ request.ws_protocol = x
+ break
+
+ if request.ws_protocol == "test-3":
+ time.sleep(3)
+ elif request.ws_protocol == "test-6":
+ time.sleep(6)
+ else:
+ pass
+
+
+def web_socket_passive_closing_handshake(request):
+ if request.ws_close_code == 1005:
+ return None, None
+ return request.ws_close_code, request.ws_close_reason
+
+
+def web_socket_transfer_data(request):
+ msgutil.close_connection(request)
diff --git a/toolkit/components/antitracking/test/browser/head.js b/toolkit/components/antitracking/test/browser/head.js
new file mode 100644
index 0000000000..da99cb433b
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/head.js
@@ -0,0 +1,149 @@
+/* vim: set ts=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/. */
+
+"use strict";
+
+const TEST_DOMAIN = "http://example.net/";
+const TEST_DOMAIN_HTTPS = "https://example.net/";
+const TEST_DOMAIN_2 = "http://xn--exmple-cua.test/";
+const TEST_DOMAIN_3 = "https://xn--hxajbheg2az3al.xn--jxalpdlp/";
+const TEST_DOMAIN_4 = "http://prefixexample.com/";
+const TEST_DOMAIN_5 = "http://test/";
+const TEST_DOMAIN_6 = "http://mochi.test:8888/";
+const TEST_DOMAIN_7 = "http://example.com/";
+const TEST_DOMAIN_8 = "http://www.example.com/";
+const TEST_3RD_PARTY_DOMAIN = "https://tracking.example.org/";
+const TEST_3RD_PARTY_DOMAIN_HTTP = "http://tracking.example.org/";
+const TEST_3RD_PARTY_DOMAIN_TP = "https://tracking.example.com/";
+const TEST_3RD_PARTY_DOMAIN_STP = "https://social-tracking.example.org/";
+const TEST_4TH_PARTY_DOMAIN = "http://not-tracking.example.com/";
+const TEST_4TH_PARTY_DOMAIN_HTTPS = "https://not-tracking.example.com/";
+const TEST_ANOTHER_3RD_PARTY_DOMAIN_HTTP =
+ "http://another-tracking.example.net/";
+const TEST_ANOTHER_3RD_PARTY_DOMAIN_HTTPS =
+ "https://another-tracking.example.net/";
+const TEST_ANOTHER_3RD_PARTY_DOMAIN = SpecialPowers.useRemoteSubframes
+ ? TEST_ANOTHER_3RD_PARTY_DOMAIN_HTTP
+ : TEST_ANOTHER_3RD_PARTY_DOMAIN_HTTPS;
+const TEST_EMAIL_TRACKER_DOMAIN = "http://email-tracking.example.org/";
+
+const TEST_PATH = "browser/toolkit/components/antitracking/test/browser/";
+
+const TEST_TOP_PAGE = TEST_DOMAIN + TEST_PATH + "page.html";
+const TEST_TOP_PAGE_HTTPS = TEST_DOMAIN_HTTPS + TEST_PATH + "page.html";
+const TEST_TOP_PAGE_2 = TEST_DOMAIN_2 + TEST_PATH + "page.html";
+const TEST_TOP_PAGE_3 = TEST_DOMAIN_3 + TEST_PATH + "page.html";
+const TEST_TOP_PAGE_4 = TEST_DOMAIN_4 + TEST_PATH + "page.html";
+const TEST_TOP_PAGE_5 = TEST_DOMAIN_5 + TEST_PATH + "page.html";
+const TEST_TOP_PAGE_6 = TEST_DOMAIN_6 + TEST_PATH + "page.html";
+const TEST_TOP_PAGE_7 = TEST_DOMAIN_7 + TEST_PATH + "page.html";
+const TEST_TOP_PAGE_8 = TEST_DOMAIN_8 + TEST_PATH + "page.html";
+const TEST_EMBEDDER_PAGE = TEST_DOMAIN + TEST_PATH + "embedder.html";
+const TEST_POPUP_PAGE = TEST_DOMAIN + TEST_PATH + "popup.html";
+const TEST_IFRAME_PAGE = TEST_DOMAIN + TEST_PATH + "iframe.html";
+const TEST_3RD_PARTY_PAGE = TEST_3RD_PARTY_DOMAIN + TEST_PATH + "3rdParty.html";
+const TEST_3RD_PARTY_PAGE_HTTP =
+ TEST_3RD_PARTY_DOMAIN_HTTP + TEST_PATH + "3rdParty.html";
+const TEST_3RD_PARTY_PAGE_WO =
+ TEST_3RD_PARTY_DOMAIN + TEST_PATH + "3rdPartyWO.html";
+const TEST_3RD_PARTY_PAGE_UI =
+ TEST_3RD_PARTY_DOMAIN + TEST_PATH + "3rdPartyUI.html";
+const TEST_3RD_PARTY_PAGE_WITH_SVG =
+ TEST_3RD_PARTY_DOMAIN + TEST_PATH + "3rdPartySVG.html";
+const TEST_3RD_PARTY_PAGE_RELAY =
+ TEST_4TH_PARTY_DOMAIN + TEST_PATH + "3rdPartyRelay.html";
+const TEST_4TH_PARTY_PAGE = TEST_4TH_PARTY_DOMAIN + TEST_PATH + "3rdParty.html";
+const TEST_ANOTHER_3RD_PARTY_PAGE =
+ TEST_ANOTHER_3RD_PARTY_DOMAIN + TEST_PATH + "3rdParty.html";
+const TEST_ANOTHER_3RD_PARTY_PAGE_HTTPS =
+ TEST_ANOTHER_3RD_PARTY_DOMAIN_HTTPS + TEST_PATH + "3rdParty.html";
+const TEST_3RD_PARTY_STORAGE_PAGE =
+ TEST_3RD_PARTY_DOMAIN_HTTP + TEST_PATH + "3rdPartyStorage.html";
+const TEST_3RD_PARTY_PAGE_WORKER =
+ TEST_3RD_PARTY_DOMAIN + TEST_PATH + "3rdPartyWorker.html";
+const TEST_3RD_PARTY_PARTITIONED_PAGE =
+ TEST_3RD_PARTY_DOMAIN + TEST_PATH + "3rdPartyPartitioned.html";
+const TEST_4TH_PARTY_STORAGE_PAGE =
+ TEST_4TH_PARTY_DOMAIN + TEST_PATH + "3rdPartyStorage.html";
+const TEST_4TH_PARTY_STORAGE_PAGE_HTTPS =
+ TEST_4TH_PARTY_DOMAIN_HTTPS + TEST_PATH + "3rdPartyStorage.html";
+const TEST_4TH_PARTY_PARTITIONED_PAGE =
+ TEST_4TH_PARTY_DOMAIN + TEST_PATH + "3rdPartyPartitioned.html";
+
+const BEHAVIOR_ACCEPT = Ci.nsICookieService.BEHAVIOR_ACCEPT;
+const BEHAVIOR_REJECT = Ci.nsICookieService.BEHAVIOR_REJECT;
+const BEHAVIOR_LIMIT_FOREIGN = Ci.nsICookieService.BEHAVIOR_LIMIT_FOREIGN;
+const BEHAVIOR_REJECT_FOREIGN = Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN;
+const BEHAVIOR_REJECT_TRACKER = Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER;
+const BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN =
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN;
+
+let originalRequestLongerTimeout = requestLongerTimeout;
+// eslint-disable-next-line no-global-assign
+requestLongerTimeout = function AntiTrackingRequestLongerTimeout(factor) {
+ let ccovMultiplier = AppConstants.MOZ_CODE_COVERAGE ? 2 : 1;
+ let fissionMultiplier = SpecialPowers.useRemoteSubframes ? 2 : 1;
+ originalRequestLongerTimeout(ccovMultiplier * fissionMultiplier * factor);
+};
+
+requestLongerTimeout(3);
+
+const { UrlClassifierTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/UrlClassifierTestUtils.sys.mjs"
+);
+
+const { PermissionTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PermissionTestUtils.sys.mjs"
+);
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/toolkit/components/antitracking/test/browser/antitracking_head.js",
+ this
+);
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/toolkit/components/antitracking/test/browser/partitionedstorage_head.js",
+ this
+);
+
+function setCookieBehaviorPref(cookieBehavior, runInPrivateWindow) {
+ let cbRegular;
+ let cbPrivate;
+
+ // Set different cookieBehaviors to regular mode and private mode so that we
+ // can make sure these two prefs don't interfere with each other for all
+ // tests.
+ if (runInPrivateWindow) {
+ cbPrivate = cookieBehavior;
+
+ let defaultPrefBranch = Services.prefs.getDefaultBranch("");
+ // In order to test the default private cookieBehavior pref, we need to set
+ // the regular pref to the default value because we don't want the private
+ // pref to mirror the regular pref in this case.
+ //
+ // Note that the private pref will mirror the regular pref if the private
+ // pref is in default value and the regular pref is not in default value.
+ if (
+ cookieBehavior ==
+ defaultPrefBranch.getIntPref("network.cookie.cookieBehavior.pbmode")
+ ) {
+ cbRegular = defaultPrefBranch.getIntPref("network.cookie.cookieBehavior");
+ } else {
+ cbRegular =
+ cookieBehavior == BEHAVIOR_ACCEPT ? BEHAVIOR_REJECT : BEHAVIOR_ACCEPT;
+ }
+ } else {
+ cbRegular = cookieBehavior;
+ cbPrivate =
+ cookieBehavior == BEHAVIOR_ACCEPT ? BEHAVIOR_REJECT : BEHAVIOR_ACCEPT;
+ }
+
+ return SpecialPowers.pushPrefEnv({
+ set: [
+ ["network.cookie.cookieBehavior", cbRegular],
+ ["network.cookie.cookieBehavior.pbmode", cbPrivate],
+ ],
+ });
+}
diff --git a/toolkit/components/antitracking/test/browser/iframe.html b/toolkit/components/antitracking/test/browser/iframe.html
new file mode 100644
index 0000000000..85d37ed7fa
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/iframe.html
@@ -0,0 +1,8 @@
+<html>
+<head>
+ <title>Just a first-level iframe</title>
+</head>
+<body>
+ <h1>This is the first-level iframe</h1>
+</body>
+</html>
diff --git a/toolkit/components/antitracking/test/browser/image.sjs b/toolkit/components/antitracking/test/browser/image.sjs
new file mode 100644
index 0000000000..145ff1f0a4
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/image.sjs
@@ -0,0 +1,22 @@
+// A 1x1 PNG image.
+// Source: https://commons.wikimedia.org/wiki/File:1x1.png (Public Domain)
+const IMAGE = atob(
+ "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEUAA" +
+ "ACnej3aAAAAAXRSTlMAQObYZgAAAApJREFUCNdjYAAAAAIAAeIhvDMAAAAASUVORK5CYII="
+);
+
+function handleRequest(aRequest, aResponse) {
+ aResponse.setStatusLine(aRequest.httpVersion, 200);
+
+ if (aRequest.queryString.includes("result")) {
+ aResponse.write(getState("hints") || 0);
+ setState("hints", "0");
+ } else {
+ let hints = parseInt(getState("hints") || 0) + 1;
+ setState("hints", hints.toString());
+
+ aResponse.setHeader("Set-Cookie", "foopy=1");
+ aResponse.setHeader("Content-Type", "image/png", false);
+ aResponse.write(IMAGE);
+ }
+}
diff --git a/toolkit/components/antitracking/test/browser/imageCacheWorker.js b/toolkit/components/antitracking/test/browser/imageCacheWorker.js
new file mode 100644
index 0000000000..d11221112c
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/imageCacheWorker.js
@@ -0,0 +1,78 @@
+/* import-globals-from head.js */
+/* import-globals-from antitracking_head.js */
+/* import-globals-from browser_imageCache4.js */
+
+AntiTracking.runTest(
+ "Image cache - should load the image three times.",
+ // blocking callback
+ async _ => {
+ // Let's load the image twice here.
+ let img = document.createElement("img");
+ document.body.appendChild(img);
+ img.src =
+ "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/image.sjs";
+ await new Promise(resolve => {
+ img.onload = resolve;
+ });
+ ok(true, "Image 1 loaded");
+
+ img = document.createElement("img");
+ document.body.appendChild(img);
+ img.src =
+ "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/image.sjs";
+ await new Promise(resolve => {
+ img.onload = resolve;
+ });
+ ok(true, "Image 2 loaded");
+ },
+
+ // non-blocking callback
+ {
+ runExtraTests: false,
+ cookieBehavior,
+ blockingByAllowList,
+ expectedBlockingNotifications,
+ callback: async _ => {
+ // Let's load the image twice here as well.
+ let img = document.createElement("img");
+ document.body.appendChild(img);
+ img.src =
+ "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/image.sjs";
+ await new Promise(resolve => {
+ img.onload = resolve;
+ });
+ ok(true, "Image 3 loaded");
+
+ img = document.createElement("img");
+ document.body.appendChild(img);
+ img.src =
+ "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/image.sjs";
+ await new Promise(resolve => {
+ img.onload = resolve;
+ });
+ ok(true, "Image 4 loaded");
+ },
+ },
+ null, // cleanup function
+ null, // no extra prefs
+ false, // no window open test
+ false, // no user-interaction test
+ expectedBlockingNotifications
+);
+
+// We still want to see just expected requests.
+add_task(async _ => {
+ await fetch(
+ "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/image.sjs?result"
+ )
+ .then(r => r.text())
+ .then(text => {
+ is(text, "2", "The image should be loaded correctly.");
+ });
+
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+});
diff --git a/toolkit/components/antitracking/test/browser/localStorage.html b/toolkit/components/antitracking/test/browser/localStorage.html
new file mode 100644
index 0000000000..e08c25f2c4
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/localStorage.html
@@ -0,0 +1,68 @@
+<h1>Here a tracker!</h1>
+<script>
+
+if (window.opener) {
+ SpecialPowers.wrap(document).userInteractionForTesting();
+ localStorage.foo = "opener" + Math.random();
+ // Don't call window.close immediatelly. It can happen that adding the
+ // "storage" event listener below takes more time than usual (it may need to
+ // synchronously subscribe in the parent process to receive storage
+ // notifications). Spending more time in the initial script can prevent
+ // the "load" event from being fired for the window opened by "open and test".
+ setTimeout(() => {
+ window.close();
+ }, 0);
+}
+
+if (parent) {
+ window.onmessage = e => {
+ if (e.data == "test") {
+ let status;
+ try {
+ localStorage.foo = "value" + Math.random();
+ status = true;
+ } catch (e) {
+ status = false;
+ }
+
+ parent.postMessage({type: "test", status }, "*");
+ return;
+ }
+
+ if (e.data == "open") {
+ window.open("localStorage.html");
+ return;
+ }
+
+ if (e.data == "open and test") {
+ let w = window.open("localStorage.html");
+ w.addEventListener("load", _ => {
+ let status;
+ try {
+ localStorage.foo = "value" + Math.random();
+ status = true;
+ } catch (e) {
+ status = false;
+ }
+
+ parent.postMessage({type: "test", status }, "*");
+ }, {once: true});
+ }
+ };
+
+ window.addEventListener("storage", e => {
+ let fromOpener = localStorage.foo.startsWith("opener");
+
+ let status;
+ try {
+ localStorage.foo = "value" + Math.random();
+ status = true;
+ } catch (e) {
+ status = false;
+ }
+
+ parent.postMessage({type: "test", status: status && fromOpener }, "*");
+ });
+}
+
+</script>
diff --git a/toolkit/components/antitracking/test/browser/localStorageEvents.html b/toolkit/components/antitracking/test/browser/localStorageEvents.html
new file mode 100644
index 0000000000..737d1e0cab
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/localStorageEvents.html
@@ -0,0 +1,30 @@
+<script>
+
+let eventCounter = 0;
+
+onmessage = e => {
+ if (e.data == "getValue") {
+ parent.postMessage(localStorage.foo, "*");
+ return;
+ }
+
+ if (e.data == "setValue") {
+ localStorage.foo = "tracker-" + Math.random();
+ return;
+ }
+
+ if (e.data == "getEvents") {
+ parent.postMessage(eventCounter, "*");
+ return;
+ }
+
+ if (e.data == "reload") {
+ window.location.reload();
+ }
+};
+
+addEventListener("storage", _ => {
+ ++eventCounter;
+});
+
+</script>
diff --git a/toolkit/components/antitracking/test/browser/matchAll.js b/toolkit/components/antitracking/test/browser/matchAll.js
new file mode 100644
index 0000000000..8f112b0804
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/matchAll.js
@@ -0,0 +1,16 @@
+self.addEventListener("message", async e => {
+ let clients = await self.clients.matchAll({
+ type: "window",
+ includeUncontrolled: true,
+ });
+
+ let hasWindow = false;
+ for (let client of clients) {
+ if (e.data == client.url) {
+ hasWindow = true;
+ break;
+ }
+ }
+
+ e.source.postMessage(hasWindow);
+});
diff --git a/toolkit/components/antitracking/test/browser/page.html b/toolkit/components/antitracking/test/browser/page.html
new file mode 100644
index 0000000000..a99e8be179
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/page.html
@@ -0,0 +1,8 @@
+<html>
+<head>
+ <title>Just a top-level page</title>
+</head>
+<body>
+ <h1>This is the top-level page</h1>
+</body>
+</html>
diff --git a/toolkit/components/antitracking/test/browser/partitionedSharedWorker.js b/toolkit/components/antitracking/test/browser/partitionedSharedWorker.js
new file mode 100644
index 0000000000..5ac4ec9f27
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/partitionedSharedWorker.js
@@ -0,0 +1,17 @@
+let value = "";
+self.onconnect = e => {
+ e.ports[0].onmessage = event => {
+ if (event.data.what === "get") {
+ e.ports[0].postMessage(value);
+ return;
+ }
+
+ if (event.data.what === "put") {
+ value = event.data.value;
+ return;
+ }
+
+ // Error.
+ e.ports[0].postMessage(-1);
+ };
+};
diff --git a/toolkit/components/antitracking/test/browser/partitionedstorage_head.js b/toolkit/components/antitracking/test/browser/partitionedstorage_head.js
new file mode 100644
index 0000000000..42574fdb93
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/partitionedstorage_head.js
@@ -0,0 +1,455 @@
+/* vim: set ts=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/. */
+
+/* import-globals-from head.js */
+
+"use strict";
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/toolkit/components/antitracking/test/browser/dynamicfpi_head.js",
+ this
+);
+
+this.PartitionedStorageHelper = {
+ runTestInNormalAndPrivateMode(name, callback, cleanupFunction, extraPrefs) {
+ // Normal mode
+ this.runTest(name, callback, cleanupFunction, extraPrefs, {
+ runInPrivateWindow: false,
+ });
+
+ // Private mode
+ this.runTest(name, callback, cleanupFunction, extraPrefs, {
+ runInPrivateWindow: true,
+ });
+ },
+
+ runTest(
+ name,
+ callback,
+ cleanupFunction,
+ extraPrefs,
+ { runInPrivateWindow = false, runInSecureContext = false } = {}
+ ) {
+ DynamicFPIHelper.runTest(
+ name,
+ callback,
+ cleanupFunction,
+ extraPrefs,
+ runInPrivateWindow,
+ { runInSecureContext }
+ );
+ },
+
+ runPartitioningTestInNormalAndPrivateMode(
+ name,
+ testCategory,
+ getDataCallback,
+ addDataCallback,
+ cleanupFunction,
+ expectUnpartition = false
+ ) {
+ // Normal mode
+ this.runPartitioningTest(
+ name,
+ testCategory,
+ getDataCallback,
+ addDataCallback,
+ cleanupFunction,
+ expectUnpartition,
+ false
+ );
+
+ // Private mode
+ this.runPartitioningTest(
+ name,
+ testCategory,
+ getDataCallback,
+ addDataCallback,
+ cleanupFunction,
+ expectUnpartition,
+ true
+ );
+ },
+
+ runPartitioningTest(
+ name,
+ testCategory,
+ getDataCallback,
+ addDataCallback,
+ cleanupFunction,
+ expectUnpartition,
+ runInPrivateWindow = false
+ ) {
+ for (let variant of ["normal", "initial-aboutblank"]) {
+ for (let limitForeignContexts of [false, true]) {
+ this.runPartitioningTestInner(
+ name,
+ testCategory,
+ getDataCallback,
+ addDataCallback,
+ cleanupFunction,
+ variant,
+ runInPrivateWindow,
+ limitForeignContexts,
+ expectUnpartition
+ );
+ }
+ }
+ },
+
+ runPartitioningTestInner(
+ name,
+ testCategory,
+ getDataCallback,
+ addDataCallback,
+ cleanupFunction,
+ variant,
+ runInPrivateWindow,
+ limitForeignContexts,
+ expectUnpartition
+ ) {
+ add_task(async _ => {
+ info(
+ "Starting test `" +
+ name +
+ "' testCategory `" +
+ testCategory +
+ "' variant `" +
+ variant +
+ "' in a " +
+ (runInPrivateWindow ? "private" : "normal") +
+ " window " +
+ (limitForeignContexts ? "with" : "without") +
+ " limitForeignContexts to check that 2 tabs are correctly partititioned"
+ );
+
+ await SpecialPowers.flushPrefEnv();
+ await setCookieBehaviorPref(
+ BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN,
+ runInPrivateWindow
+ );
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.storage_access.enabled", true],
+ [
+ "privacy.partition.always_partition_third_party_non_cookie_storage",
+ true,
+ ],
+ ["privacy.dynamic_firstparty.limitForeign", limitForeignContexts],
+ ["privacy.trackingprotection.enabled", false],
+ ["privacy.trackingprotection.pbmode.enabled", false],
+ ["privacy.trackingprotection.annotate_channels", true],
+ ["dom.security.https_first_pbm", false],
+ [
+ "privacy.restrict3rdpartystorage.userInteractionRequiredForHosts",
+ "not-tracking.example.com",
+ ],
+ ],
+ });
+
+ let win = window;
+ if (runInPrivateWindow) {
+ win = OpenBrowserWindow({ private: true });
+ await TestUtils.topicObserved("browser-delayed-startup-finished");
+ }
+
+ info("Creating the first tab");
+ let tab1 = BrowserTestUtils.addTab(win.gBrowser, TEST_TOP_PAGE);
+ win.gBrowser.selectedTab = tab1;
+
+ let browser1 = win.gBrowser.getBrowserForTab(tab1);
+ await BrowserTestUtils.browserLoaded(browser1);
+
+ info("Creating the second tab");
+ let tab2 = BrowserTestUtils.addTab(win.gBrowser, TEST_TOP_PAGE_6);
+ win.gBrowser.selectedTab = tab2;
+
+ let browser2 = win.gBrowser.getBrowserForTab(tab2);
+ await BrowserTestUtils.browserLoaded(browser2);
+
+ info("Creating the third tab");
+ let tab3 = BrowserTestUtils.addTab(
+ win.gBrowser,
+ TEST_4TH_PARTY_PARTITIONED_PAGE
+ );
+ win.gBrowser.selectedTab = tab3;
+
+ let browser3 = win.gBrowser.getBrowserForTab(tab3);
+ await BrowserTestUtils.browserLoaded(browser3);
+
+ // Use the same URL as first tab to check partitioned data
+ info("Creating the forth tab");
+ let tab4 = BrowserTestUtils.addTab(win.gBrowser, TEST_TOP_PAGE);
+ win.gBrowser.selectedTab = tab4;
+
+ let browser4 = win.gBrowser.getBrowserForTab(tab4);
+ await BrowserTestUtils.browserLoaded(browser4);
+
+ async function getDataFromThirdParty(browser, result) {
+ // Overwrite the special case here since third party cookies are not
+ // avilable when `limitForeignContexts` is enabled.
+ if (testCategory === "cookies" && limitForeignContexts) {
+ info("overwrite result to empty");
+ result = "";
+ }
+
+ await SpecialPowers.spawn(
+ browser,
+ [
+ {
+ page: TEST_4TH_PARTY_PARTITIONED_PAGE + "?variant=" + variant,
+ getDataCallback: getDataCallback.toString(),
+ result,
+ },
+ ],
+ async obj => {
+ await new content.Promise(resolve => {
+ let ifr = content.document.createElement("iframe");
+ ifr.onload = __ => {
+ info("Sending code to the 3rd party content");
+ ifr.contentWindow.postMessage({ cb: obj.getDataCallback }, "*");
+ };
+
+ content.addEventListener(
+ "message",
+ function msg(event) {
+ is(
+ event.data,
+ obj.result,
+ "Partitioned cookie jar has value: " + obj.result
+ );
+ resolve();
+ },
+ { once: true }
+ );
+
+ content.document.body.appendChild(ifr);
+ ifr.src = obj.page;
+ });
+ }
+ );
+ }
+
+ async function getDataFromFirstParty(browser, result) {
+ await SpecialPowers.spawn(
+ browser,
+ [
+ {
+ getDataCallback: getDataCallback.toString(),
+ result,
+ variant,
+ },
+ ],
+ async obj => {
+ let runnableStr = `(() => {return (${obj.getDataCallback});})();`;
+ let runnable = eval(runnableStr); // eslint-disable-line no-eval
+ let win = content;
+ if (obj.variant == "initial-aboutblank") {
+ let i = win.document.createElement("iframe");
+ i.src = "about:blank";
+ win.document.body.appendChild(i);
+ // override win to make it point to the initial about:blank window
+ win = i.contentWindow;
+ }
+
+ let result = await runnable.call(content, win);
+ is(
+ result,
+ obj.result,
+ "Partitioned cookie jar is empty: " + obj.result
+ );
+ }
+ );
+ }
+
+ info("Checking 3rd party has an empty cookie jar in first tab");
+ await getDataFromThirdParty(browser1, "");
+
+ info("Checking 3rd party has an empty cookie jar in second tab");
+ await getDataFromThirdParty(browser2, "");
+
+ info("Checking first party has an empty cookie jar in third tab");
+ await getDataFromFirstParty(browser3, "");
+
+ info("Checking 3rd party has an empty cookie jar in forth tab");
+ await getDataFromThirdParty(browser4, "");
+
+ async function createDataInThirdParty(browser, value) {
+ await SpecialPowers.spawn(
+ browser,
+ [
+ {
+ page: TEST_4TH_PARTY_PARTITIONED_PAGE + "?variant=" + variant,
+ addDataCallback: addDataCallback.toString(),
+ value,
+ },
+ ],
+ async obj => {
+ await new content.Promise(resolve => {
+ let ifr = content.document.getElementsByTagName("iframe")[0];
+ content.addEventListener(
+ "message",
+ function msg(event) {
+ ok(event.data, "Data created");
+ resolve();
+ },
+ { once: true }
+ );
+
+ ifr.contentWindow.postMessage(
+ {
+ cb: obj.addDataCallback,
+ value: obj.value,
+ },
+ "*"
+ );
+ });
+ }
+ );
+ }
+
+ async function createDataInFirstParty(browser, value) {
+ await SpecialPowers.spawn(
+ browser,
+ [
+ {
+ addDataCallback: addDataCallback.toString(),
+ value,
+ variant,
+ },
+ ],
+ async obj => {
+ let runnableStr = `(() => {return (${obj.addDataCallback});})();`;
+ let runnable = eval(runnableStr); // eslint-disable-line no-eval
+ let win = content;
+ if (obj.variant == "initial-aboutblank") {
+ let i = win.document.createElement("iframe");
+ i.src = "about:blank";
+ win.document.body.appendChild(i);
+ // override win to make it point to the initial about:blank window
+ win = i.contentWindow;
+ }
+
+ let result = await runnable.call(content, win, obj.value);
+ ok(result, "Data created");
+ }
+ );
+ }
+
+ info("Creating data in the first tab");
+ await createDataInThirdParty(browser1, "A");
+
+ info("Creating data in the second tab");
+ await createDataInThirdParty(browser2, "B");
+
+ // Before writing browser4, check data written by browser1
+ info("First tab should still have just 'A'");
+ await getDataFromThirdParty(browser1, "A");
+ info("Forth tab should still have just 'A'");
+ await getDataFromThirdParty(browser4, "A");
+
+ // Ensure to create data in the forth tab before the third tab,
+ // otherwise cookie will be written successfully due to prior cookie
+ // of the base domain exists.
+ info("Creating data in the forth tab");
+ await createDataInThirdParty(browser4, "D");
+
+ info("Creating data in the third tab");
+ await createDataInFirstParty(browser3, "C");
+
+ // read all tabs
+ info("First tab should be changed to 'D'");
+ await getDataFromThirdParty(browser1, "D");
+
+ info("Second tab should still have just 'B'");
+ await getDataFromThirdParty(browser2, "B");
+
+ info("Third tab should still have just 'C'");
+ await getDataFromFirstParty(browser3, "C");
+
+ info("Forth tab should still have just 'D'");
+ await getDataFromThirdParty(browser4, "D");
+
+ async function setStorageAccessForThirdParty(browser) {
+ info(`Setting permission for ${browser.currentURI.spec}`);
+ let type = "3rdPartyStorage^http://not-tracking.example.com";
+ let permission = Services.perms.ALLOW_ACTION;
+ let expireType = Services.perms.EXPIRE_SESSION;
+ Services.perms.addFromPrincipal(
+ browser.contentPrincipal,
+ type,
+ permission,
+ expireType,
+ 0
+ );
+ // Wait for permission to be set successfully
+ let originAttributes = runInPrivateWindow
+ ? { privateBrowsingId: 1 }
+ : {};
+ await new Promise(resolve => {
+ let id = setInterval(async _ => {
+ if (
+ await SpecialPowers.testPermission(type, permission, {
+ url: browser.currentURI.spec,
+ originAttributes,
+ })
+ ) {
+ clearInterval(id);
+ resolve();
+ }
+ }, 0);
+ });
+ }
+
+ if (!expectUnpartition) {
+ info("Setting Storage access for third parties");
+
+ await setStorageAccessForThirdParty(browser1);
+ await setStorageAccessForThirdParty(browser2);
+ await setStorageAccessForThirdParty(browser3);
+ await setStorageAccessForThirdParty(browser4);
+
+ info("Done setting Storage access for third parties");
+
+ // read all tabs
+ info("First tab should still have just 'D'");
+ await getDataFromThirdParty(browser1, "D");
+
+ info("Second tab should still have just 'B'");
+ await getDataFromThirdParty(browser2, "B");
+
+ info("Third tab should still have just 'C'");
+ await getDataFromFirstParty(browser3, "C");
+
+ info("Forth tab should still have just 'D'");
+ await getDataFromThirdParty(browser4, "D");
+ }
+
+ info("Done checking departitioned state");
+
+ info("Removing the tabs");
+ BrowserTestUtils.removeTab(tab1);
+ BrowserTestUtils.removeTab(tab2);
+ BrowserTestUtils.removeTab(tab3);
+ BrowserTestUtils.removeTab(tab4);
+
+ if (runInPrivateWindow) {
+ win.close();
+ }
+ });
+
+ add_task(async _ => {
+ info("Cleaning up.");
+ if (cleanupFunction) {
+ await cleanupFunction();
+ }
+
+ // While running these tests we typically do not have enough idle time to do
+ // GC reliably, so force it here.
+ /* import-globals-from antitracking_head.js */
+ forceGC();
+ });
+ },
+};
diff --git a/toolkit/components/antitracking/test/browser/popup.html b/toolkit/components/antitracking/test/browser/popup.html
new file mode 100644
index 0000000000..f195add788
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/popup.html
@@ -0,0 +1,11 @@
+<html>
+<head>
+ <title>Just a popup that does a redirect</title>
+</head>
+<body>
+ <h1>Just a popup that does a redirect</h1>
+ <script>
+ window.location = "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/3rdPartyOpenUI.html";
+ </script>
+</body>
+</html>
diff --git a/toolkit/components/antitracking/test/browser/raptor.jpg b/toolkit/components/antitracking/test/browser/raptor.jpg
new file mode 100644
index 0000000000..243ba9e2d4
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/raptor.jpg
Binary files differ
diff --git a/toolkit/components/antitracking/test/browser/redirect.sjs b/toolkit/components/antitracking/test/browser/redirect.sjs
new file mode 100644
index 0000000000..4645aedca5
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/redirect.sjs
@@ -0,0 +1,11 @@
+function handleRequest(aRequest, aResponse) {
+ aResponse.setStatusLine(aRequest.httpVersion, 302);
+
+ let query = aRequest.queryString;
+ let locations = query.split("|");
+ let nextLocation = locations.shift();
+ if (locations.length) {
+ nextLocation += "?" + locations.join("|");
+ }
+ aResponse.setHeader("Location", nextLocation);
+}
diff --git a/toolkit/components/antitracking/test/browser/referrer.sjs b/toolkit/components/antitracking/test/browser/referrer.sjs
new file mode 100644
index 0000000000..8e80eb7fa3
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/referrer.sjs
@@ -0,0 +1,49 @@
+// A 1x1 PNG image.
+// Source: https://commons.wikimedia.org/wiki/File:1x1.png (Public Domain)
+const IMAGE = atob(
+ "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEUAA" +
+ "ACnej3aAAAAAXRSTlMAQObYZgAAAApJREFUCNdjYAAAAAIAAeIhvDMAAAAASUVORK5CYII="
+);
+
+const IFRAME =
+ "<!DOCTYPE html>\n" +
+ "<script>\n" +
+ "onmessage = event => {\n" +
+ "parent.postMessage(document.referrer, '*');\n" +
+ "};\n" +
+ "</script>";
+
+function handleRequest(aRequest, aResponse) {
+ aResponse.setStatusLine(aRequest.httpVersion, 200);
+
+ let key;
+ if (aRequest.queryString.includes("what=script")) {
+ key = "script";
+ } else if (aRequest.queryString.includes("what=image")) {
+ key = "image";
+ } else {
+ key = "iframe";
+ }
+
+ if (aRequest.queryString.includes("result")) {
+ aResponse.write(getState(key));
+ setState(key, "");
+ return;
+ }
+
+ if (aRequest.hasHeader("Referer")) {
+ let referrer = aRequest.getHeader("Referer");
+ setState(key, referrer);
+ }
+
+ if (key == "script") {
+ aResponse.setHeader("Content-Type", "text/javascript", false);
+ aResponse.write("42;");
+ } else if (key == "image") {
+ aResponse.setHeader("Content-Type", "image/png", false);
+ aResponse.write(IMAGE);
+ } else {
+ aResponse.setHeader("Content-Type", "text/html", false);
+ aResponse.write(IFRAME);
+ }
+}
diff --git a/toolkit/components/antitracking/test/browser/sandboxed.html b/toolkit/components/antitracking/test/browser/sandboxed.html
new file mode 100644
index 0000000000..a359ae0aa3
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/sandboxed.html
@@ -0,0 +1,12 @@
+<!DOCTYPE HTML>
+<!-- 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/. -->
+<html dir="ltr" xml:lang="en-US" lang="en-US">
+ <head>
+ <meta charset="utf8">
+ </head>
+ <body>
+ <p>Hello, World!</p>
+ </body>
+</html>
diff --git a/toolkit/components/antitracking/test/browser/sandboxed.html^headers^ b/toolkit/components/antitracking/test/browser/sandboxed.html^headers^
new file mode 100644
index 0000000000..4705ce9ded
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/sandboxed.html^headers^
@@ -0,0 +1 @@
+Content-Security-Policy: sandbox allow-scripts;
diff --git a/toolkit/components/antitracking/test/browser/server.sjs b/toolkit/components/antitracking/test/browser/server.sjs
new file mode 100644
index 0000000000..4d1b2ef01a
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/server.sjs
@@ -0,0 +1,20 @@
+function handleRequest(aRequest, aResponse) {
+ if (aRequest.queryString.includes("redirect")) {
+ aResponse.setStatusLine(aRequest.httpVersion, 302);
+ if (aRequest.queryString.includes("redirect-checkonly")) {
+ aResponse.setHeader("Location", "server.sjs?checkonly");
+ } else {
+ aResponse.setHeader("Location", "server.sjs");
+ }
+ return;
+ }
+ aResponse.setStatusLine(aRequest.httpVersion, 200);
+ if (aRequest.hasHeader("Cookie")) {
+ aResponse.write("cookie-present");
+ } else {
+ if (!aRequest.queryString.includes("checkonly")) {
+ aResponse.setHeader("Set-Cookie", "foopy=1");
+ }
+ aResponse.write("cookie-not-present");
+ }
+}
diff --git a/toolkit/components/antitracking/test/browser/serviceWorker.js b/toolkit/components/antitracking/test/browser/serviceWorker.js
new file mode 100644
index 0000000000..71c88c721a
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/serviceWorker.js
@@ -0,0 +1,103 @@
+let value = "";
+let fetch_url = "";
+
+self.onfetch = function (e) {
+ fetch_url = e.request.url;
+};
+
+// Call clients.claim() to enable fetch event.
+self.addEventListener("activate", e => {
+ e.waitUntil(self.clients.claim());
+});
+
+self.addEventListener("message", async e => {
+ let res = {};
+
+ switch (e.data.type) {
+ case "GetHWConcurrency":
+ res.result = "OK";
+ res.value = navigator.hardwareConcurrency;
+ break;
+
+ case "GetScriptValue":
+ res.result = "OK";
+ res.value = value;
+ break;
+
+ case "SetScriptValue":
+ res.result = "OK";
+ value = e.data.value;
+ break;
+
+ case "HasCache":
+ // Return if the cache storage exists or not.
+ try {
+ res.value = await caches.has(e.data.value);
+ res.result = "OK";
+ } catch (e) {
+ res.result = "ERROR";
+ }
+ break;
+
+ case "SetCache":
+ // Open a cache storage with the given name.
+ try {
+ let cache = await caches.open(e.data.value);
+ await cache.add("empty.js");
+ res.result = "OK";
+ } catch (e) {
+ res.result = "ERROR";
+ }
+ break;
+
+ case "SetIndexedDB":
+ await new Promise(resolve => {
+ let idxDB = indexedDB.open("test", 1);
+
+ idxDB.onupgradeneeded = evt => {
+ let db = evt.target.result;
+ db.createObjectStore("foobar", { keyPath: "id" });
+ };
+
+ idxDB.onsuccess = evt => {
+ let db = evt.target.result;
+ db
+ .transaction("foobar", "readwrite")
+ .objectStore("foobar")
+ .put({ id: 1, value: e.data.value }).onsuccess = _ => {
+ resolve();
+ };
+ };
+ });
+ res.result = "OK";
+ break;
+
+ case "GetIndexedDB":
+ res.value = await new Promise(resolve => {
+ let idxDB = indexedDB.open("test", 1);
+
+ idxDB.onsuccess = evt => {
+ let db = evt.target.result;
+ db.transaction("foobar").objectStore("foobar").get(1).onsuccess =
+ ee => {
+ resolve(
+ ee.target.result === undefined ? "" : ee.target.result.value
+ );
+ };
+ };
+ });
+ res.result = "OK";
+ break;
+
+ case "GetFetchURL":
+ res.value = fetch_url;
+ fetch_url = "";
+ res.result = "OK";
+ break;
+
+ default:
+ res.result = "ERROR";
+ }
+
+ e.source.postMessage(res);
+});
diff --git a/toolkit/components/antitracking/test/browser/sharedWorker.js b/toolkit/components/antitracking/test/browser/sharedWorker.js
new file mode 100644
index 0000000000..01188ed10a
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/sharedWorker.js
@@ -0,0 +1,18 @@
+let ports = 0;
+self.onconnect = e => {
+ ++ports;
+ e.ports[0].onmessage = event => {
+ if (event.data === "count") {
+ e.ports[0].postMessage(ports);
+ return;
+ }
+
+ if (event.data === "close") {
+ self.close();
+ return;
+ }
+
+ // Error.
+ e.ports[0].postMessage(-1);
+ };
+};
diff --git a/toolkit/components/antitracking/test/browser/storageAccessAPIHelpers.js b/toolkit/components/antitracking/test/browser/storageAccessAPIHelpers.js
new file mode 100644
index 0000000000..6a983ec250
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/storageAccessAPIHelpers.js
@@ -0,0 +1,236 @@
+/* global allowListed */
+
+async function hasStorageAccessInitially() {
+ let hasAccess = await document.hasStorageAccess();
+ ok(hasAccess, "Has storage access");
+}
+
+async function noStorageAccessInitially() {
+ let hasAccess = await document.hasStorageAccess();
+ ok(!hasAccess, "Doesn't yet have storage access");
+}
+
+async function stillNoStorageAccess() {
+ let hasAccess = await document.hasStorageAccess();
+ ok(!hasAccess, "Still doesn't have storage access");
+}
+
+async function callRequestStorageAccess(callback, expectFail) {
+ SpecialPowers.wrap(document).notifyUserGestureActivation();
+
+ let origin = new URL(location.href).origin;
+
+ let effectiveCookieBehavior = SpecialPowers.isContentWindowPrivate(window)
+ ? SpecialPowers.Services.prefs.getIntPref(
+ "network.cookie.cookieBehavior.pbmode"
+ )
+ : SpecialPowers.Services.prefs.getIntPref("network.cookie.cookieBehavior");
+
+ let success = true;
+ // We only grant storage exceptions when the reject tracker behavior is enabled.
+ let rejectTrackers =
+ [
+ SpecialPowers.Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER,
+ SpecialPowers.Ci.nsICookieService
+ .BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN,
+ ].includes(effectiveCookieBehavior) && !isOnContentBlockingAllowList();
+ const TEST_ANOTHER_3RD_PARTY_ORIGIN = SpecialPowers.useRemoteSubframes
+ ? "http://another-tracking.example.net"
+ : "https://another-tracking.example.net";
+ // With another-tracking.example.net, we're same-eTLD+1, so the first try succeeds.
+ if (origin != TEST_ANOTHER_3RD_PARTY_ORIGIN) {
+ if (rejectTrackers) {
+ let p;
+ let threw = false;
+ try {
+ p = document.requestStorageAccess();
+ } catch (e) {
+ threw = true;
+ }
+ ok(!threw, "requestStorageAccess should not throw");
+ try {
+ if (callback) {
+ if (expectFail) {
+ await p.catch(_ => callback());
+ success = false;
+ } else {
+ await p.then(_ => callback());
+ }
+ } else {
+ await p;
+ }
+ } catch (e) {
+ success = false;
+ } finally {
+ SpecialPowers.wrap(document).clearUserGestureActivation();
+ }
+ ok(!success, "Should not have worked without user interaction");
+
+ await noStorageAccessInitially();
+
+ await interactWithTracker();
+
+ SpecialPowers.wrap(document).notifyUserGestureActivation();
+ }
+ if (
+ effectiveCookieBehavior ==
+ SpecialPowers.Ci.nsICookieService.BEHAVIOR_ACCEPT &&
+ !isOnContentBlockingAllowList()
+ ) {
+ try {
+ if (callback) {
+ if (expectFail) {
+ await document.requestStorageAccess().catch(_ => callback());
+ success = false;
+ } else {
+ await document.requestStorageAccess().then(_ => callback());
+ }
+ } else {
+ await document.requestStorageAccess();
+ }
+ } catch (e) {
+ success = false;
+ } finally {
+ SpecialPowers.wrap(document).clearUserGestureActivation();
+ }
+ ok(success, "Should not have thrown");
+
+ await hasStorageAccessInitially();
+
+ await interactWithTracker();
+
+ SpecialPowers.wrap(document).notifyUserGestureActivation();
+ }
+ }
+
+ let p;
+ let threw = false;
+ try {
+ p = document.requestStorageAccess();
+ } catch (e) {
+ threw = true;
+ }
+ let rejected = false;
+ try {
+ if (callback) {
+ if (expectFail) {
+ await p.catch(_ => callback());
+ rejected = true;
+ } else {
+ await p.then(_ => callback());
+ }
+ } else {
+ await p;
+ }
+ } catch (e) {
+ rejected = true;
+ } finally {
+ SpecialPowers.wrap(document).clearUserGestureActivation();
+ }
+
+ success = !threw && !rejected;
+ let hasAccess = await document.hasStorageAccess();
+ is(
+ hasAccess,
+ success,
+ "Should " + (success ? "" : "not ") + "have storage access now"
+ );
+ if (
+ success &&
+ rejectTrackers &&
+ window.location.search != "?disableWaitUntilPermission" &&
+ origin != TEST_ANOTHER_3RD_PARTY_ORIGIN
+ ) {
+ let protocol = isSecureContext ? "https" : "http";
+ // Wait until the permission is visible in parent process to avoid race
+ // conditions. We don't need to wait the permission to be visible in content
+ // processes since the content process doesn't rely on the permission to
+ // know the storage access is updated.
+ await waitUntilPermission(
+ `${protocol}://example.net/browser/toolkit/components/antitracking/test/browser/page.html`,
+ "3rdPartyStorage^" + window.origin
+ );
+ }
+
+ return [threw, rejected];
+}
+
+async function waitUntilPermission(
+ url,
+ name,
+ value = SpecialPowers.Services.perms.ALLOW_ACTION
+) {
+ let originAttributes = SpecialPowers.isContentWindowPrivate(window)
+ ? { privateBrowsingId: 1 }
+ : {};
+ await new Promise(resolve => {
+ let id = setInterval(async _ => {
+ if (
+ await SpecialPowers.testPermission(name, value, {
+ url,
+ originAttributes,
+ })
+ ) {
+ clearInterval(id);
+ resolve();
+ }
+ }, 0);
+ });
+}
+
+async function interactWithTracker() {
+ await new Promise(resolve => {
+ let orionmessage = onmessage;
+ onmessage = _ => {
+ onmessage = orionmessage;
+ resolve();
+ };
+
+ info("Let's interact with the tracker");
+ window.open(
+ "/browser/toolkit/components/antitracking/test/browser/3rdPartyOpenUI.html?messageme"
+ );
+ });
+
+ // Wait until the user interaction permission becomes visible in our process
+ await waitUntilPermission(window.origin, "storageAccessAPI");
+}
+
+function isOnContentBlockingAllowList() {
+ // We directly check the window.allowListed here instead of checking the
+ // permission. The allow list permission might not be available since it is
+ // not in the preload list.
+
+ return window.allowListed;
+}
+
+async function registerServiceWorker(win, url) {
+ let reg = await win.navigator.serviceWorker.register(url);
+ if (reg.installing.state !== "activated") {
+ await new Promise(resolve => {
+ let w = reg.installing;
+ w.addEventListener("statechange", function onStateChange() {
+ if (w.state === "activated") {
+ w.removeEventListener("statechange", onStateChange);
+ resolve();
+ }
+ });
+ });
+ }
+
+ return reg.active;
+}
+
+function sendAndWaitWorkerMessage(target, worker, message) {
+ return new Promise(resolve => {
+ worker.addEventListener(
+ "message",
+ msg => {
+ resolve(msg.data);
+ },
+ { once: true }
+ );
+
+ target.postMessage(message);
+ });
+}
diff --git a/toolkit/components/antitracking/test/browser/storage_access_head.js b/toolkit/components/antitracking/test/browser/storage_access_head.js
new file mode 100644
index 0000000000..274e128783
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/storage_access_head.js
@@ -0,0 +1,248 @@
+/* import-globals-from ../../../../../browser/modules/test/browser/head.js */
+/* import-globals-from antitracking_head.js */
+
+async function openPageAndRunCode(
+ topPage,
+ topPageCallback,
+ embeddedPage,
+ embeddedPageCallback
+) {
+ let tab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: topPage,
+ waitForLoad: true,
+ });
+ let browser = gBrowser.getBrowserForTab(tab);
+
+ await topPageCallback();
+ await SpecialPowers.spawn(
+ browser,
+ [{ page: embeddedPage, callback: embeddedPageCallback.toString() }],
+ async function (obj) {
+ await new content.Promise(resolve => {
+ let ifr = content.document.createElement("iframe");
+ ifr.onload = function () {
+ ifr.contentWindow.postMessage(obj.callback, "*");
+ };
+
+ content.addEventListener("message", function msg(event) {
+ if (event.data.type == "finish") {
+ content.removeEventListener("message", msg);
+ resolve();
+ return;
+ }
+
+ if (event.data.type == "ok") {
+ ok(event.data.what, event.data.msg);
+ return;
+ }
+
+ if (event.data.type == "info") {
+ info(event.data.msg);
+ return;
+ }
+
+ ok(false, "Unknown message");
+ });
+
+ content.document.body.appendChild(ifr);
+ ifr.src = obj.page;
+ });
+ }
+ );
+
+ await BrowserTestUtils.removeTab(tab);
+}
+
+// This function returns a function that spawns an asynchronous task to handle
+// the popup and click on the appropriate values. If that task is never executed
+// the catch case is reached and we fail the test. If for some reason that catch
+// case isn't reached, having an extra event listener at the end of the test
+// will cause the test to fail anyway.
+// Note: this means that tests that use this callback should probably be in
+// their own test file.
+function getExpectPopupAndClick(accept) {
+ return function () {
+ let shownPromise = BrowserTestUtils.waitForEvent(
+ PopupNotifications.panel,
+ "popupshown"
+ );
+ shownPromise
+ .then(async _ => {
+ // This occurs when the promise resolves on the test finishing
+ let popupNotifications = PopupNotifications.panel.childNodes;
+ if (!popupNotifications.length) {
+ ok(false, "Prompt did not show up");
+ } else if (accept == "accept") {
+ ok(true, "Prompt shows up, clicking accept.");
+ await clickMainAction();
+ } else if (accept == "reject") {
+ ok(true, "Prompt shows up, clicking reject.");
+ await clickSecondaryAction();
+ } else {
+ ok(false, "Unknown accept value for test: " + accept);
+ info("Clicking accept so that the test can finish.");
+ await clickMainAction();
+ }
+ })
+ .catch(() => {
+ ok(false, "Prompt did not show up");
+ });
+ };
+}
+
+// This function spawns an asynchronous task that fails the test if a popup
+// appears. If that never happens, the catch case is executed on the test
+// cleanup.
+// Note: this means that tests that use this callback should probably be in
+// their own test file.
+function expectNoPopup() {
+ let shownPromise = BrowserTestUtils.waitForEvent(
+ PopupNotifications.panel,
+ "popupshown"
+ );
+ shownPromise
+ .then(async _ => {
+ // This occurs when the promise resolves on the test finishing
+ let popupNotifications = PopupNotifications.panel.childNodes;
+ if (!popupNotifications.length) {
+ ok(true, "Prompt did not show up");
+ } else {
+ ok(false, "Prompt shows up");
+ info(PopupNotifications.panel);
+ await clickSecondaryAction();
+ }
+ })
+ .catch(() => {
+ ok(true, "Prompt did not show up");
+ });
+}
+
+async function requestStorageAccessAndExpectSuccess() {
+ const aps = SpecialPowers.Services.prefs.getBoolPref(
+ "privacy.partition.always_partition_third_party_non_cookie_storage"
+ );
+
+ // When always partitioning storage, we do not clear non-cookie storage
+ // after a requestStorageAccess is accepted by the user. So here we test
+ // that indexedDB is cleared when the pref is off, but not when it is on.
+ await new Promise((resolve, reject) => {
+ const db = window.indexedDB.open("rSATest", 1);
+ db.onupgradeneeded = resolve;
+ db.success = resolve;
+ db.onerror = reject;
+ });
+
+ const hadAccessAlready = await document.hasStorageAccess();
+ const shouldClearIDB = !aps && !hadAccessAlready;
+
+ SpecialPowers.wrap(document).notifyUserGestureActivation();
+ let p = document.requestStorageAccess();
+ try {
+ await p;
+ ok(true, "gain storage access.");
+ } catch {
+ ok(false, "denied storage access.");
+ }
+
+ await new Promise((resolve, reject) => {
+ const req = window.indexedDB.open("rSATest", 1);
+ req.onerror = reject;
+ req.onupgradeneeded = () => {
+ ok(shouldClearIDB, "iDB was cleared");
+ req.onsuccess = undefined;
+ resolve();
+ };
+ req.onsuccess = () => {
+ ok(!shouldClearIDB, "iDB was not cleared");
+ resolve();
+ };
+ });
+
+ await new Promise(resolve => {
+ const req = window.indexedDB.deleteDatabase("rSATest");
+ req.onsuccess = resolve;
+ req.onerror = resolve;
+ });
+
+ SpecialPowers.wrap(document).clearUserGestureActivation();
+}
+
+async function requestStorageAccessAndExpectFailure() {
+ // When always partitioning storage, we do not clear non-cookie storage
+ // after a requestStorageAccess is accepted by the user. So here we test
+ // that indexedDB is cleared when the pref is off, but not when it is on.
+ await new Promise((resolve, reject) => {
+ const db = window.indexedDB.open("rSATest", 1);
+ db.onupgradeneeded = resolve;
+ db.success = resolve;
+ db.onerror = reject;
+ });
+
+ SpecialPowers.wrap(document).notifyUserGestureActivation();
+ let p = document.requestStorageAccess();
+ try {
+ await p;
+ ok(false, "gain storage access.");
+ } catch {
+ ok(true, "denied storage access.");
+ }
+
+ await new Promise((resolve, reject) => {
+ const req = window.indexedDB.open("rSATest", 1);
+ req.onerror = reject;
+ req.onupgradeneeded = () => {
+ ok(false, "iDB was cleared");
+ req.onsuccess = undefined;
+ resolve();
+ };
+ req.onsuccess = () => {
+ ok(true, "iDB was not cleared");
+ resolve();
+ };
+ });
+
+ await new Promise(resolve => {
+ const req = window.indexedDB.deleteDatabase("rSATest");
+ req.onsuccess = resolve;
+ req.onerror = resolve;
+ });
+
+ SpecialPowers.wrap(document).clearUserGestureActivation();
+}
+
+async function cleanUpData() {
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value =>
+ resolve()
+ );
+ });
+ ok(true, "Deleted all data.");
+}
+
+async function setPreferences(alwaysPartitionStorage = false) {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.storage_access.auto_grants", true],
+ ["dom.storage_access.auto_grants.delayed", false],
+ ["dom.storage_access.enabled", true],
+ ["dom.storage_access.max_concurrent_auto_grants", 0],
+ ["dom.storage_access.prompt.testing", false],
+ [
+ "network.cookie.cookieBehavior",
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN,
+ ],
+ [
+ "network.cookie.cookieBehavior.pbmode",
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN,
+ ],
+ [
+ "privacy.partition.always_partition_third_party_non_cookie_storage",
+ alwaysPartitionStorage,
+ ],
+ ["privacy.trackingprotection.enabled", false],
+ ["privacy.trackingprotection.pbmode.enabled", false],
+ ["privacy.trackingprotection.annotate_channels", true],
+ ],
+ });
+}
diff --git a/toolkit/components/antitracking/test/browser/subResources.sjs b/toolkit/components/antitracking/test/browser/subResources.sjs
new file mode 100644
index 0000000000..f40d5cac97
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/subResources.sjs
@@ -0,0 +1,33 @@
+// A 1x1 PNG image.
+// Source: https://commons.wikimedia.org/wiki/File:1x1.png (Public Domain)
+const IMAGE = atob(
+ "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEUAA" +
+ "ACnej3aAAAAAXRSTlMAQObYZgAAAApJREFUCNdjYAAAAAIAAeIhvDMAAAAASUVORK5CYII="
+);
+
+function handleRequest(aRequest, aResponse) {
+ aResponse.setStatusLine(aRequest.httpVersion, 200);
+
+ let key = aRequest.queryString.includes("what=script") ? "script" : "image";
+
+ if (aRequest.queryString.includes("result")) {
+ aResponse.write(getState(key) || 0);
+ setState(key, "0");
+ return;
+ }
+
+ if (aRequest.hasHeader("Cookie")) {
+ let hints = parseInt(getState(key) || 0) + 1;
+ setState(key, hints.toString());
+ }
+
+ aResponse.setHeader("Set-Cookie", "foopy=1");
+
+ if (key == "script") {
+ aResponse.setHeader("Content-Type", "text/javascript", false);
+ aResponse.write("42;");
+ } else {
+ aResponse.setHeader("Content-Type", "image/png", false);
+ aResponse.write(IMAGE);
+ }
+}
diff --git a/toolkit/components/antitracking/test/browser/tracker.js b/toolkit/components/antitracking/test/browser/tracker.js
new file mode 100644
index 0000000000..85e943f7c4
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/tracker.js
@@ -0,0 +1,7 @@
+window.addEventListener("message", e => {
+ let bc = new BroadcastChannel("a");
+ bc.postMessage("ready!");
+});
+window.open(
+ "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/3rdPartyOpen.html"
+);
diff --git a/toolkit/components/antitracking/test/browser/workerIframe.html b/toolkit/components/antitracking/test/browser/workerIframe.html
new file mode 100644
index 0000000000..37aa5d7c0d
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/workerIframe.html
@@ -0,0 +1,71 @@
+<html>
+<head>
+ <title>3rd party content!</title>
+ <script type="text/javascript" src="https://example.com/browser/toolkit/components/antitracking/test/browser/storageAccessAPIHelpers.js"></script>
+</head>
+<body>
+<h1>Here the 3rd party content!</h1>
+<script>
+
+function info(msg) {
+ parent.postMessage({ type: "info", msg }, "*");
+}
+
+function ok(what, msg) {
+ parent.postMessage({ type: "ok", what: !!what, msg }, "*");
+}
+
+function is(a, b, msg) {
+ ok(a === b, msg);
+}
+
+async function runTest() {
+ function workerCode() {
+ onmessage = e => {
+ try {
+ indexedDB.open("test", "1");
+ postMessage(true);
+ } catch (e) {
+ postMessage(false);
+ }
+ };
+ }
+
+ /* import-globals-from storageAccessAPIHelpers.js */
+ await noStorageAccessInitially();
+ info("Initialized");
+
+ let blob = new Blob([workerCode.toString() + "; workerCode();"]);
+ let blobURL = URL.createObjectURL(blob);
+ info("Blob created");
+
+ let w = new Worker(blobURL);
+ info("Worker created");
+
+ await new Promise(resolve => {
+ w.addEventListener("message", e => {
+ ok(!e.data, "IDB is disabled");
+ resolve();
+ }, { once: true });
+ w.postMessage("go");
+ });
+
+ /* import-globals-from storageAccessAPIHelpers.js */
+ await callRequestStorageAccess();
+
+ await new Promise(resolve => {
+ w.addEventListener("message", e => {
+ ok(e.data, "IDB is enabled");
+ resolve();
+ }, { once: true });
+ w.postMessage("go");
+ });
+
+ parent.postMessage({ type: "finish" }, "*");
+}
+
+runTest();
+
+</script>
+</body>
+</html>
diff --git a/toolkit/components/antitracking/test/gtest/TestPartitioningExceptionList.cpp b/toolkit/components/antitracking/test/gtest/TestPartitioningExceptionList.cpp
new file mode 100644
index 0000000000..1aa0614622
--- /dev/null
+++ b/toolkit/components/antitracking/test/gtest/TestPartitioningExceptionList.cpp
@@ -0,0 +1,184 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/Preferences.h"
+#include "mozilla/PartitioningExceptionList.h"
+
+using namespace mozilla;
+
+static const char kPrefPartitioningExceptionList[] =
+ "privacy.restrict3rdpartystorage.skip_list";
+
+static const char kPrefEnableWebcompat[] =
+ "privacy.antitracking.enableWebcompat";
+
+TEST(TestPartitioningExceptionList, TestPrefBasic)
+{
+ nsAutoCString oldPartitioningExceptionList;
+ Preferences::GetCString(kPrefEnableWebcompat, oldPartitioningExceptionList);
+ bool oldEnableWebcompat = Preferences::GetBool(kPrefEnableWebcompat);
+
+ for (uint32_t populateList = 0; populateList <= 1; populateList++) {
+ for (uint32_t enableWebcompat = 0; enableWebcompat <= 1;
+ enableWebcompat++) {
+ if (populateList) {
+ Preferences::SetCString(kPrefPartitioningExceptionList,
+ "https://example.com,https://example.net");
+ } else {
+ Preferences::SetCString(kPrefPartitioningExceptionList, "");
+ }
+
+ Preferences::SetBool(kPrefEnableWebcompat, enableWebcompat);
+
+ EXPECT_FALSE(
+ PartitioningExceptionList::Check(""_ns, "https://example.net"_ns));
+ EXPECT_FALSE(
+ PartitioningExceptionList::Check("https://example.com"_ns, ""_ns));
+ EXPECT_FALSE(PartitioningExceptionList::Check(""_ns, ""_ns));
+ EXPECT_FALSE(PartitioningExceptionList::Check("https://example.net"_ns,
+ "https://example.com"_ns));
+ EXPECT_FALSE(PartitioningExceptionList::Check("https://example.com"_ns,
+ "https://example.org"_ns));
+ EXPECT_FALSE(PartitioningExceptionList::Check("https://example.com"_ns,
+ "https://example.com"_ns));
+ EXPECT_FALSE(PartitioningExceptionList::Check("http://example.com"_ns,
+ "http://example.net"_ns));
+ EXPECT_FALSE(PartitioningExceptionList::Check("https://example.com"_ns,
+ "http://example.net"_ns));
+ EXPECT_FALSE(PartitioningExceptionList::Check("https://example.com."_ns,
+ "https://example.net"_ns));
+
+ bool result = PartitioningExceptionList::Check("https://example.com"_ns,
+ "https://example.net"_ns);
+ EXPECT_TRUE(result == (populateList && enableWebcompat));
+ }
+ }
+
+ Preferences::SetCString(kPrefPartitioningExceptionList,
+ oldPartitioningExceptionList);
+ Preferences::SetBool(kPrefEnableWebcompat, oldEnableWebcompat);
+}
+
+TEST(TestPartitioningExceptionList, TestPrefWildcard)
+{
+ nsAutoCString oldPartitioningExceptionList;
+ Preferences::GetCString(kPrefEnableWebcompat, oldPartitioningExceptionList);
+ bool oldEnableWebcompat = Preferences::GetBool(kPrefEnableWebcompat);
+
+ Preferences::SetCString(kPrefPartitioningExceptionList,
+ "https://example.com,https://example.net;"
+ "https://*.foo.com,https://bar.com;"
+ "https://*.foo.com,https://foobar.net;"
+ "https://test.net,https://*.example.com;"
+ "https://test.com,https://*.example.com;"
+ "https://*.test2.org,*;"
+ "*,http://notatracker.org");
+
+ Preferences::SetBool(kPrefEnableWebcompat, true);
+
+ EXPECT_TRUE(PartitioningExceptionList::Check("https://example.com"_ns,
+ "https://example.net"_ns));
+
+ EXPECT_TRUE(PartitioningExceptionList::Check("https://two.foo.com"_ns,
+ "https://bar.com"_ns));
+ EXPECT_TRUE(PartitioningExceptionList::Check("https://another.foo.com"_ns,
+ "https://bar.com"_ns));
+ EXPECT_TRUE(PartitioningExceptionList::Check("https://three.two.foo.com"_ns,
+ "https://bar.com"_ns));
+ EXPECT_FALSE(PartitioningExceptionList::Check("https://two.foo.com"_ns,
+ "https://example.com"_ns));
+ EXPECT_FALSE(PartitioningExceptionList::Check("https://foo.com"_ns,
+ "https://bar.com"_ns));
+ EXPECT_FALSE(PartitioningExceptionList::Check("https://two.foo.com"_ns,
+ "http://bar.com"_ns));
+ EXPECT_FALSE(PartitioningExceptionList::Check("http://two.foo.com"_ns,
+ "https://bar.com"_ns));
+
+ EXPECT_TRUE(PartitioningExceptionList::Check("https://a.foo.com"_ns,
+ "https://foobar.net"_ns));
+
+ EXPECT_TRUE(PartitioningExceptionList::Check("https://test.net"_ns,
+ "https://test.example.com"_ns));
+ EXPECT_TRUE(PartitioningExceptionList::Check(
+ "https://test.net"_ns, "https://foo.bar.example.com"_ns));
+ EXPECT_FALSE(PartitioningExceptionList::Check("https://test.com"_ns,
+ "https://foo.test.net"_ns));
+
+ EXPECT_TRUE(PartitioningExceptionList::Check("https://one.test2.org"_ns,
+ "https://example.net"_ns));
+ EXPECT_TRUE(PartitioningExceptionList::Check("https://two.test2.org"_ns,
+ "https://foo.example.net"_ns));
+ EXPECT_TRUE(PartitioningExceptionList::Check("https://three.test2.org"_ns,
+ "http://example.net"_ns));
+ EXPECT_TRUE(PartitioningExceptionList::Check("https://four.sub.test2.org"_ns,
+ "https://bar.com"_ns));
+ EXPECT_FALSE(PartitioningExceptionList::Check("https://four.sub.test2.com"_ns,
+ "https://bar.com"_ns));
+ EXPECT_FALSE(PartitioningExceptionList::Check("http://four.sub.test2.org"_ns,
+ "https://bar.com"_ns));
+ EXPECT_FALSE(PartitioningExceptionList::Check(
+ "https://four.sub.test2.org."_ns, "https://bar.com"_ns));
+
+ EXPECT_TRUE(PartitioningExceptionList::Check("https://example.com"_ns,
+ "http://notatracker.org"_ns));
+
+ Preferences::SetCString(kPrefPartitioningExceptionList,
+ oldPartitioningExceptionList);
+ Preferences::SetBool(kPrefEnableWebcompat, oldEnableWebcompat);
+}
+
+TEST(TestPartitioningExceptionList, TestInvalidEntries)
+{
+ nsAutoCString oldPartitioningExceptionList;
+ Preferences::GetCString(kPrefEnableWebcompat, oldPartitioningExceptionList);
+ bool oldEnableWebcompat = Preferences::GetBool(kPrefEnableWebcompat);
+
+ Preferences::SetBool(kPrefEnableWebcompat, true);
+
+ // Empty entries.
+ Preferences::SetCString(kPrefPartitioningExceptionList, ";;;,;");
+
+ EXPECT_FALSE(PartitioningExceptionList::Check("https://example.com"_ns,
+ "https://example.net"_ns));
+
+ // Schemeless entries.
+ Preferences::SetCString(kPrefPartitioningExceptionList,
+ "example.com,example.net");
+
+ EXPECT_FALSE(PartitioningExceptionList::Check("https://example.com"_ns,
+ "https://example.net"_ns));
+
+ // Invalid entry should be skipped and not break other entries.
+ Preferences::SetCString(kPrefPartitioningExceptionList,
+ "*,*;"
+ "https://example.com,https://example.net;"
+ "http://example.org,");
+
+ EXPECT_TRUE(PartitioningExceptionList::Check("https://example.com"_ns,
+ "https://example.net"_ns));
+ EXPECT_FALSE(PartitioningExceptionList::Check("https://foo.com"_ns,
+ "https://bar.net"_ns));
+
+ // Unsupported schemes should not be accepted.
+ Preferences::SetCString(kPrefPartitioningExceptionList,
+ "ftp://example.com,ftp://example.net;");
+
+ EXPECT_FALSE(PartitioningExceptionList::Check("https://example.com"_ns,
+ "https://example.net"_ns));
+ EXPECT_FALSE(PartitioningExceptionList::Check("ftp://example.com"_ns,
+ "ftp://example.net"_ns));
+
+ // Test invalid origins with trailing '/'.
+ Preferences::SetCString(kPrefPartitioningExceptionList,
+ "https://example.com/,https://example.net/");
+ EXPECT_FALSE(PartitioningExceptionList::Check("https://example.com"_ns,
+ "https://example.net"_ns));
+
+ Preferences::SetCString(kPrefPartitioningExceptionList,
+ oldPartitioningExceptionList);
+ Preferences::SetBool(kPrefEnableWebcompat, oldEnableWebcompat);
+}
diff --git a/toolkit/components/antitracking/test/gtest/TestStoragePrincipalHelper.cpp b/toolkit/components/antitracking/test/gtest/TestStoragePrincipalHelper.cpp
new file mode 100644
index 0000000000..44959e4dc8
--- /dev/null
+++ b/toolkit/components/antitracking/test/gtest/TestStoragePrincipalHelper.cpp
@@ -0,0 +1,215 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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 "nsCOMPtr.h"
+#include "nsContentUtils.h"
+#include "nsIChannel.h"
+#include "nsIContentPolicy.h"
+#include "nsICookieJarSettings.h"
+#include "nsILoadInfo.h"
+#include "nsIURI.h"
+#include "nsNetUtil.h"
+#include "nsStringFwd.h"
+
+#include "mozilla/gtest/MozAssertions.h"
+#include "mozilla/NullPrincipal.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/StoragePrincipalHelper.h"
+
+using mozilla::Preferences;
+using namespace mozilla;
+
+/**
+ * Creates a test channel with CookieJarSettings which have a partitionKey set.
+ */
+nsresult CreateMockChannel(nsIPrincipal* aPrincipal, bool isThirdParty,
+ const nsACString& aPartitionKey,
+ nsIChannel** aChannel,
+ nsICookieJarSettings** aCookieJarSettings) {
+ nsCOMPtr<nsIURI> mockUri;
+ nsresult rv = NS_NewURI(getter_AddRefs(mockUri), "http://example.com"_ns);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIChannel> mockChannel;
+ nsCOMPtr<nsIIOService> service = do_GetIOService(&rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = service->NewChannelFromURI(mockUri, nullptr, aPrincipal, aPrincipal, 0,
+ nsContentPolicyType::TYPE_OTHER,
+ getter_AddRefs(mockChannel));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsILoadInfo> mockLoadInfo = mockChannel->LoadInfo();
+ rv = mockLoadInfo->SetIsThirdPartyContextToTopWindow(isThirdParty);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsICookieJarSettings> cjs;
+ rv = mockLoadInfo->GetCookieJarSettings(getter_AddRefs(cjs));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIURI> partitionKeyUri;
+ rv = NS_NewURI(getter_AddRefs(partitionKeyUri), aPartitionKey);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = cjs->InitWithURI(partitionKeyUri, false);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ cjs.forget(aCookieJarSettings);
+ mockChannel.forget(aChannel);
+ return NS_OK;
+}
+
+TEST(TestStoragePrincipalHelper, TestCreateContentPrincipal)
+{
+ nsCOMPtr<nsIPrincipal> contentPrincipal =
+ BasePrincipal::CreateContentPrincipal("https://example.com"_ns);
+ EXPECT_TRUE(contentPrincipal);
+
+ nsCOMPtr<nsIChannel> mockChannel;
+ nsCOMPtr<nsICookieJarSettings> cookieJarSettings;
+ nsresult rv = CreateMockChannel(
+ contentPrincipal, false, "https://example.org"_ns,
+ getter_AddRefs(mockChannel), getter_AddRefs(cookieJarSettings));
+ ASSERT_EQ(rv, NS_OK) << "Could not create a mock channel";
+
+ nsCOMPtr<nsIPrincipal> storagePrincipal;
+ rv = StoragePrincipalHelper::Create(mockChannel, contentPrincipal, true,
+ getter_AddRefs(storagePrincipal));
+ ASSERT_EQ(rv, NS_OK) << "Should not fail for ContentPrincipal";
+ EXPECT_TRUE(storagePrincipal);
+
+ nsCOMPtr<nsIPrincipal> storagePrincipalSW;
+ rv = StoragePrincipalHelper::CreatePartitionedPrincipalForServiceWorker(
+ contentPrincipal, cookieJarSettings, getter_AddRefs(storagePrincipalSW));
+ ASSERT_EQ(rv, NS_OK) << "Should not fail for ContentPrincipal";
+ EXPECT_TRUE(storagePrincipalSW);
+}
+
+TEST(TestStoragePrincipalHelper, TestCreateNullPrincipal)
+{
+ RefPtr<NullPrincipal> nullPrincipal =
+ NullPrincipal::CreateWithoutOriginAttributes();
+ EXPECT_TRUE(nullPrincipal);
+
+ nsCOMPtr<nsIChannel> mockChannel;
+ nsCOMPtr<nsICookieJarSettings> cookieJarSettings;
+ nsresult rv = CreateMockChannel(
+ nullPrincipal, false, "https://example.org"_ns,
+ getter_AddRefs(mockChannel), getter_AddRefs(cookieJarSettings));
+ ASSERT_EQ(rv, NS_OK) << "Could not create a mock channel";
+
+ nsCOMPtr<nsIPrincipal> storagePrincipal;
+ rv = StoragePrincipalHelper::Create(mockChannel, nullPrincipal, true,
+ getter_AddRefs(storagePrincipal));
+ EXPECT_NS_FAILED(rv) << "Should fail for NullPrincipal";
+ EXPECT_FALSE(storagePrincipal);
+
+ nsCOMPtr<nsIPrincipal> storagePrincipalSW;
+ rv = StoragePrincipalHelper::CreatePartitionedPrincipalForServiceWorker(
+ nullPrincipal, cookieJarSettings, getter_AddRefs(storagePrincipalSW));
+ EXPECT_NS_FAILED(rv) << "Should fail for NullPrincipal";
+ EXPECT_FALSE(storagePrincipal);
+}
+
+TEST(TestStoragePrincipalHelper, TestGetPrincipalCookieBehavior4)
+{
+ Preferences::SetInt("network.cookie.cookieBehavior", 4);
+
+ nsCOMPtr<nsIPrincipal> contentPrincipal =
+ BasePrincipal::CreateContentPrincipal("https://example.com"_ns);
+ EXPECT_TRUE(contentPrincipal);
+
+ for (auto isThirdParty : {false, true}) {
+ nsCOMPtr<nsIChannel> mockChannel;
+ nsCOMPtr<nsICookieJarSettings> cookieJarSettings;
+ nsresult rv = CreateMockChannel(
+ contentPrincipal, isThirdParty, "https://example.org"_ns,
+ getter_AddRefs(mockChannel), getter_AddRefs(cookieJarSettings));
+ ASSERT_EQ(rv, NS_OK) << "Could not create a mock channel";
+
+ nsCOMPtr<nsIPrincipal> testPrincipal;
+ rv = StoragePrincipalHelper::GetPrincipal(
+ mockChannel, StoragePrincipalHelper::eRegularPrincipal,
+ getter_AddRefs(testPrincipal));
+ ASSERT_EQ(rv, NS_OK) << "Could not get regular principal";
+ EXPECT_TRUE(testPrincipal);
+ EXPECT_TRUE(testPrincipal->OriginAttributesRef().mPartitionKey.IsEmpty());
+
+ rv = StoragePrincipalHelper::GetPrincipal(
+ mockChannel, StoragePrincipalHelper::ePartitionedPrincipal,
+ getter_AddRefs(testPrincipal));
+ ASSERT_EQ(rv, NS_OK) << "Could not get partitioned principal";
+ EXPECT_TRUE(testPrincipal);
+ EXPECT_TRUE(
+ testPrincipal->OriginAttributesRef().mPartitionKey.EqualsLiteral(
+ "(https,example.org)"));
+
+ // We should always get regular principal if the dFPI is disabled.
+ rv = StoragePrincipalHelper::GetPrincipal(
+ mockChannel, StoragePrincipalHelper::eForeignPartitionedPrincipal,
+ getter_AddRefs(testPrincipal));
+ ASSERT_EQ(rv, NS_OK) << "Could not get foreign partitioned principal";
+ EXPECT_TRUE(testPrincipal);
+ EXPECT_TRUE(testPrincipal->OriginAttributesRef().mPartitionKey.IsEmpty());
+
+ // Note that we don't test eStorageAccessPrincipal here because it's hard to
+ // setup the right state for the storage access in gTest.
+ }
+}
+
+TEST(TestStoragePrincipalHelper, TestGetPrincipalCookieBehavior5)
+{
+ Preferences::SetInt("network.cookie.cookieBehavior", 5);
+
+ nsCOMPtr<nsIPrincipal> contentPrincipal =
+ BasePrincipal::CreateContentPrincipal("https://example.com"_ns);
+ EXPECT_TRUE(contentPrincipal);
+
+ for (auto isThirdParty : {false, true}) {
+ nsCOMPtr<nsIChannel> mockChannel;
+ nsCOMPtr<nsICookieJarSettings> cookieJarSettings;
+ nsresult rv = CreateMockChannel(
+ contentPrincipal, isThirdParty, "https://example.org"_ns,
+ getter_AddRefs(mockChannel), getter_AddRefs(cookieJarSettings));
+ ASSERT_EQ(rv, NS_OK) << "Could not create a mock channel";
+
+ nsCOMPtr<nsIPrincipal> testPrincipal;
+ rv = StoragePrincipalHelper::GetPrincipal(
+ mockChannel, StoragePrincipalHelper::eRegularPrincipal,
+ getter_AddRefs(testPrincipal));
+ ASSERT_EQ(rv, NS_OK) << "Could not get regular principal";
+ EXPECT_TRUE(testPrincipal);
+ EXPECT_TRUE(testPrincipal->OriginAttributesRef().mPartitionKey.IsEmpty());
+
+ rv = StoragePrincipalHelper::GetPrincipal(
+ mockChannel, StoragePrincipalHelper::ePartitionedPrincipal,
+ getter_AddRefs(testPrincipal));
+ ASSERT_EQ(rv, NS_OK) << "Could not get partitioned principal";
+ EXPECT_TRUE(testPrincipal);
+ EXPECT_TRUE(
+ testPrincipal->OriginAttributesRef().mPartitionKey.EqualsLiteral(
+ "(https,example.org)"));
+
+ // We should always get regular principal if the dFPI is disabled.
+ rv = StoragePrincipalHelper::GetPrincipal(
+ mockChannel, StoragePrincipalHelper::eForeignPartitionedPrincipal,
+ getter_AddRefs(testPrincipal));
+ ASSERT_EQ(rv, NS_OK) << "Could not get foreign partitioned principal";
+ EXPECT_TRUE(testPrincipal);
+ if (isThirdParty) {
+ EXPECT_TRUE(
+ testPrincipal->OriginAttributesRef().mPartitionKey.EqualsLiteral(
+ "(https,example.org)"));
+ } else {
+ EXPECT_TRUE(testPrincipal->OriginAttributesRef().mPartitionKey.IsEmpty());
+ }
+
+ // Note that we don't test eStorageAccessPrincipal here because it's hard to
+ // setup the right state for the storage access in gTest.
+ }
+}
diff --git a/toolkit/components/antitracking/test/gtest/TestURLQueryStringStripper.cpp b/toolkit/components/antitracking/test/gtest/TestURLQueryStringStripper.cpp
new file mode 100644
index 0000000000..3f77479f74
--- /dev/null
+++ b/toolkit/components/antitracking/test/gtest/TestURLQueryStringStripper.cpp
@@ -0,0 +1,176 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/Components.h"
+#include "nsIURLQueryStringStripper.h"
+#include "nsIURI.h"
+#include "nsNetUtil.h"
+#include "nsStringFwd.h"
+
+#include "mozilla/Preferences.h"
+#include "mozilla/SpinEventLoopUntil.h"
+#include "mozilla/URLQueryStringStripper.h"
+
+using namespace mozilla;
+
+static const char kPrefQueryStrippingEnabled[] =
+ "privacy.query_stripping.enabled";
+static const char kPrefQueryStrippingEnabledPBM[] =
+ "privacy.query_stripping.enabled.pbmode";
+static const char kPrefQueryStrippingList[] =
+ "privacy.query_stripping.strip_list";
+
+/**
+ * Waits for the strip list in the URLQueryStringStripper to match aExpected.
+ */
+void waitForStripListChange(const nsACString& aExpected) {
+ nsresult rv;
+ nsCOMPtr<nsIURLQueryStringStripper> queryStripper =
+ components::URLQueryStringStripper::Service(&rv);
+ EXPECT_TRUE(NS_SUCCEEDED(rv));
+
+ MOZ_ALWAYS_TRUE(mozilla::SpinEventLoopUntil(
+ "TestURLQueryStringStripper waitForStripListChange"_ns, [&]() -> bool {
+ nsAutoCString stripList;
+ rv = queryStripper->TestGetStripList(stripList);
+ return NS_SUCCEEDED(rv) && stripList.Equals(aExpected);
+ }));
+}
+
+void DoTest(const nsACString& aTestURL, const bool aIsPBM,
+ const nsACString& aExpectedURL, uint32_t aExpectedResult) {
+ nsCOMPtr<nsIURI> testURI;
+
+ NS_NewURI(getter_AddRefs(testURI), aTestURL);
+
+ nsresult rv;
+ nsCOMPtr<nsIURLQueryStringStripper> queryStripper =
+ components::URLQueryStringStripper::Service(&rv);
+ EXPECT_TRUE(NS_SUCCEEDED(rv));
+
+ nsCOMPtr<nsIURI> strippedURI;
+ uint32_t numStripped;
+ rv = queryStripper->Strip(testURI, aIsPBM, getter_AddRefs(strippedURI),
+ &numStripped);
+ EXPECT_TRUE(NS_SUCCEEDED(rv));
+
+ EXPECT_TRUE(numStripped == aExpectedResult);
+
+ if (!numStripped) {
+ EXPECT_TRUE(!strippedURI);
+ } else {
+ EXPECT_TRUE(strippedURI->GetSpecOrDefault().Equals(aExpectedURL));
+ }
+}
+
+TEST(TestURLQueryStringStripper, TestPrefDisabled)
+{
+ // Disable the query string stripping by the pref and make sure the stripping
+ // is disabled.
+ // Note that we don't need to run a dummy test to create the
+ // URLQueryStringStripper here because the stripper will never be created if
+ // the query stripping is disabled.
+ Preferences::SetCString(kPrefQueryStrippingList, "fooBar foobaz");
+ Preferences::SetBool(kPrefQueryStrippingEnabled, false);
+ Preferences::SetBool(kPrefQueryStrippingEnabledPBM, false);
+
+ for (bool isPBM : {false, true}) {
+ DoTest("https://example.com/"_ns, isPBM, ""_ns, 0);
+ DoTest("https://example.com/?Barfoo=123"_ns, isPBM, ""_ns, 0);
+ DoTest("https://example.com/?fooBar=123&foobaz"_ns, isPBM, ""_ns, 0);
+ }
+}
+
+TEST(TestURLQueryStringStripper, TestEmptyStripList)
+{
+ // Make sure there is no error if the strip list is empty.
+ Preferences::SetBool(kPrefQueryStrippingEnabled, true);
+ Preferences::SetBool(kPrefQueryStrippingEnabledPBM, true);
+
+ // To create the URLQueryStringStripper, we need to run a dummy test after
+ // the query stripping is enabled. By doing this, the stripper will be
+ // initiated and we are good to test.
+ DoTest("https://example.com/"_ns, false, ""_ns, 0);
+
+ // Set the strip list to empty and wait until the pref setting is set to the
+ // stripper.
+ Preferences::SetCString(kPrefQueryStrippingList, "");
+
+ waitForStripListChange(""_ns);
+
+ for (bool isPBM : {false, true}) {
+ DoTest("https://example.com/"_ns, isPBM, ""_ns, 0);
+ DoTest("https://example.com/?Barfoo=123"_ns, isPBM, ""_ns, 0);
+ DoTest("https://example.com/?fooBar=123&foobaz"_ns, isPBM, ""_ns, 0);
+ }
+}
+
+TEST(TestURLQueryStringStripper, TestStripping)
+{
+ Preferences::SetBool(kPrefQueryStrippingEnabled, true);
+ Preferences::SetBool(kPrefQueryStrippingEnabledPBM, true);
+ DoTest("https://example.com/"_ns, false, ""_ns, 0);
+
+ Preferences::SetCString(kPrefQueryStrippingList, "fooBar foobaz");
+ waitForStripListChange("foobar foobaz"_ns);
+
+ // Test all pref combinations.
+ for (bool pref : {false, true}) {
+ for (bool prefPBM : {false, true}) {
+ Preferences::SetBool(kPrefQueryStrippingEnabled, pref);
+ Preferences::SetBool(kPrefQueryStrippingEnabledPBM, prefPBM);
+
+ // If the service is enabled with the given pref config we need for the
+ // list changes to propagate as they happen async.
+ if (pref || prefPBM) {
+ waitForStripListChange("foobar foobaz"_ns);
+ }
+
+ // Test with normal and private browsing mode.
+ for (bool isPBM : {false, true}) {
+ bool expectStrip = (prefPBM && isPBM) || (pref && !isPBM);
+
+ DoTest("https://example.com/"_ns, isPBM, ""_ns, 0);
+ DoTest("https://example.com/?Barfoo=123"_ns, isPBM, ""_ns, 0);
+
+ DoTest("https://example.com/?fooBar=123"_ns, isPBM,
+ "https://example.com/"_ns, expectStrip ? 1 : 0);
+ DoTest("https://example.com/?fooBar=123&foobaz"_ns, isPBM,
+ "https://example.com/"_ns, expectStrip ? 2 : 0);
+ DoTest("https://example.com/?fooBar=123&Barfoo=456&foobaz"_ns, isPBM,
+ "https://example.com/?Barfoo=456"_ns, expectStrip ? 2 : 0);
+
+ DoTest("https://example.com/?FOOBAR=123"_ns, isPBM,
+ "https://example.com/"_ns, expectStrip ? 1 : 0);
+ DoTest("https://example.com/?barfoo=foobar"_ns, isPBM,
+ "https://example.com/?barfoo=foobar"_ns, 0);
+ DoTest("https://example.com/?foobar=123&nostrip=456&FooBar=789"_ns,
+ isPBM, "https://example.com/?nostrip=456"_ns,
+ expectStrip ? 2 : 0);
+ DoTest("https://example.com/?AfoobazB=123"_ns, isPBM,
+ "https://example.com/?AfoobazB=123"_ns, 0);
+ }
+ }
+ }
+
+ // Change the strip list pref to see if it is updated properly.
+ // We test this in normal browsing, so set the prefs accordingly.
+ Preferences::SetBool(kPrefQueryStrippingEnabled, true);
+ Preferences::SetBool(kPrefQueryStrippingEnabledPBM, false);
+
+ Preferences::SetCString(kPrefQueryStrippingList, "Barfoo bazfoo");
+
+ waitForStripListChange("barfoo bazfoo"_ns);
+
+ DoTest("https://example.com/?fooBar=123"_ns, false, ""_ns, 0);
+ DoTest("https://example.com/?fooBar=123&foobaz"_ns, false, ""_ns, 0);
+
+ DoTest("https://example.com/?bazfoo=123"_ns, false, "https://example.com/"_ns,
+ 1);
+ DoTest("https://example.com/?fooBar=123&Barfoo=456&foobaz=abc"_ns, false,
+ "https://example.com/?fooBar=123&foobaz=abc"_ns, 1);
+}
diff --git a/toolkit/components/antitracking/test/gtest/moz.build b/toolkit/components/antitracking/test/gtest/moz.build
new file mode 100644
index 0000000000..a7e8595c5e
--- /dev/null
+++ b/toolkit/components/antitracking/test/gtest/moz.build
@@ -0,0 +1,19 @@
+# -*- Mode: python; c-basic-offset: 4; 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 += [
+ "TestPartitioningExceptionList.cpp",
+ "TestStoragePrincipalHelper.cpp",
+ "TestURLQueryStringStripper.cpp",
+]
+
+LOCAL_INCLUDES += [
+ "/xpcom/tests/gtest",
+]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+FINAL_LIBRARY = "xul-gtest"
diff --git a/toolkit/components/antitracking/test/xpcshell/data/font.woff b/toolkit/components/antitracking/test/xpcshell/data/font.woff
new file mode 100644
index 0000000000..acda4f3d9f
--- /dev/null
+++ b/toolkit/components/antitracking/test/xpcshell/data/font.woff
Binary files differ
diff --git a/toolkit/components/antitracking/test/xpcshell/head.js b/toolkit/components/antitracking/test/xpcshell/head.js
new file mode 100644
index 0000000000..f9bf797641
--- /dev/null
+++ b/toolkit/components/antitracking/test/xpcshell/head.js
@@ -0,0 +1,11 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* import-globals-from ../../../../components/url-classifier/tests/unit/head_urlclassifier.js */
+
+const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+const { TestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TestUtils.sys.mjs"
+);
diff --git a/toolkit/components/antitracking/test/xpcshell/test_ExceptionListService.js b/toolkit/components/antitracking/test/xpcshell/test_ExceptionListService.js
new file mode 100644
index 0000000000..4063d067f5
--- /dev/null
+++ b/toolkit/components/antitracking/test/xpcshell/test_ExceptionListService.js
@@ -0,0 +1,107 @@
+// This test ensures that the URL decoration annotations service works as
+// expected, and also we successfully downgrade document.referrer to the
+// eTLD+1 URL when tracking identifiers controlled by this service are
+// present in the referrer URI.
+
+"use strict";
+
+/* Unit tests for the nsIPartitioningExceptionListService implementation. */
+
+const { RemoteSettings } = ChromeUtils.importESModule(
+ "resource://services-settings/remote-settings.sys.mjs"
+);
+
+const COLLECTION_NAME = "partitioning-exempt-urls";
+const PREF_NAME = "privacy.restrict3rdpartystorage.skip_list";
+
+do_get_profile();
+
+class UpdateEvent extends EventTarget {}
+function waitForEvent(element, eventName) {
+ return new Promise(function (resolve) {
+ element.addEventListener(eventName, e => resolve(e.detail), { once: true });
+ });
+}
+
+add_task(async _ => {
+ let peuService = Cc[
+ "@mozilla.org/partitioning/exception-list-service;1"
+ ].getService(Ci.nsIPartitioningExceptionListService);
+
+ // Make sure we have a pref initially, since the exception list service
+ // requires it.
+ Services.prefs.setStringPref(PREF_NAME, "");
+
+ let updateEvent = new UpdateEvent();
+ let records = [
+ {
+ id: "1",
+ last_modified: 1000000000000001,
+ firstPartyOrigin: "https://example.org",
+ thirdPartyOrigin: "https://tracking.example.com",
+ },
+ ];
+
+ // Add some initial data
+ let db = RemoteSettings(COLLECTION_NAME).db;
+ await db.importChanges({}, Date.now(), records);
+
+ let promise = waitForEvent(updateEvent, "update");
+ let obs = data => {
+ let event = new CustomEvent("update", { detail: data });
+ updateEvent.dispatchEvent(event);
+ };
+ peuService.registerAndRunExceptionListObserver(obs);
+ let list = await promise;
+ Assert.equal(list, "", "No items in the list");
+
+ // Second event is from the RemoteSettings record.
+ list = await waitForEvent(updateEvent, "update");
+ Assert.equal(
+ list,
+ "https://example.org,https://tracking.example.com",
+ "Has one item in the list"
+ );
+
+ records.push({
+ id: "2",
+ last_modified: 1000000000000002,
+ firstPartyOrigin: "https://foo.org",
+ thirdPartyOrigin: "https://bar.com",
+ });
+
+ promise = waitForEvent(updateEvent, "update");
+ await RemoteSettings(COLLECTION_NAME).emit("sync", {
+ data: { current: records },
+ });
+ list = await promise;
+ Assert.equal(
+ list,
+ "https://example.org,https://tracking.example.com;https://foo.org,https://bar.com",
+ "Has several items in the list"
+ );
+
+ promise = waitForEvent(updateEvent, "update");
+ Services.prefs.setStringPref(PREF_NAME, "https://test.com,https://test3.com");
+ list = await promise;
+ Assert.equal(
+ list,
+ "https://test.com,https://test3.com;https://example.org,https://tracking.example.com;https://foo.org,https://bar.com",
+ "Has several items in the list"
+ );
+
+ promise = waitForEvent(updateEvent, "update");
+ Services.prefs.setStringPref(
+ PREF_NAME,
+ "https://test.com,https://test3.com;https://abc.com,https://def.com"
+ );
+ list = await promise;
+ Assert.equal(
+ list,
+ "https://test.com,https://test3.com;https://abc.com,https://def.com;https://example.org,https://tracking.example.com;https://foo.org,https://bar.com",
+ "Has several items in the list"
+ );
+
+ peuService.unregisterExceptionListObserver(obs);
+ await db.clear();
+});
diff --git a/toolkit/components/antitracking/test/xpcshell/test_cookie_behavior.js b/toolkit/components/antitracking/test/xpcshell/test_cookie_behavior.js
new file mode 100644
index 0000000000..3ce1f8bfb7
--- /dev/null
+++ b/toolkit/components/antitracking/test/xpcshell/test_cookie_behavior.js
@@ -0,0 +1,94 @@
+/* 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/. */
+
+// Note: This test may cause intermittents if run at exactly midnight.
+
+"use strict";
+
+const PREF_FPI = "privacy.firstparty.isolate";
+const PREF_COOKIE_BEHAVIOR = "network.cookie.cookieBehavior";
+const PREF_COOKIE_BEHAVIOR_PBMODE = "network.cookie.cookieBehavior.pbmode";
+
+registerCleanupFunction(() => {
+ Services.prefs.clearUserPref(PREF_FPI);
+ Services.prefs.clearUserPref(PREF_COOKIE_BEHAVIOR);
+ Services.prefs.clearUserPref(PREF_COOKIE_BEHAVIOR_PBMODE);
+});
+
+add_task(function test_FPI_off() {
+ Services.prefs.setBoolPref(PREF_FPI, false);
+
+ for (let i = 0; i <= Ci.nsICookieService.BEHAVIOR_LAST; ++i) {
+ Services.prefs.setIntPref(PREF_COOKIE_BEHAVIOR, i);
+ equal(Services.prefs.getIntPref(PREF_COOKIE_BEHAVIOR), i);
+ equal(Services.cookies.getCookieBehavior(false), i);
+ }
+
+ Services.prefs.clearUserPref(PREF_COOKIE_BEHAVIOR);
+
+ for (let i = 0; i <= Ci.nsICookieService.BEHAVIOR_LAST; ++i) {
+ Services.prefs.setIntPref(PREF_COOKIE_BEHAVIOR_PBMODE, i);
+ equal(Services.prefs.getIntPref(PREF_COOKIE_BEHAVIOR_PBMODE), i);
+ equal(Services.cookies.getCookieBehavior(true), i);
+ }
+});
+
+add_task(function test_FPI_on() {
+ Services.prefs.setBoolPref(PREF_FPI, true);
+
+ for (let i = 0; i <= Ci.nsICookieService.BEHAVIOR_LAST; ++i) {
+ Services.prefs.setIntPref(PREF_COOKIE_BEHAVIOR, i);
+ equal(Services.prefs.getIntPref(PREF_COOKIE_BEHAVIOR), i);
+ equal(
+ Services.cookies.getCookieBehavior(false),
+ i == Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN
+ ? Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER
+ : i
+ );
+ }
+
+ Services.prefs.clearUserPref(PREF_COOKIE_BEHAVIOR);
+
+ for (let i = 0; i <= Ci.nsICookieService.BEHAVIOR_LAST; ++i) {
+ Services.prefs.setIntPref(PREF_COOKIE_BEHAVIOR_PBMODE, i);
+ equal(Services.prefs.getIntPref(PREF_COOKIE_BEHAVIOR_PBMODE), i);
+ equal(
+ Services.cookies.getCookieBehavior(true),
+ i == Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN
+ ? Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER
+ : i
+ );
+ }
+
+ Services.prefs.clearUserPref(PREF_FPI);
+});
+
+add_task(function test_private_cookieBehavior_mirroring() {
+ // Test that the private cookieBehavior getter will return the regular pref if
+ // the regular pref has a user value and the private pref has a default value.
+ Services.prefs.clearUserPref(PREF_COOKIE_BEHAVIOR_PBMODE);
+ for (let i = 0; i <= Ci.nsICookieService.BEHAVIOR_LAST; ++i) {
+ Services.prefs.setIntPref(PREF_COOKIE_BEHAVIOR, i);
+ if (!Services.prefs.prefHasUserValue(PREF_COOKIE_BEHAVIOR)) {
+ continue;
+ }
+
+ equal(Services.cookies.getCookieBehavior(true), i);
+ }
+
+ // Test that the private cookieBehavior getter will always return the private
+ // pref if the private cookieBehavior has a user value.
+ for (let i = 0; i <= Ci.nsICookieService.BEHAVIOR_LAST; ++i) {
+ Services.prefs.setIntPref(PREF_COOKIE_BEHAVIOR_PBMODE, i);
+ if (!Services.prefs.prefHasUserValue(PREF_COOKIE_BEHAVIOR_PBMODE)) {
+ continue;
+ }
+
+ for (let j = 0; j <= Ci.nsICookieService.BEHAVIOR_LAST; ++j) {
+ Services.prefs.setIntPref(PREF_COOKIE_BEHAVIOR, j);
+
+ equal(Services.cookies.getCookieBehavior(true), i);
+ }
+ }
+});
diff --git a/toolkit/components/antitracking/test/xpcshell/test_getPartitionKeyFromURL.js b/toolkit/components/antitracking/test/xpcshell/test_getPartitionKeyFromURL.js
new file mode 100644
index 0000000000..cdf2cdec2c
--- /dev/null
+++ b/toolkit/components/antitracking/test/xpcshell/test_getPartitionKeyFromURL.js
@@ -0,0 +1,223 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { CookieXPCShellUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/CookieXPCShellUtils.sys.mjs"
+);
+
+CookieXPCShellUtils.init(this);
+
+const TEST_CASES = [
+ // Tests for different schemes.
+ {
+ url: "http://example.com/",
+ partitionKeySite: "(http,example.com)",
+ partitionKeyWithoutSite: "example.com",
+ },
+ {
+ url: "https://example.com/",
+ partitionKeySite: "(https,example.com)",
+ partitionKeyWithoutSite: "example.com",
+ },
+ // Tests for sub domains
+ {
+ url: "http://sub.example.com/",
+ partitionKeySite: "(http,example.com)",
+ partitionKeyWithoutSite: "example.com",
+ },
+ {
+ url: "http://sub.sub.example.com/",
+ partitionKeySite: "(http,example.com)",
+ partitionKeyWithoutSite: "example.com",
+ },
+ // Tests for path and query.
+ {
+ url: "http://www.example.com/path/to/somewhere/",
+ partitionKeySite: "(http,example.com)",
+ partitionKeyWithoutSite: "example.com",
+ },
+ {
+ url: "http://www.example.com/?query=string",
+ partitionKeySite: "(http,example.com)",
+ partitionKeyWithoutSite: "example.com",
+ },
+ // Tests for other ports.
+ {
+ url: "http://example.com:8080/",
+ partitionKeySite: "(http,example.com)",
+ partitionKeyWithoutSite: "example.com",
+ },
+ {
+ url: "https://example.com:8080/",
+ partitionKeySite: "(https,example.com)",
+ partitionKeyWithoutSite: "example.com",
+ },
+ // Tests for about urls
+ {
+ url: "about:about",
+ partitionKeySite:
+ "(about,about.ef2a7dd5-93bc-417f-a698-142c3116864f.mozilla)",
+ partitionKeyWithoutSite:
+ "about.ef2a7dd5-93bc-417f-a698-142c3116864f.mozilla",
+ },
+ {
+ url: "about:preferences",
+ partitionKeySite:
+ "(about,about.ef2a7dd5-93bc-417f-a698-142c3116864f.mozilla)",
+ partitionKeyWithoutSite:
+ "about.ef2a7dd5-93bc-417f-a698-142c3116864f.mozilla",
+ },
+ // Test for ip addresses
+ {
+ url: "http://127.0.0.1/",
+ partitionKeySite: "(http,127.0.0.1)",
+ partitionKeyWithoutSite: "127.0.0.1",
+ },
+ {
+ url: "http://127.0.0.1:8080/",
+ partitionKeySite: "(http,127.0.0.1,8080)",
+ partitionKeyWithoutSite: "127.0.0.1",
+ },
+ {
+ url: "http://[2001:db8::ff00:42:8329]",
+ partitionKeySite: "(http,[2001:db8::ff00:42:8329])",
+ partitionKeyWithoutSite: "[2001:db8::ff00:42:8329]",
+ },
+ {
+ url: "http://[2001:db8::ff00:42:8329]:8080",
+ partitionKeySite: "(http,[2001:db8::ff00:42:8329],8080)",
+ partitionKeyWithoutSite: "[2001:db8::ff00:42:8329]",
+ },
+ // Tests for moz-extension
+ {
+ url: "moz-extension://bafa4a3f-5c49-48d6-9788-03489419b70e",
+ partitionKeySite: "",
+ partitionKeyWithoutSite: "",
+ },
+ // Tests for non tld
+ {
+ url: "http://notld",
+ partitionKeySite: "(http,notld)",
+ partitionKeyWithoutSite: "notld",
+ },
+ {
+ url: "http://com",
+ partitionKeySite: "(http,com)",
+ partitionKeyWithoutSite: "com",
+ },
+ {
+ url: "http://com:8080",
+ partitionKeySite: "(http,com,8080)",
+ partitionKeyWithoutSite: "com",
+ },
+];
+
+const TEST_INVALID_URLS = [
+ "",
+ "/foo",
+ "An invalid URL",
+ "https://",
+ "http:///",
+ "http://foo:bar",
+];
+
+add_task(async function test_get_partition_key_from_url() {
+ for (const test of TEST_CASES) {
+ info(`Testing url: ${test.url}`);
+ let partitionKey = ChromeUtils.getPartitionKeyFromURL(test.url);
+
+ Assert.equal(
+ partitionKey,
+ test.partitionKeySite,
+ "The partitionKey is correct."
+ );
+ }
+});
+
+add_task(async function test_get_partition_key_from_url_without_site() {
+ Services.prefs.setBoolPref("privacy.dynamic_firstparty.use_site", false);
+
+ for (const test of TEST_CASES) {
+ info(`Testing url: ${test.url}`);
+ let partitionKey = ChromeUtils.getPartitionKeyFromURL(test.url);
+
+ Assert.equal(
+ partitionKey,
+ test.partitionKeyWithoutSite,
+ "The partitionKey is correct."
+ );
+ }
+
+ Services.prefs.clearUserPref("privacy.dynamic_firstparty.use_site");
+});
+
+add_task(async function test_blob_url() {
+ do_get_profile();
+
+ const server = CookieXPCShellUtils.createServer({
+ hosts: ["example.org", "foo.com"],
+ });
+
+ server.registerPathHandler("/empty", (metadata, response) => {
+ var body = "<h1>Hello!</h1>";
+ response.write(body);
+ });
+
+ server.registerPathHandler("/iframe", (metadata, response) => {
+ var body = `
+ <script>
+ var blobUrl = URL.createObjectURL(new Blob([]));
+ parent.postMessage(blobUrl, "http://example.org");
+ </script>
+ `;
+ response.write(body);
+ });
+
+ let contentPage = await CookieXPCShellUtils.loadContentPage(
+ "http://example.org/empty"
+ );
+
+ let blobUrl = await contentPage.spawn([], async () => {
+ // Create a third-party iframe and create a blob url in there.
+ let f = this.content.document.createElement("iframe");
+ f.src = "http://foo.com/iframe";
+
+ let blob_url = await new Promise(resolve => {
+ this.content.addEventListener("message", event => resolve(event.data), {
+ once: true,
+ });
+ this.content.document.body.append(f);
+ });
+
+ return blob_url;
+ });
+
+ let partitionKey = ChromeUtils.getPartitionKeyFromURL(blobUrl);
+
+ // The partitionKey of the blob url is empty because the principal of the
+ // blob url is the JS principal of the global, which doesn't have
+ // partitionKey. And ChromeUtils.getPartitionKeyFromURL() will get
+ // partitionKey from that principal. So, we will get an empty partitionKey
+ // here.
+ // XXX: The behavior here is debatable.
+ Assert.equal(partitionKey, "", "The partitionKey of blob url is correct.");
+
+ await contentPage.close();
+});
+
+add_task(async function test_throw_with_invalid_URL() {
+ // The API should throw if the url is invalid.
+ for (const invalidURL of TEST_INVALID_URLS) {
+ info(`Testing invalid url: ${invalidURL}`);
+
+ Assert.throws(
+ () => {
+ ChromeUtils.getPartitionKeyFromURL(invalidURL);
+ },
+ /NS_ERROR_MALFORMED_URI/,
+ "It should fail on invalid URLs."
+ );
+ }
+});
diff --git a/toolkit/components/antitracking/test/xpcshell/test_purge_trackers.js b/toolkit/components/antitracking/test/xpcshell/test_purge_trackers.js
new file mode 100644
index 0000000000..84694a35d5
--- /dev/null
+++ b/toolkit/components/antitracking/test/xpcshell/test_purge_trackers.js
@@ -0,0 +1,523 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const TRACKING_PAGE = "https://tracking.example.org";
+const TRACKING_PAGE2 =
+ "https://tracking.example.org^partitionKey=(https,example.com)";
+const BENIGN_PAGE = "https://example.com";
+const FOREIGN_PAGE = "https://example.net";
+const FOREIGN_PAGE2 = "https://example.net^partitionKey=(https,example.com)";
+const FOREIGN_PAGE3 = "https://example.net^partitionKey=(https,example.org)";
+
+const { UrlClassifierTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/UrlClassifierTestUtils.sys.mjs"
+);
+const { SiteDataTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/SiteDataTestUtils.sys.mjs"
+);
+const { PermissionTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PermissionTestUtils.sys.mjs"
+);
+const { setTimeout } = ChromeUtils.importESModule(
+ "resource://gre/modules/Timer.sys.mjs"
+);
+
+XPCOMUtils.defineLazyServiceGetter(
+ this,
+ "PurgeTrackerService",
+ "@mozilla.org/purge-tracker-service;1",
+ "nsIPurgeTrackerService"
+);
+
+async function setupTest(aCookieBehavior) {
+ Services.prefs.setIntPref("network.cookie.cookieBehavior", aCookieBehavior);
+ Services.prefs.setBoolPref("privacy.purge_trackers.enabled", true);
+ Services.prefs.setCharPref("privacy.purge_trackers.logging.level", "Debug");
+ Services.prefs.setStringPref(
+ "urlclassifier.trackingAnnotationTable.testEntries",
+ "tracking.example.org"
+ );
+
+ // Enables us to test localStorage in xpcshell.
+ Services.prefs.setBoolPref("dom.storage.client_validation", false);
+}
+
+/**
+ * Test that purging doesn't happen when it shouldn't happen.
+ */
+add_task(async function testNotPurging() {
+ await UrlClassifierTestUtils.addTestTrackers();
+ setupTest(Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN);
+ SiteDataTestUtils.addToCookies({ origin: TRACKING_PAGE });
+
+ Services.prefs.setIntPref(
+ "network.cookie.cookieBehavior",
+ Ci.nsICookieService.BEHAVIOR_ACCEPT
+ );
+ await PurgeTrackerService.purgeTrackingCookieJars();
+ ok(SiteDataTestUtils.hasCookies(TRACKING_PAGE), "cookie remains.");
+ Services.prefs.setIntPref(
+ "network.cookie.cookieBehavior",
+ Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN
+ );
+
+ Services.prefs.setBoolPref("privacy.purge_trackers.enabled", false);
+ await PurgeTrackerService.purgeTrackingCookieJars();
+ ok(SiteDataTestUtils.hasCookies(TRACKING_PAGE), "cookie remains.");
+ Services.prefs.setBoolPref("privacy.purge_trackers.enabled", true);
+
+ Services.prefs.setBoolPref("privacy.sanitize.sanitizeOnShutdown", true);
+ Services.prefs.setBoolPref("privacy.clearOnShutdown.history", true);
+ await PurgeTrackerService.purgeTrackingCookieJars();
+ ok(SiteDataTestUtils.hasCookies(TRACKING_PAGE), "cookie remains.");
+ Services.prefs.clearUserPref("privacy.sanitize.sanitizeOnShutdown");
+ Services.prefs.clearUserPref("privacy.clearOnShutdown.history");
+
+ await PurgeTrackerService.purgeTrackingCookieJars();
+ ok(!SiteDataTestUtils.hasCookies(TRACKING_PAGE), "cookie cleared.");
+
+ UrlClassifierTestUtils.cleanupTestTrackers();
+});
+
+/**
+ * Test that cookies indexedDB and localStorage are purged if the cookie is found
+ * on the tracking list and does not have an Interaction Permission.
+ */
+async function testIndexedDBAndLocalStorage() {
+ await UrlClassifierTestUtils.addTestTrackers();
+
+ PermissionTestUtils.add(
+ TRACKING_PAGE,
+ "storageAccessAPI",
+ Services.perms.ALLOW_ACTION
+ );
+
+ SiteDataTestUtils.addToCookies({ origin: BENIGN_PAGE });
+ for (let url of [
+ TRACKING_PAGE,
+ TRACKING_PAGE2,
+ FOREIGN_PAGE,
+ FOREIGN_PAGE2,
+ FOREIGN_PAGE3,
+ ]) {
+ SiteDataTestUtils.addToLocalStorage(url);
+ SiteDataTestUtils.addToCookies({ origin: url });
+ await SiteDataTestUtils.addToIndexedDB(url);
+ }
+
+ // Purge while storage access permission exists.
+ await PurgeTrackerService.purgeTrackingCookieJars();
+
+ for (let url of [TRACKING_PAGE, TRACKING_PAGE2]) {
+ ok(
+ SiteDataTestUtils.hasCookies(url),
+ "cookie remains while storage access permission exists."
+ );
+ ok(
+ SiteDataTestUtils.hasLocalStorage(url),
+ "localStorage should not have been removed while storage access permission exists."
+ );
+ Assert.greater(
+ await SiteDataTestUtils.getQuotaUsage(url),
+ 0,
+ `We have data for ${url}`
+ );
+ }
+
+ // Run purge after storage access permission has been removed.
+ PermissionTestUtils.remove(TRACKING_PAGE, "storageAccessAPI");
+ await PurgeTrackerService.purgeTrackingCookieJars();
+
+ ok(
+ SiteDataTestUtils.hasCookies(BENIGN_PAGE),
+ "A non-tracking page should retain cookies after purging"
+ );
+
+ for (let url of [FOREIGN_PAGE, FOREIGN_PAGE2, FOREIGN_PAGE3]) {
+ ok(
+ SiteDataTestUtils.hasCookies(url),
+ `A non-tracking foreign page should retain cookies after purging`
+ );
+ ok(
+ SiteDataTestUtils.hasLocalStorage(url),
+ `localStorage for ${url} should not have been removed.`
+ );
+ Assert.greater(
+ await SiteDataTestUtils.getQuotaUsage(url),
+ 0,
+ `We have data for ${url}`
+ );
+ }
+
+ // Cookie should have been removed.
+
+ for (let url of [TRACKING_PAGE, TRACKING_PAGE2]) {
+ ok(
+ !SiteDataTestUtils.hasCookies(url),
+ "cookie is removed after purge with no storage access permission."
+ );
+ ok(
+ !SiteDataTestUtils.hasLocalStorage(url),
+ "localStorage should have been removed"
+ );
+ Assert.equal(
+ await SiteDataTestUtils.getQuotaUsage(url),
+ 0,
+ "quota storage was deleted"
+ );
+ }
+
+ UrlClassifierTestUtils.cleanupTestTrackers();
+}
+
+/**
+ * Test that trackers are treated based on their base domain, not origin.
+ */
+async function testBaseDomain() {
+ await UrlClassifierTestUtils.addTestTrackers();
+
+ let associatedOrigins = [
+ "https://itisatracker.org",
+ "https://sub.itisatracker.org",
+ "https://www.itisatracker.org",
+ "https://sub.sub.sub.itisatracker.org",
+ "http://itisatracker.org",
+ "http://sub.itisatracker.org",
+ ];
+
+ for (let permissionOrigin of associatedOrigins) {
+ // Only one of the associated origins gets permission, but
+ // all should be exempt from purging.
+ PermissionTestUtils.add(
+ permissionOrigin,
+ "storageAccessAPI",
+ Services.perms.ALLOW_ACTION
+ );
+
+ for (let origin of associatedOrigins) {
+ SiteDataTestUtils.addToCookies({ origin });
+ }
+
+ // Add another tracker to verify we're actually purging.
+ SiteDataTestUtils.addToCookies({ origin: TRACKING_PAGE });
+
+ await PurgeTrackerService.purgeTrackingCookieJars();
+
+ for (let origin of associatedOrigins) {
+ ok(
+ SiteDataTestUtils.hasCookies(origin),
+ `${origin} should have retained its cookies when permission is set for ${permissionOrigin}.`
+ );
+ }
+
+ ok(
+ !SiteDataTestUtils.hasCookies(TRACKING_PAGE),
+ "cookie is removed after purge with no storage access permission."
+ );
+
+ PermissionTestUtils.remove(permissionOrigin, "storageAccessAPI");
+ await SiteDataTestUtils.clear();
+ }
+
+ UrlClassifierTestUtils.cleanupTestTrackers();
+}
+
+/**
+ * Test that trackers are not cleared if they are associated
+ * with an entry on the entity list that has user interaction.
+ */
+async function testUserInteraction(ownerPage) {
+ Services.prefs.setBoolPref(
+ "privacy.purge_trackers.consider_entity_list",
+ true
+ );
+ // The test URL for the entity list for annotation is
+ // itisatrap.org/?resource=example.org, so we need to
+ // add example.org as a tracker.
+ Services.prefs.setCharPref(
+ "urlclassifier.trackingAnnotationTable.testEntries",
+ "example.org"
+ );
+ await UrlClassifierTestUtils.addTestTrackers();
+
+ // example.org and itisatrap.org are hard coded test values on the entity list.
+ const RESOURCE_PAGE = "https://example.org";
+
+ PermissionTestUtils.add(
+ ownerPage,
+ "storageAccessAPI",
+ Services.perms.ALLOW_ACTION
+ );
+
+ SiteDataTestUtils.addToCookies({ origin: RESOURCE_PAGE });
+
+ // Add another tracker to verify we're actually purging.
+ SiteDataTestUtils.addToCookies({
+ origin: "https://another-tracking.example.net",
+ });
+
+ await PurgeTrackerService.purgeTrackingCookieJars();
+
+ ok(
+ SiteDataTestUtils.hasCookies(RESOURCE_PAGE),
+ `${RESOURCE_PAGE} should have retained its cookies when permission is set for ${ownerPage}.`
+ );
+
+ ok(
+ !SiteDataTestUtils.hasCookies("https://another-tracking.example.net"),
+ "cookie is removed after purge with no storage access permission."
+ );
+
+ Services.prefs.setBoolPref(
+ "privacy.purge_trackers.consider_entity_list",
+ false
+ );
+
+ await PurgeTrackerService.purgeTrackingCookieJars();
+
+ ok(
+ !SiteDataTestUtils.hasCookies(RESOURCE_PAGE),
+ `${RESOURCE_PAGE} should not have retained its cookies when permission is set for ${ownerPage} and the entity list pref is off.`
+ );
+
+ PermissionTestUtils.remove(ownerPage, "storageAccessAPI");
+ await SiteDataTestUtils.clear();
+
+ Services.prefs.clearUserPref("privacy.purge_trackers.consider_entity_list");
+ UrlClassifierTestUtils.cleanupTestTrackers();
+}
+
+/**
+ * Test that quota storage (even without cookies) is considered when purging trackers.
+ */
+async function testQuotaStorage() {
+ await UrlClassifierTestUtils.addTestTrackers();
+
+ let testCases = [
+ { localStorage: true, indexedDB: true },
+ { localStorage: false, indexedDB: true },
+ { localStorage: true, indexedDB: false },
+ ];
+
+ for (let { localStorage, indexedDB } of testCases) {
+ PermissionTestUtils.add(
+ TRACKING_PAGE,
+ "storageAccessAPI",
+ Services.perms.ALLOW_ACTION
+ );
+
+ if (localStorage) {
+ for (let url of [
+ TRACKING_PAGE,
+ TRACKING_PAGE2,
+ BENIGN_PAGE,
+ FOREIGN_PAGE,
+ FOREIGN_PAGE2,
+ FOREIGN_PAGE3,
+ ]) {
+ SiteDataTestUtils.addToLocalStorage(url);
+ }
+ }
+
+ if (indexedDB) {
+ for (let url of [
+ TRACKING_PAGE,
+ TRACKING_PAGE2,
+ BENIGN_PAGE,
+ FOREIGN_PAGE,
+ FOREIGN_PAGE2,
+ FOREIGN_PAGE3,
+ ]) {
+ await SiteDataTestUtils.addToIndexedDB(url);
+ }
+ }
+
+ // Purge while storage access permission exists.
+ await PurgeTrackerService.purgeTrackingCookieJars();
+
+ if (localStorage) {
+ ok(
+ SiteDataTestUtils.hasLocalStorage(TRACKING_PAGE),
+ "localStorage should not have been removed while storage access permission exists."
+ );
+ }
+
+ if (indexedDB) {
+ for (let url of [
+ TRACKING_PAGE,
+ TRACKING_PAGE2,
+ FOREIGN_PAGE,
+ FOREIGN_PAGE2,
+ FOREIGN_PAGE3,
+ ]) {
+ Assert.greater(
+ await SiteDataTestUtils.getQuotaUsage(url),
+ 0,
+ `We have data for ${url}`
+ );
+ }
+ }
+
+ // Run purge after storage access permission has been removed.
+ PermissionTestUtils.remove(TRACKING_PAGE, "storageAccessAPI");
+ await PurgeTrackerService.purgeTrackingCookieJars();
+
+ if (localStorage) {
+ for (let url of [
+ BENIGN_PAGE,
+ FOREIGN_PAGE,
+ FOREIGN_PAGE2,
+ FOREIGN_PAGE3,
+ ]) {
+ ok(
+ SiteDataTestUtils.hasLocalStorage(url),
+ "localStorage should not have been removed for non-tracking page."
+ );
+ }
+ for (let url of [TRACKING_PAGE, TRACKING_PAGE2]) {
+ ok(
+ !SiteDataTestUtils.hasLocalStorage(url),
+ "localStorage should have been removed."
+ );
+ }
+ }
+
+ if (indexedDB) {
+ for (let url of [
+ BENIGN_PAGE,
+ FOREIGN_PAGE,
+ FOREIGN_PAGE2,
+ FOREIGN_PAGE3,
+ ]) {
+ Assert.greater(
+ await SiteDataTestUtils.getQuotaUsage(url),
+ 0,
+ "quota storage for non-tracking page was not deleted"
+ );
+ }
+
+ for (let url of [TRACKING_PAGE, TRACKING_PAGE2]) {
+ Assert.equal(
+ await SiteDataTestUtils.getQuotaUsage(url),
+ 0,
+ "quota storage was deleted"
+ );
+ }
+ }
+
+ await SiteDataTestUtils.clear();
+ }
+
+ UrlClassifierTestUtils.cleanupTestTrackers();
+}
+
+/**
+ * Test that we correctly delete cookies and storage for sites
+ * with an expired interaction permission.
+ */
+async function testExpiredInteractionPermission() {
+ await UrlClassifierTestUtils.addTestTrackers();
+
+ PermissionTestUtils.add(
+ TRACKING_PAGE,
+ "storageAccessAPI",
+ Services.perms.ALLOW_ACTION,
+ Services.perms.EXPIRE_TIME,
+ Date.now() + 500
+ );
+
+ for (let url of [
+ TRACKING_PAGE,
+ TRACKING_PAGE2,
+ FOREIGN_PAGE,
+ FOREIGN_PAGE2,
+ FOREIGN_PAGE3,
+ ]) {
+ SiteDataTestUtils.addToLocalStorage(url);
+ SiteDataTestUtils.addToCookies({ origin: url });
+ await SiteDataTestUtils.addToIndexedDB(url);
+ }
+
+ // Purge while storage access permission exists.
+ await PurgeTrackerService.purgeTrackingCookieJars();
+
+ for (let url of [
+ TRACKING_PAGE,
+ TRACKING_PAGE2,
+ FOREIGN_PAGE,
+ FOREIGN_PAGE2,
+ FOREIGN_PAGE3,
+ ]) {
+ ok(
+ SiteDataTestUtils.hasCookies(url),
+ "cookie remains while storage access permission exists."
+ );
+ ok(
+ SiteDataTestUtils.hasLocalStorage(url),
+ "localStorage should not have been removed while storage access permission exists."
+ );
+ Assert.greater(
+ await SiteDataTestUtils.getQuotaUsage(url),
+ 0,
+ `We have data for ${url}`
+ );
+ }
+
+ // Run purge after storage access permission has been removed.
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(c => setTimeout(c, 500));
+ await PurgeTrackerService.purgeTrackingCookieJars();
+
+ // Cookie should have been removed.
+ for (let url of [TRACKING_PAGE, TRACKING_PAGE2]) {
+ ok(
+ !SiteDataTestUtils.hasCookies(url),
+ "cookie is removed after purge with no storage access permission."
+ );
+ ok(
+ !SiteDataTestUtils.hasLocalStorage(url),
+ "localStorage should not have been removed while storage access permission exists."
+ );
+ Assert.equal(
+ await SiteDataTestUtils.getQuotaUsage(url),
+ 0,
+ "quota storage was deleted"
+ );
+ }
+
+ // Cookie should not have been removed.
+ for (let url of [FOREIGN_PAGE, FOREIGN_PAGE2, FOREIGN_PAGE3]) {
+ ok(
+ SiteDataTestUtils.hasCookies(url),
+ "cookie remains while storage access permission exists."
+ );
+ ok(
+ SiteDataTestUtils.hasLocalStorage(url),
+ "localStorage should not have been removed while storage access permission exists."
+ );
+ }
+
+ UrlClassifierTestUtils.cleanupTestTrackers();
+}
+
+add_task(async function () {
+ const cookieBehaviors = [
+ Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN,
+ Ci.nsICookieService.BEHAVIOR_LIMIT_FOREIGN,
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER,
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN,
+ ];
+
+ for (let cookieBehavior of cookieBehaviors) {
+ await setupTest(cookieBehavior);
+ await testIndexedDBAndLocalStorage();
+ await testBaseDomain();
+ // example.org and itisatrap.org are hard coded test values on the entity list.
+ await testUserInteraction("https://itisatrap.org");
+ await testUserInteraction(
+ "https://itisatrap.org^firstPartyDomain=example.net"
+ );
+ await testQuotaStorage();
+ await testExpiredInteractionPermission();
+ }
+});
diff --git a/toolkit/components/antitracking/test/xpcshell/test_purge_trackers_telemetry.js b/toolkit/components/antitracking/test/xpcshell/test_purge_trackers_telemetry.js
new file mode 100644
index 0000000000..a1502373dc
--- /dev/null
+++ b/toolkit/components/antitracking/test/xpcshell/test_purge_trackers_telemetry.js
@@ -0,0 +1,175 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const TRACKING_PAGE = "https://tracking.example.org";
+const BENIGN_PAGE = "https://example.com";
+
+const { UrlClassifierTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/UrlClassifierTestUtils.sys.mjs"
+);
+const { SiteDataTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/SiteDataTestUtils.sys.mjs"
+);
+const { PermissionTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PermissionTestUtils.sys.mjs"
+);
+const { TelemetryTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TelemetryTestUtils.sys.mjs"
+);
+
+XPCOMUtils.defineLazyServiceGetter(
+ this,
+ "PurgeTrackerService",
+ "@mozilla.org/purge-tracker-service;1",
+ "nsIPurgeTrackerService"
+);
+
+add_task(async function setup() {
+ Services.prefs.setIntPref(
+ "network.cookie.cookieBehavior",
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER
+ );
+ Services.prefs.setBoolPref("privacy.purge_trackers.enabled", true);
+ Services.prefs.setStringPref(
+ "urlclassifier.trackingAnnotationTable.testEntries",
+ "tracking.example.org"
+ );
+ Services.prefs.setBoolPref(
+ "toolkit.telemetry.testing.overrideProductsCheck",
+ true
+ );
+
+ // Enables us to test localStorage in xpcshell.
+ Services.prefs.setBoolPref("dom.storage.client_validation", false);
+});
+
+/**
+ * Test telemetry for cookie purging.
+ */
+add_task(async function () {
+ await UrlClassifierTestUtils.addTestTrackers();
+
+ let FIVE_DAYS = 5 * 24 * 60 * 60 * 1000;
+
+ PermissionTestUtils.add(
+ TRACKING_PAGE,
+ "storageAccessAPI",
+ Services.perms.ALLOW_ACTION,
+ Services.perms.EXPIRE_TIME,
+ Date.now() + FIVE_DAYS
+ );
+
+ SiteDataTestUtils.addToLocalStorage(TRACKING_PAGE);
+ SiteDataTestUtils.addToCookies({ origin: BENIGN_PAGE });
+ SiteDataTestUtils.addToCookies({ origin: TRACKING_PAGE });
+ await SiteDataTestUtils.addToIndexedDB(TRACKING_PAGE);
+
+ let purgedHistogram = TelemetryTestUtils.getAndClearHistogram(
+ "COOKIE_PURGING_ORIGINS_PURGED"
+ );
+ let notPurgedHistogram = TelemetryTestUtils.getAndClearHistogram(
+ "COOKIE_PURGING_TRACKERS_WITH_USER_INTERACTION"
+ );
+ let remainingDaysHistogram = TelemetryTestUtils.getAndClearHistogram(
+ "COOKIE_PURGING_TRACKERS_USER_INTERACTION_REMAINING_DAYS"
+ );
+ let intervalHistogram = TelemetryTestUtils.getAndClearHistogram(
+ "COOKIE_PURGING_INTERVAL_HOURS"
+ );
+
+ // Purge while storage access permission exists.
+ await PurgeTrackerService.purgeTrackingCookieJars();
+
+ ok(
+ SiteDataTestUtils.hasCookies(TRACKING_PAGE),
+ "cookie remains while storage access permission exists."
+ );
+ ok(
+ SiteDataTestUtils.hasLocalStorage(TRACKING_PAGE),
+ "localStorage should not have been removed while storage access permission exists."
+ );
+ Assert.greater(
+ await SiteDataTestUtils.getQuotaUsage(TRACKING_PAGE),
+ 0,
+ `We have data for ${TRACKING_PAGE}`
+ );
+
+ TelemetryTestUtils.assertHistogram(purgedHistogram, 0, 1);
+ TelemetryTestUtils.assertHistogram(notPurgedHistogram, 1, 1);
+ TelemetryTestUtils.assertHistogram(remainingDaysHistogram, 4, 2);
+ TelemetryTestUtils.assertHistogram(intervalHistogram, 0, 1);
+
+ purgedHistogram = TelemetryTestUtils.getAndClearHistogram(
+ "COOKIE_PURGING_ORIGINS_PURGED"
+ );
+ notPurgedHistogram = TelemetryTestUtils.getAndClearHistogram(
+ "COOKIE_PURGING_TRACKERS_WITH_USER_INTERACTION"
+ );
+ intervalHistogram = TelemetryTestUtils.getAndClearHistogram(
+ "COOKIE_PURGING_INTERVAL_HOURS"
+ );
+
+ // Run purge after storage access permission has been removed.
+ PermissionTestUtils.remove(TRACKING_PAGE, "storageAccessAPI");
+ await PurgeTrackerService.purgeTrackingCookieJars();
+
+ ok(
+ SiteDataTestUtils.hasCookies(BENIGN_PAGE),
+ "A non-tracking page should retain cookies after purging"
+ );
+
+ // Cookie should have been removed.
+ ok(
+ !SiteDataTestUtils.hasCookies(TRACKING_PAGE),
+ "cookie is removed after purge with no storage access permission."
+ );
+ ok(
+ !SiteDataTestUtils.hasLocalStorage(TRACKING_PAGE),
+ "localStorage should not have been removed while storage access permission exists."
+ );
+ Assert.equal(
+ await SiteDataTestUtils.getQuotaUsage(TRACKING_PAGE),
+ 0,
+ "quota storage was deleted"
+ );
+
+ TelemetryTestUtils.assertHistogram(purgedHistogram, 1, 1);
+ Assert.equal(
+ notPurgedHistogram.snapshot().sum,
+ 0,
+ "no origins with user interaction"
+ );
+ TelemetryTestUtils.assertHistogram(intervalHistogram, 0, 1);
+
+ UrlClassifierTestUtils.cleanupTestTrackers();
+});
+
+/**
+ * Test counting correctly across cookies batches
+ */
+add_task(async function () {
+ await UrlClassifierTestUtils.addTestTrackers();
+
+ // Enforce deleting the same origin twice by adding two cookies and setting
+ // the max number of cookies per batch to 1.
+ SiteDataTestUtils.addToCookies({ origin: TRACKING_PAGE, name: "cookie1" });
+ SiteDataTestUtils.addToCookies({ origin: TRACKING_PAGE, name: "cookie2" });
+ Services.prefs.setIntPref("privacy.purge_trackers.max_purge_count", 1);
+
+ let purgedHistogram = TelemetryTestUtils.getAndClearHistogram(
+ "COOKIE_PURGING_ORIGINS_PURGED"
+ );
+
+ await PurgeTrackerService.purgeTrackingCookieJars();
+
+ // Cookie should have been removed.
+ await TestUtils.waitForCondition(
+ () => !SiteDataTestUtils.hasCookies(TRACKING_PAGE),
+ "cookie is removed after purge."
+ );
+
+ TelemetryTestUtils.assertHistogram(purgedHistogram, 1, 1);
+
+ Services.prefs.clearUserPref("privacy.purge_trackers.max_purge_count");
+ UrlClassifierTestUtils.cleanupTestTrackers();
+});
diff --git a/toolkit/components/antitracking/test/xpcshell/test_rejectForeignAllowList.js b/toolkit/components/antitracking/test/xpcshell/test_rejectForeignAllowList.js
new file mode 100644
index 0000000000..97e95a43f4
--- /dev/null
+++ b/toolkit/components/antitracking/test/xpcshell/test_rejectForeignAllowList.js
@@ -0,0 +1,116 @@
+"use strict";
+
+do_get_profile();
+
+// Let's use XPCShellContentUtils to open/close tabs.
+const { XPCShellContentUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/XPCShellContentUtils.sys.mjs"
+);
+
+XPCShellContentUtils.init(this);
+
+var createHttpServer = (...args) => {
+ return XPCShellContentUtils.createHttpServer(...args);
+};
+
+const server = createHttpServer({
+ hosts: ["3rdparty.org", "4thparty.org", "foobar.com"],
+});
+
+async function testThings(prefValue, expected) {
+ await new Promise(resolve =>
+ Services.clearData.deleteData(
+ Ci.nsIClearDataService.CLEAR_ALL_CACHES,
+ resolve
+ )
+ );
+
+ Services.prefs.setCharPref("privacy.rejectForeign.allowList", prefValue);
+
+ let cookiePromise = new Promise(resolve => {
+ server.registerPathHandler("/test3rdPartyChannel", (request, response) => {
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.setHeader("Content-Type", "text/html; charset=utf-8", false);
+ response.write(`<html><img src="http://3rdparty.org/img" /></html>`);
+ });
+
+ server.registerPathHandler("/img", (request, response) => {
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ resolve(request.hasHeader("Cookie") ? request.getHeader("Cookie") : "");
+ response.setHeader("Content-Type", "image/png", false);
+ response.write("Not an image");
+ });
+ });
+
+ // Let's load 3rdparty.org as a 3rd-party.
+ let contentPage = await XPCShellContentUtils.loadContentPage(
+ "http://foobar.com/test3rdPartyChannel"
+ );
+ Assert.equal(await cookiePromise, expected, "Cookies received?");
+ await contentPage.close();
+
+ cookiePromise = new Promise(resolve => {
+ server.registerPathHandler("/test3rdPartyDocument", (request, response) => {
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.setHeader("Content-Type", "text/html; charset=utf-8", false);
+ response.write(
+ `<html><iframe src="http://3rdparty.org/iframe" /></html>`
+ );
+ });
+
+ server.registerPathHandler("/iframe", (request, response) => {
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ resolve(request.hasHeader("Cookie") ? request.getHeader("Cookie") : "");
+ response.setHeader("Content-Type", "text/html; charset=utf-8", false);
+ response.write(`<html><img src="http://4thparty.org/img" /></html>`);
+ });
+
+ server.registerPathHandler("/img", (request, response) => {
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ resolve(request.hasHeader("Cookie") ? request.getHeader("Cookie") : "");
+ response.setHeader("Content-Type", "image/png", false);
+ response.write("Not an image");
+ });
+ });
+
+ // Let's load 3rdparty.org loading a 4th-party.
+ contentPage = await XPCShellContentUtils.loadContentPage(
+ "http://foobar.com/test3rdPartyDocument"
+ );
+ Assert.equal(await cookiePromise, expected, "Cookies received?");
+ await contentPage.close();
+}
+
+add_task(async function test_rejectForeignAllowList() {
+ Services.prefs.setIntPref("network.cookie.cookieBehavior", 1);
+ Services.prefs.setBoolPref(
+ "network.cookie.rejectForeignWithExceptions.enabled",
+ true
+ );
+
+ // We don't want to have 'secure' cookies because our test http server doesn't run in https.
+ Services.prefs.setBoolPref(
+ "network.cookie.sameSite.noneRequiresSecure",
+ false
+ );
+
+ server.registerPathHandler("/setCookies", (request, response) => {
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.setHeader("Content-Type", "text/html; charset=utf-8", false);
+ response.setHeader("Set-Cookie", "cookie=wow; sameSite=none", true);
+ response.write("<html></html>");
+ });
+
+ // Let's set a cookie.
+ let contentPage = await XPCShellContentUtils.loadContentPage(
+ "http://3rdparty.org/setCookies"
+ );
+ await contentPage.close();
+ Assert.equal(Services.cookies.cookies.length, 1);
+
+ // Without exceptionlisting, no cookies should be shared.
+ await testThings("", "");
+
+ // Let's exceptionlist 3rdparty.org
+ await testThings("3rdparty.org", "cookie=wow");
+});
diff --git a/toolkit/components/antitracking/test/xpcshell/test_staticPartition_authhttp.js b/toolkit/components/antitracking/test/xpcshell/test_staticPartition_authhttp.js
new file mode 100644
index 0000000000..0492f5ff2a
--- /dev/null
+++ b/toolkit/components/antitracking/test/xpcshell/test_staticPartition_authhttp.js
@@ -0,0 +1,125 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+"use strict";
+
+const { CookieXPCShellUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/CookieXPCShellUtils.sys.mjs"
+);
+
+CookieXPCShellUtils.init(this);
+
+function Requestor() {}
+Requestor.prototype = {
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIInterfaceRequestor",
+ "nsIAuthPrompt2",
+ ]),
+
+ getInterface(iid) {
+ if (iid.equals(Ci.nsIAuthPrompt2)) {
+ return this;
+ }
+
+ throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE);
+ },
+
+ promptAuth(channel, level, authInfo) {
+ Assert.equal("secret", authInfo.realm);
+ // No passwords in the URL -> nothing should be prefilled
+ Assert.equal(authInfo.username, "");
+ Assert.equal(authInfo.password, "");
+ Assert.equal(authInfo.domain, "");
+
+ authInfo.username = "guest";
+ authInfo.password = "guest";
+
+ return true;
+ },
+
+ asyncPromptAuth(chan, cb, ctx, lvl, info) {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
+ },
+};
+
+let observer = channel => {
+ if (
+ !(channel instanceof Ci.nsIHttpChannel && channel.URI.host === "localhost")
+ ) {
+ return;
+ }
+ channel.notificationCallbacks = new Requestor();
+};
+Services.obs.addObserver(observer, "http-on-modify-request");
+
+add_task(async () => {
+ do_get_profile();
+
+ Services.prefs.setBoolPref("network.predictor.enabled", false);
+ Services.prefs.setBoolPref("network.predictor.enable-prefetch", false);
+ Services.prefs.setBoolPref("network.http.rcwn.enabled", false);
+ Services.prefs.setIntPref("network.cookie.cookieBehavior", 0);
+ Services.prefs.setIntPref("network.auth.subresource-http-auth-allow", 2);
+
+ for (let test of [true, false]) {
+ Cc["@mozilla.org/network/http-auth-manager;1"]
+ .getService(Ci.nsIHttpAuthManager)
+ .clearAll();
+
+ await new Promise(resolve =>
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, resolve)
+ );
+
+ info("Enabling network state partitioning");
+ Services.prefs.setBoolPref("privacy.partition.network_state", test);
+
+ const httpserv = new HttpServer();
+ httpserv.registerPathHandler("/auth", (metadata, response) => {
+ // btoa("guest:guest"), but that function is not available here
+ const expectedHeader = "Basic Z3Vlc3Q6Z3Vlc3Q=";
+
+ let body;
+ if (
+ metadata.hasHeader("Authorization") &&
+ metadata.getHeader("Authorization") == expectedHeader
+ ) {
+ response.setStatusLine(metadata.httpVersion, 200, "OK, authorized");
+ response.setHeader("WWW-Authenticate", 'Basic realm="secret"', false);
+
+ body = "success";
+ } else {
+ // didn't know guest:guest, failure
+ response.setStatusLine(metadata.httpVersion, 401, "Unauthorized");
+ response.setHeader("WWW-Authenticate", 'Basic realm="secret"', false);
+
+ body = "failed";
+ }
+
+ response.bodyOutputStream.write(body, body.length);
+ });
+
+ httpserv.start(-1);
+ const URL = "http://localhost:" + httpserv.identity.primaryPort;
+
+ const httpHandler = Cc[
+ "@mozilla.org/network/protocol;1?name=http"
+ ].getService(Ci.nsIHttpProtocolHandler);
+
+ const contentPage = await CookieXPCShellUtils.loadContentPage(
+ URL + "/auth?r=" + Math.random()
+ );
+ await contentPage.close();
+
+ let key;
+ if (test) {
+ key = `^partitionKey=%28http%2Clocalhost%2C${httpserv.identity.primaryPort}%29:http://localhost:${httpserv.identity.primaryPort}`;
+ } else {
+ key = `:http://localhost:${httpserv.identity.primaryPort}`;
+ }
+
+ Assert.equal(httpHandler.authCacheKeys.includes(key), true, "Key found!");
+
+ await new Promise(resolve => httpserv.stop(resolve));
+ }
+});
diff --git a/toolkit/components/antitracking/test/xpcshell/test_staticPartition_clientAuthRemember.js b/toolkit/components/antitracking/test/xpcshell/test_staticPartition_clientAuthRemember.js
new file mode 100644
index 0000000000..ba2f6c6894
--- /dev/null
+++ b/toolkit/components/antitracking/test/xpcshell/test_staticPartition_clientAuthRemember.js
@@ -0,0 +1,129 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+"use strict";
+
+let cars = Cc["@mozilla.org/security/clientAuthRememberService;1"].getService(
+ Ci.nsIClientAuthRememberService
+);
+let certDB = Cc["@mozilla.org/security/x509certdb;1"].getService(
+ Ci.nsIX509CertDB
+);
+
+function getOAWithPartitionKey(
+ { scheme = "https", topLevelBaseDomain, port = null } = {},
+ originAttributes = {}
+) {
+ if (!topLevelBaseDomain || !scheme) {
+ return originAttributes;
+ }
+
+ return {
+ ...originAttributes,
+ partitionKey: `(${scheme},${topLevelBaseDomain}${port ? `,${port}` : ""})`,
+ };
+}
+
+// These are not actual server and client certs. The ClientAuthRememberService
+// does not care which certs we store decisions for, as long as they're valid.
+let [clientCert] = certDB.getCerts();
+
+function addSecurityInfo({ host, topLevelBaseDomain, originAttributes = {} }) {
+ let attrs = getOAWithPartitionKey({ topLevelBaseDomain }, originAttributes);
+ cars.rememberDecisionScriptable(host, attrs, clientCert);
+}
+
+function testSecurityInfo({
+ host,
+ topLevelBaseDomain,
+ originAttributes = {},
+ expected = true,
+}) {
+ let attrs = getOAWithPartitionKey({ topLevelBaseDomain }, originAttributes);
+
+ let messageSuffix = `for ${host}`;
+ if (topLevelBaseDomain) {
+ messageSuffix += ` partitioned under ${topLevelBaseDomain}`;
+ }
+
+ let hasRemembered = cars.hasRememberedDecisionScriptable(host, attrs, {});
+
+ Assert.equal(
+ hasRemembered,
+ expected,
+ `CAR ${expected ? "is set" : "is not set"} ${messageSuffix}`
+ );
+}
+
+function addTestEntries() {
+ let entries = [
+ { host: "example.net" },
+ { host: "test.example.net" },
+ { host: "example.org" },
+ { host: "example.com", topLevelBaseDomain: "example.net" },
+ {
+ host: "test.example.net",
+ topLevelBaseDomain: "example.org",
+ },
+ {
+ host: "foo.example.com",
+ originAttributes: {
+ privateBrowsingId: 1,
+ },
+ },
+ ];
+
+ info("Add test state");
+ entries.forEach(addSecurityInfo);
+ info("Ensure we have the correct state initially");
+ entries.forEach(testSecurityInfo);
+}
+
+add_task(async () => {
+ addTestEntries();
+
+ info("Should not be set for unrelated host");
+ [undefined, "example.org", "example.net", "example.com"].forEach(
+ topLevelBaseDomain =>
+ testSecurityInfo({
+ host: "mochit.test",
+ topLevelBaseDomain,
+ expected: false,
+ })
+ );
+
+ info("Should not be set for unrelated subdomain");
+ testSecurityInfo({ host: "foo.example.net", expected: false });
+
+ info("Should not be set for unpartitioned first party");
+ testSecurityInfo({
+ host: "example.com",
+ expected: false,
+ });
+
+ info("Should not be set under different first party");
+ testSecurityInfo({
+ host: "example.com",
+ topLevelBaseDomain: "example.org",
+ expected: false,
+ });
+ testSecurityInfo({
+ host: "test.example.net",
+ topLevelBaseDomain: "example.com",
+ expected: false,
+ });
+
+ info("Should not be set in partitioned context");
+ ["example.com", "example.net", "example.org", "mochi.test"].forEach(
+ topLevelBaseDomain =>
+ testSecurityInfo({
+ host: "foo.example.com",
+ topLevelBaseDomain,
+ expected: false,
+ })
+ );
+
+ // Cleanup
+ cars.clearRememberedDecisions();
+});
diff --git a/toolkit/components/antitracking/test/xpcshell/test_staticPartition_font.js b/toolkit/components/antitracking/test/xpcshell/test_staticPartition_font.js
new file mode 100644
index 0000000000..46e230ec3e
--- /dev/null
+++ b/toolkit/components/antitracking/test/xpcshell/test_staticPartition_font.js
@@ -0,0 +1,112 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+const { CookieXPCShellUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/CookieXPCShellUtils.sys.mjs"
+);
+
+CookieXPCShellUtils.init(this);
+
+let gHits = 0;
+
+add_task(async function () {
+ do_get_profile();
+
+ info("Disable predictor and accept all");
+ Services.prefs.setBoolPref("network.predictor.enabled", false);
+ Services.prefs.setBoolPref("network.predictor.enable-prefetch", false);
+ Services.prefs.setBoolPref("network.http.rcwn.enabled", false);
+ Services.prefs.setIntPref("network.cookie.cookieBehavior", 0);
+
+ const server = CookieXPCShellUtils.createServer({
+ hosts: ["example.org", "foo.com", "bar.com"],
+ });
+
+ server.registerFile(
+ "/font.woff",
+ do_get_file("data/font.woff"),
+ (_, response) => {
+ response.setHeader("Access-Control-Allow-Origin", "*", false);
+ gHits++;
+ }
+ );
+
+ server.registerPathHandler("/font", (request, response) => {
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.setHeader("Content-Type", "text/html", false);
+ let body = `
+ <style type="text/css">
+ @font-face {
+ font-family: foo;
+ src: url("http://example.org/font.woff") format('woff');
+ }
+ body { font-family: foo }
+ </style>
+ <iframe src="http://example.org/font-iframe">
+ </iframe>`;
+ response.bodyOutputStream.write(body, body.length);
+ });
+
+ server.registerPathHandler("/font-iframe", (request, response) => {
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.setHeader("Content-Type", "text/html", false);
+ let body = `
+ <style type="text/css">
+ @font-face {
+ font-family: foo;
+ src: url("http://example.org/font.woff") format('woff');
+ }
+ body { font-family: foo }
+ </style>`;
+ response.bodyOutputStream.write(body, body.length);
+ });
+
+ const tests = [
+ {
+ prefValue: true,
+ hitsCount: 5,
+ },
+ {
+ prefValue: false,
+ // The font in page B/C is CORS, the channel will be flagged with
+ // nsIRequest::LOAD_ANONYMOUS.
+ // The flag makes the font in A and B/C use different cache key.
+ hitsCount: 2,
+ },
+ ];
+
+ for (let test of tests) {
+ info("Clear network caches");
+ Services.cache2.clear();
+
+ info("Reset the hits count");
+ gHits = 0;
+
+ info("Enabling network state partitioning");
+ Services.prefs.setBoolPref(
+ "privacy.partition.network_state",
+ test.prefValue
+ );
+
+ info("Let's load a page with origin A");
+ let contentPage = await CookieXPCShellUtils.loadContentPage(
+ "http://example.org/font"
+ );
+ await contentPage.close();
+
+ info("Let's load a page with origin B");
+ contentPage = await CookieXPCShellUtils.loadContentPage(
+ "http://foo.com/font"
+ );
+ await contentPage.close();
+
+ info("Let's load a page with origin C");
+ contentPage = await CookieXPCShellUtils.loadContentPage(
+ "http://bar.com/font"
+ );
+ await contentPage.close();
+
+ Assert.equal(gHits, test.hitsCount, "The number of hits match");
+ }
+});
diff --git a/toolkit/components/antitracking/test/xpcshell/test_staticPartition_image.js b/toolkit/components/antitracking/test/xpcshell/test_staticPartition_image.js
new file mode 100644
index 0000000000..7492d2267a
--- /dev/null
+++ b/toolkit/components/antitracking/test/xpcshell/test_staticPartition_image.js
@@ -0,0 +1,86 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+const { CookieXPCShellUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/CookieXPCShellUtils.sys.mjs"
+);
+
+CookieXPCShellUtils.init(this);
+
+let gHits = 0;
+
+add_task(async function () {
+ do_get_profile();
+
+ info("Disable predictor and accept all");
+ Services.prefs.setBoolPref("network.predictor.enabled", false);
+ Services.prefs.setBoolPref("network.predictor.enable-prefetch", false);
+ Services.prefs.setBoolPref("network.http.rcwn.enabled", false);
+ Services.prefs.setIntPref("network.cookie.cookieBehavior", 0);
+
+ const server = CookieXPCShellUtils.createServer({
+ hosts: ["example.org", "foo.com"],
+ });
+ server.registerPathHandler("/image.png", (metadata, response) => {
+ gHits++;
+ response.setHeader("Cache-Control", "max-age=10000", false);
+ response.setStatusLine(metadata.httpVersion, 200, "OK");
+ response.setHeader("Content-Type", "image/png", false);
+ var body = atob(
+ "iVBORw0KGgoAAAANSUhEUgAAAAMAAAADCAIAAADZSiLoAAAAEUlEQVQImWP4z8AAQTAamQkAhpcI+DeMzFcAAAAASUVORK5CYII="
+ );
+ response.bodyOutputStream.write(body, body.length);
+ });
+
+ server.registerPathHandler("/image", (metadata, response) => {
+ response.setStatusLine(metadata.httpVersion, 200, "OK");
+ response.setHeader("Content-Type", "text/html", false);
+ var body = `<img src="http://example.org/image.png">`;
+ response.bodyOutputStream.write(body, body.length);
+ });
+
+ const tests = [
+ {
+ prefValue: true,
+ hitsCount: 2,
+ },
+ {
+ prefValue: false,
+ hitsCount: 1,
+ },
+ ];
+
+ for (let test of tests) {
+ info("Clear image and network caches");
+ let imageCache = Cc["@mozilla.org/image/tools;1"]
+ .getService(Ci.imgITools)
+ .getImgCacheForDocument(null);
+ imageCache.clearCache(true); // true=chrome
+ imageCache.clearCache(false); // false=content
+ Services.cache2.clear();
+
+ info("Reset the hits count");
+ gHits = 0;
+
+ info("Enabling network state partitioning");
+ Services.prefs.setBoolPref(
+ "privacy.partition.network_state",
+ test.prefValue
+ );
+
+ info("Let's load a page with origin A");
+ let contentPage = await CookieXPCShellUtils.loadContentPage(
+ "http://example.org/image"
+ );
+ await contentPage.close();
+
+ info("Let's load a page with origin B");
+ contentPage = await CookieXPCShellUtils.loadContentPage(
+ "http://foo.com/image"
+ );
+ await contentPage.close();
+
+ Assert.equal(gHits, test.hitsCount, "The number of hits match");
+ }
+});
diff --git a/toolkit/components/antitracking/test/xpcshell/test_staticPartition_prefetch.js b/toolkit/components/antitracking/test/xpcshell/test_staticPartition_prefetch.js
new file mode 100644
index 0000000000..1215ec2384
--- /dev/null
+++ b/toolkit/components/antitracking/test/xpcshell/test_staticPartition_prefetch.js
@@ -0,0 +1,177 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+"use strict";
+
+const { CookieXPCShellUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/CookieXPCShellUtils.sys.mjs"
+);
+
+// Small red image.
+const IMG_BYTES = atob(
+ "iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12" +
+ "P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=="
+);
+
+let gHints = 0;
+
+CookieXPCShellUtils.init(this);
+
+function countMatchingCacheEntries(cacheEntries, domain, path) {
+ return cacheEntries
+ .map(entry => entry.uri.asciiSpec)
+ .filter(spec => spec.includes(domain))
+ .filter(spec => spec.includes(path)).length;
+}
+
+async function checkCache(originAttributes) {
+ const loadContextInfo = Services.loadContextInfo.custom(
+ false,
+ originAttributes
+ );
+
+ const data = await new Promise(resolve => {
+ let cacheEntries = [];
+ let cacheVisitor = {
+ onCacheStorageInfo(num, consumption) {},
+ onCacheEntryInfo(uri, idEnhance) {
+ cacheEntries.push({ uri, idEnhance });
+ },
+ onCacheEntryVisitCompleted() {
+ resolve(cacheEntries);
+ },
+ QueryInterface: ChromeUtils.generateQI(["nsICacheStorageVisitor"]),
+ };
+ // Visiting the disk cache also visits memory storage so we do not
+ // need to use Services.cache2.memoryCacheStorage() here.
+ let storage = Services.cache2.diskCacheStorage(loadContextInfo);
+ storage.asyncVisitStorage(cacheVisitor, true);
+ });
+
+ let foundEntryCount = countMatchingCacheEntries(
+ data,
+ "example.org",
+ "image.png"
+ );
+ ok(
+ foundEntryCount > 0,
+ `Cache entries expected for image.png and OA=${originAttributes}`
+ );
+}
+
+add_task(async () => {
+ do_get_profile();
+
+ Services.prefs.setBoolPref("network.prefetch-next", true);
+ Services.prefs.setIntPref("network.cookie.cookieBehavior", 0);
+
+ const server = CookieXPCShellUtils.createServer({
+ hosts: ["example.org", "foo.com"],
+ });
+
+ server.registerPathHandler("/image.png", (metadata, response) => {
+ gHints++;
+ response.setHeader("Cache-Control", "max-age=10000", false);
+ response.setStatusLine(metadata.httpVersion, 200, "OK");
+ response.setHeader("Content-Type", "image/png", false);
+ response.write(IMG_BYTES);
+ });
+
+ server.registerPathHandler("/prefetch", (metadata, response) => {
+ response.setStatusLine(metadata.httpVersion, 200, "OK");
+ response.setHeader("Content-Type", "text/html", false);
+ var body = `<html><head></head><body><script>
+ const link = document.createElement("link")
+ link.setAttribute("rel", "prefetch");
+ link.setAttribute("href", "http://example.org/image.png");
+ document.head.appendChild(link);
+ link.onload = () => {
+ const img = document.createElement("IMG");
+ img.src = "http://example.org/image.png";
+ document.body.appendChild(img);
+ fetch("/done").then(() => {});
+ }
+ </script></body></html>`;
+ response.bodyOutputStream.write(body, body.length);
+ });
+
+ const tests = [
+ {
+ // 2 hints because we have 2 different top-level origins, loading the
+ // same resource. This will end up creating 2 separate cache entries.
+ hints: 2,
+ originAttributes: { partitionKey: "(http,example.org)" },
+ prefValue: true,
+ },
+ {
+ // 1 hint because, with network-state isolation, the cache entry will be
+ // reused for the second loading, even if the top-level origins are
+ // different.
+ hints: 1,
+ originAttributes: {},
+ prefValue: false,
+ },
+ ];
+
+ for (let test of tests) {
+ await new Promise(resolve =>
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, resolve)
+ );
+
+ info("Reset the counter");
+ gHints = 0;
+
+ info("Enabling network state partitioning");
+ Services.prefs.setBoolPref(
+ "privacy.partition.network_state",
+ test.prefValue
+ );
+
+ let complete = new Promise(resolve => {
+ server.registerPathHandler("/done", (metadata, response) => {
+ response.setHeader("Cache-Control", "max-age=10000", false);
+ response.setStatusLine(metadata.httpVersion, 200, "OK");
+ response.setHeader("Content-Type", "text/html", false);
+ var body = "OK";
+ response.bodyOutputStream.write(body, body.length);
+ resolve();
+ });
+ });
+
+ info("Let's load a page with origin A");
+ let contentPage = await CookieXPCShellUtils.loadContentPage(
+ "http://example.org/prefetch"
+ );
+
+ await complete;
+ await checkCache(test.originAttributes);
+ await contentPage.close();
+
+ complete = new Promise(resolve => {
+ server.registerPathHandler("/done", (metadata, response) => {
+ response.setHeader("Cache-Control", "max-age=10000", false);
+ response.setStatusLine(metadata.httpVersion, 200, "OK");
+ response.setHeader("Content-Type", "text/html", false);
+ var body = "OK";
+ response.bodyOutputStream.write(body, body.length);
+ resolve();
+ });
+ });
+
+ info("Let's load a page with origin B");
+ contentPage = await CookieXPCShellUtils.loadContentPage(
+ "http://foo.com/prefetch"
+ );
+
+ await complete;
+ await checkCache(test.originAttributes);
+ await contentPage.close();
+
+ Assert.equal(
+ gHints,
+ test.hints,
+ "We have the current number of requests with pref " + test.prefValue
+ );
+ }
+});
diff --git a/toolkit/components/antitracking/test/xpcshell/test_staticPartition_preload.js b/toolkit/components/antitracking/test/xpcshell/test_staticPartition_preload.js
new file mode 100644
index 0000000000..be61a2e6d4
--- /dev/null
+++ b/toolkit/components/antitracking/test/xpcshell/test_staticPartition_preload.js
@@ -0,0 +1,187 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+"use strict";
+
+const { CookieXPCShellUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/CookieXPCShellUtils.sys.mjs"
+);
+
+let gHints = 0;
+
+CookieXPCShellUtils.init(this);
+
+function countMatchingCacheEntries(cacheEntries, domain, path) {
+ return cacheEntries
+ .map(entry => entry.uri.asciiSpec)
+ .filter(spec => spec.includes(domain))
+ .filter(spec => spec.includes(path)).length;
+}
+
+async function checkCache(originAttributes) {
+ const loadContextInfo = Services.loadContextInfo.custom(
+ false,
+ originAttributes
+ );
+
+ const data = await new Promise(resolve => {
+ let cacheEntries = [];
+ let cacheVisitor = {
+ onCacheStorageInfo(num, consumption) {},
+ onCacheEntryInfo(uri, idEnhance) {
+ cacheEntries.push({ uri, idEnhance });
+ },
+ onCacheEntryVisitCompleted() {
+ resolve(cacheEntries);
+ },
+ QueryInterface: ChromeUtils.generateQI(["nsICacheStorageVisitor"]),
+ };
+ // Visiting the disk cache also visits memory storage so we do not
+ // need to use Services.cache2.memoryCacheStorage() here.
+ let storage = Services.cache2.diskCacheStorage(loadContextInfo);
+ storage.asyncVisitStorage(cacheVisitor, true);
+ });
+
+ let foundEntryCount = countMatchingCacheEntries(
+ data,
+ "example.org",
+ "style.css"
+ );
+ ok(
+ foundEntryCount > 0,
+ `Cache entries expected for style.css and OA=${originAttributes}`
+ );
+}
+
+add_task(async () => {
+ do_get_profile();
+
+ Services.prefs.setBoolPref("network.preload", true);
+ Services.prefs.setIntPref("network.cookie.cookieBehavior", 0);
+
+ const server = CookieXPCShellUtils.createServer({
+ hosts: ["example.org", "foo.com"],
+ });
+
+ server.registerPathHandler("/empty", (metadata, response) => {
+ var body = "<h1>Hello!</h1>";
+ response.bodyOutputStream.write(body, body.length);
+ });
+
+ server.registerPathHandler("/style.css", (metadata, response) => {
+ gHints++;
+ response.setHeader("Cache-Control", "max-age=10000", false);
+ response.setStatusLine(metadata.httpVersion, 200, "OK");
+ response.setHeader("Access-Control-Allow-Origin", "*", false);
+ var body = "* { color: red }";
+ response.bodyOutputStream.write(body, body.length);
+ });
+
+ server.registerPathHandler("/preload", (metadata, response) => {
+ response.setStatusLine(metadata.httpVersion, 200, "OK");
+ response.setHeader("Content-Type", "text/html", false);
+ var body = `<html><head></head><body><script>
+ const link = document.createElement("link")
+ link.setAttribute("rel", "preload");
+ link.setAttribute("as", "style");
+ link.setAttribute("href", "http://example.org/style.css");
+ document.head.appendChild(link);
+ link.onload = () => {
+ fetch("/done").then(() => {});
+ };
+ </script></body></html>`;
+ response.bodyOutputStream.write(body, body.length);
+ });
+
+ const tests = [
+ {
+ // 2 hints because we have 2 different top-level origins, loading the
+ // same resource. This will end up creating 2 separate cache entries.
+ hints: 2,
+ prefValue: true,
+ originAttributes: { partitionKey: "(http,example.org)" },
+ },
+ {
+ // 1 hint because, with network-state isolation, the cache entry will be
+ // reused for the second loading, even if the top-level origins are
+ // different.
+ hints: 1,
+ originAttributes: {},
+ prefValue: false,
+ },
+ ];
+
+ for (let test of tests) {
+ await new Promise(resolve =>
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, resolve)
+ );
+
+ info("Reset the shared sheets");
+ let contentPage = await CookieXPCShellUtils.loadContentPage(
+ "http://example.org/empty"
+ );
+
+ await contentPage.spawn([], () =>
+ // eslint-disable-next-line no-undef
+ content.windowUtils.clearSharedStyleSheetCache()
+ );
+
+ await contentPage.close();
+
+ info("Reset the counter");
+ gHints = 0;
+
+ info("Enabling network state partitioning");
+ Services.prefs.setBoolPref(
+ "privacy.partition.network_state",
+ test.prefValue
+ );
+
+ let complete = new Promise(resolve => {
+ server.registerPathHandler("/done", (metadata, response) => {
+ response.setHeader("Cache-Control", "max-age=10000", false);
+ response.setStatusLine(metadata.httpVersion, 200, "OK");
+ response.setHeader("Content-Type", "text/html", false);
+ var body = "OK";
+ response.bodyOutputStream.write(body, body.length);
+ resolve();
+ });
+ });
+
+ info("Let's load a page with origin A");
+ contentPage = await CookieXPCShellUtils.loadContentPage(
+ "http://example.org/preload"
+ );
+
+ await complete;
+ await checkCache(test.originAttributes);
+ await contentPage.close();
+
+ complete = new Promise(resolve => {
+ server.registerPathHandler("/done", (metadata, response) => {
+ response.setHeader("Cache-Control", "max-age=10000", false);
+ response.setStatusLine(metadata.httpVersion, 200, "OK");
+ response.setHeader("Content-Type", "text/html", false);
+ var body = "OK";
+ response.bodyOutputStream.write(body, body.length);
+ resolve();
+ });
+ });
+
+ info("Let's load a page with origin B");
+ contentPage = await CookieXPCShellUtils.loadContentPage(
+ "http://foo.com/preload"
+ );
+
+ await complete;
+ await checkCache(test.originAttributes);
+ await contentPage.close();
+
+ Assert.equal(
+ gHints,
+ test.hints,
+ "We have the current number of requests with pref " + test.prefValue
+ );
+ }
+});
diff --git a/toolkit/components/antitracking/test/xpcshell/test_tracking_db_service.js b/toolkit/components/antitracking/test/xpcshell/test_tracking_db_service.js
new file mode 100644
index 0000000000..0f6ded1218
--- /dev/null
+++ b/toolkit/components/antitracking/test/xpcshell/test_tracking_db_service.js
@@ -0,0 +1,492 @@
+/* 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/. */
+
+// Note: This test may cause intermittents if run at exactly midnight.
+
+"use strict";
+
+const { Sqlite } = ChromeUtils.importESModule(
+ "resource://gre/modules/Sqlite.sys.mjs"
+);
+XPCOMUtils.defineLazyServiceGetter(
+ this,
+ "TrackingDBService",
+ "@mozilla.org/tracking-db-service;1",
+ "nsITrackingDBService"
+);
+
+XPCOMUtils.defineLazyGetter(this, "DB_PATH", function () {
+ return PathUtils.join(PathUtils.profileDir, "protections.sqlite");
+});
+
+const SQL = {
+ insertCustomTimeEvent:
+ "INSERT INTO events (type, count, timestamp)" +
+ "VALUES (:type, :count, date(:timestamp));",
+
+ selectAllEntriesOfType: "SELECT * FROM events WHERE type = :type;",
+
+ selectAll: "SELECT * FROM events",
+};
+
+// Emulate the content blocking log. We do not record the url key, nor
+// do we use the aggregated event number (the last element in the array).
+const LOG = {
+ "https://1.example.com": [
+ [Ci.nsIWebProgressListener.STATE_BLOCKED_TRACKING_CONTENT, true, 1],
+ ],
+ "https://2.example.com": [
+ [Ci.nsIWebProgressListener.STATE_BLOCKED_FINGERPRINTING_CONTENT, true, 1],
+ ],
+ "https://3.example.com": [
+ [Ci.nsIWebProgressListener.STATE_BLOCKED_CRYPTOMINING_CONTENT, true, 2],
+ ],
+ "https://4.example.com": [
+ [Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER, true, 3],
+ ],
+ "https://5.example.com": [
+ [Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER, true, 1],
+ ],
+ // Cookie blocked for other reason, then identified as a tracker
+ "https://6.example.com": [
+ [
+ Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_ALL |
+ Ci.nsIWebProgressListener.STATE_LOADED_LEVEL_1_TRACKING_CONTENT,
+ true,
+ 4,
+ ],
+ ],
+ "https://7.example.com": [
+ [Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_SOCIALTRACKER, true, 1],
+ ],
+ "https://8.example.com": [
+ [Ci.nsIWebProgressListener.STATE_BLOCKED_SOCIALTRACKING_CONTENT, true, 1],
+ ],
+
+ // The contents below should not add to the database.
+ // Cookie loaded but not blocked.
+ "https://10.example.com": [
+ [Ci.nsIWebProgressListener.STATE_COOKIES_LOADED, true, 1],
+ ],
+ // Tracker cookie loaded but not blocked.
+ "https://11.unblocked.example.com": [
+ [Ci.nsIWebProgressListener.STATE_COOKIES_LOADED_TRACKER, true, 1],
+ ],
+ // Social tracker cookie loaded but not blocked.
+ "https://12.example.com": [
+ [Ci.nsIWebProgressListener.STATE_COOKIES_LOADED_SOCIALTRACKER, true, 1],
+ ],
+ // Cookie blocked for other reason (not a tracker)
+ "https://13.example.com": [
+ [Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_BY_PERMISSION, true, 2],
+ ],
+ // Fingerprinters set to block, but this one has an exception
+ "https://14.example.com": [
+ [Ci.nsIWebProgressListener.STATE_BLOCKED_FINGERPRINTING_CONTENT, false, 1],
+ ],
+ // Two fingerprinters replaced with a shims script, should be treated as blocked
+ // and increment the counter.
+ "https://15.example.com": [
+ [Ci.nsIWebProgressListener.STATE_REPLACED_FINGERPRINTING_CONTENT, true, 1],
+ ],
+ "https://16.example.com": [
+ [Ci.nsIWebProgressListener.STATE_REPLACED_FINGERPRINTING_CONTENT, true, 1],
+ ],
+};
+
+do_get_profile();
+
+Services.prefs.setBoolPref("browser.contentblocking.database.enabled", true);
+Services.prefs.setBoolPref(
+ "privacy.socialtracking.block_cookies.enabled",
+ true
+);
+Services.prefs.setBoolPref(
+ "privacy.trackingprotection.fingerprinting.enabled",
+ true
+);
+Services.prefs.setBoolPref(
+ "browser.contentblocking.cfr-milestone.enabled",
+ true
+);
+Services.prefs.setIntPref(
+ "browser.contentblocking.cfr-milestone.update-interval",
+ 0
+);
+Services.prefs.setStringPref(
+ "browser.contentblocking.cfr-milestone.milestones",
+ "[1000, 5000, 10000, 25000, 100000, 500000]"
+);
+
+registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("browser.contentblocking.database.enabled");
+ Services.prefs.clearUserPref("privacy.socialtracking.block_cookies.enabled");
+ Services.prefs.clearUserPref(
+ "privacy.trackingprotection.fingerprinting.enabled"
+ );
+ Services.prefs.clearUserPref("browser.contentblocking.cfr-milestone.enabled");
+ Services.prefs.clearUserPref(
+ "browser.contentblocking.cfr-milestone.update-interval"
+ );
+ Services.prefs.clearUserPref(
+ "browser.contentblocking.cfr-milestone.milestones"
+ );
+});
+
+// This tests that data is added successfully, different types of events should get
+// their own entries, when the type is the same they should be aggregated. Events
+// that are not blocking events should not be recorded. Cookie blocking events
+// should only be recorded if we can identify the cookie as a tracking cookie.
+add_task(async function test_save_and_delete() {
+ await TrackingDBService.saveEvents(JSON.stringify(LOG));
+
+ // Peek in the DB to make sure we have the right data.
+ let db = await Sqlite.openConnection({ path: DB_PATH });
+ // Make sure the items table was created.
+ ok(await db.tableExists("events"), "events table exists");
+
+ // make sure we have the correct contents in the database
+ let rows = await db.execute(SQL.selectAll);
+ equal(
+ rows.length,
+ 5,
+ "Events that should not be saved have not been, length is 4"
+ );
+ rows = await db.execute(SQL.selectAllEntriesOfType, {
+ type: TrackingDBService.TRACKERS_ID,
+ });
+ equal(rows.length, 1, "Only one day has had tracker entries, length is 1");
+ let count = rows[0].getResultByName("count");
+ equal(count, 1, "there is only one tracker entry");
+
+ rows = await db.execute(SQL.selectAllEntriesOfType, {
+ type: TrackingDBService.TRACKING_COOKIES_ID,
+ });
+ equal(rows.length, 1, "Only one day has had cookies entries, length is 1");
+ count = rows[0].getResultByName("count");
+ equal(count, 3, "Cookie entries were aggregated");
+
+ rows = await db.execute(SQL.selectAllEntriesOfType, {
+ type: TrackingDBService.CRYPTOMINERS_ID,
+ });
+ equal(
+ rows.length,
+ 1,
+ "Only one day has had cryptominer entries, length is 1"
+ );
+ count = rows[0].getResultByName("count");
+ equal(count, 1, "there is only one cryptominer entry");
+
+ rows = await db.execute(SQL.selectAllEntriesOfType, {
+ type: TrackingDBService.FINGERPRINTERS_ID,
+ });
+ equal(
+ rows.length,
+ 1,
+ "Only one day has had fingerprinters entries, length is 1"
+ );
+ count = rows[0].getResultByName("count");
+ equal(count, 3, "there are three fingerprinter entries");
+
+ rows = await db.execute(SQL.selectAllEntriesOfType, {
+ type: TrackingDBService.SOCIAL_ID,
+ });
+ equal(rows.length, 1, "Only one day has had social entries, length is 1");
+ count = rows[0].getResultByName("count");
+ equal(count, 2, "there are two social entries");
+
+ // Use the TrackingDBService API to delete the data.
+ await TrackingDBService.clearAll();
+ // Make sure the data was deleted.
+ rows = await db.execute(SQL.selectAll);
+ equal(rows.length, 0, "length is 0");
+ await db.close();
+});
+
+// This tests that content blocking events encountered on the same day get aggregated,
+// and those on different days get seperate entries
+add_task(async function test_timestamp_aggragation() {
+ // This creates the schema.
+ await TrackingDBService.saveEvents(JSON.stringify({}));
+ let db = await Sqlite.openConnection({ path: DB_PATH });
+
+ let yesterday = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString();
+ let today = new Date().toISOString();
+ await db.execute(SQL.insertCustomTimeEvent, {
+ type: TrackingDBService.TRACKERS_ID,
+ count: 4,
+ timestamp: yesterday,
+ });
+ await db.execute(SQL.insertCustomTimeEvent, {
+ type: TrackingDBService.CRYPTOMINERS_ID,
+ count: 3,
+ timestamp: yesterday,
+ });
+ await db.execute(SQL.insertCustomTimeEvent, {
+ type: TrackingDBService.FINGERPRINTERS_ID,
+ count: 2,
+ timestamp: yesterday,
+ });
+ await db.execute(SQL.insertCustomTimeEvent, {
+ type: TrackingDBService.TRACKING_COOKIES_ID,
+ count: 1,
+ timestamp: yesterday,
+ });
+
+ // Add some events for today which must get aggregated
+ await db.execute(SQL.insertCustomTimeEvent, {
+ type: TrackingDBService.TRACKERS_ID,
+ count: 2,
+ timestamp: today,
+ });
+ await db.execute(SQL.insertCustomTimeEvent, {
+ type: TrackingDBService.CRYPTOMINERS_ID,
+ count: 2,
+ timestamp: today,
+ });
+ await db.execute(SQL.insertCustomTimeEvent, {
+ type: TrackingDBService.FINGERPRINTERS_ID,
+ count: 2,
+ timestamp: today,
+ });
+ await db.execute(SQL.insertCustomTimeEvent, {
+ type: TrackingDBService.TRACKING_COOKIES_ID,
+ count: 2,
+ timestamp: today,
+ });
+
+ // Add new events, they will have today's timestamp.
+ await TrackingDBService.saveEvents(JSON.stringify(LOG));
+
+ // Ensure events that are inserted today are not aggregated with past events.
+ let rows = await db.execute(SQL.selectAllEntriesOfType, {
+ type: TrackingDBService.TRACKERS_ID,
+ });
+ equal(rows.length, 2, "Tracker entries for today and yesterday, length is 2");
+ for (let i = 0; i < rows.length; i++) {
+ let count = rows[i].getResultByName("count");
+ if (i == 0) {
+ equal(count, 4, "Yesterday's count is 4");
+ } else if (i == 1) {
+ equal(count, 3, "Today's count is 3, new entries were aggregated");
+ }
+ }
+
+ rows = await db.execute(SQL.selectAllEntriesOfType, {
+ type: TrackingDBService.CRYPTOMINERS_ID,
+ });
+ equal(
+ rows.length,
+ 2,
+ "Cryptominer entries for today and yesterday, length is 2"
+ );
+ for (let i = 0; i < rows.length; i++) {
+ let count = rows[i].getResultByName("count");
+ if (i == 0) {
+ equal(count, 3, "Yesterday's count is 3");
+ } else if (i == 1) {
+ equal(count, 3, "Today's count is 3, new entries were aggregated");
+ }
+ }
+
+ rows = await db.execute(SQL.selectAllEntriesOfType, {
+ type: TrackingDBService.FINGERPRINTERS_ID,
+ });
+ equal(
+ rows.length,
+ 2,
+ "Fingerprinter entries for today and yesterday, length is 2"
+ );
+ for (let i = 0; i < rows.length; i++) {
+ let count = rows[i].getResultByName("count");
+ if (i == 0) {
+ equal(count, 2, "Yesterday's count is 2");
+ } else if (i == 1) {
+ equal(count, 5, "Today's count is 5, new entries were aggregated");
+ }
+ }
+
+ rows = await db.execute(SQL.selectAllEntriesOfType, {
+ type: TrackingDBService.TRACKING_COOKIES_ID,
+ });
+ equal(
+ rows.length,
+ 2,
+ "Tracking Cookies entries for today and yesterday, length is 2"
+ );
+ for (let i = 0; i < rows.length; i++) {
+ let count = rows[i].getResultByName("count");
+ if (i == 0) {
+ equal(count, 1, "Yesterday's count is 1");
+ } else if (i == 1) {
+ equal(count, 5, "Today's count is 5, new entries were aggregated");
+ }
+ }
+
+ // Use the TrackingDBService API to delete the data.
+ await TrackingDBService.clearAll();
+ // Make sure the data was deleted.
+ rows = await db.execute(SQL.selectAll);
+ equal(rows.length, 0, "length is 0");
+ await db.close();
+});
+
+let addEventsToDB = async db => {
+ let d = new Date(1521009000000);
+ let date = d.toISOString();
+ await db.execute(SQL.insertCustomTimeEvent, {
+ type: TrackingDBService.CRYPTOMINERS_ID,
+ count: 3,
+ timestamp: date,
+ });
+
+ date = new Date(d - 2 * 24 * 60 * 60 * 1000).toISOString();
+ await db.execute(SQL.insertCustomTimeEvent, {
+ type: TrackingDBService.TRACKERS_ID,
+ count: 2,
+ timestamp: date,
+ });
+
+ date = new Date(d - 3 * 24 * 60 * 60 * 1000).toISOString();
+ await db.execute(SQL.insertCustomTimeEvent, {
+ type: TrackingDBService.TRACKING_COOKIES_ID,
+ count: 2,
+ timestamp: date,
+ });
+
+ date = new Date(d - 4 * 24 * 60 * 60 * 1000).toISOString();
+ await db.execute(SQL.insertCustomTimeEvent, {
+ type: TrackingDBService.TRACKING_COOKIES_ID,
+ count: 2,
+ timestamp: date,
+ });
+
+ date = new Date(d - 9 * 24 * 60 * 60 * 1000).toISOString();
+ await db.execute(SQL.insertCustomTimeEvent, {
+ type: TrackingDBService.FINGERPRINTERS_ID,
+ count: 2,
+ timestamp: date,
+ });
+};
+
+// This tests that TrackingDBService.getEventsByDateRange can accept two timestamps in unix epoch time
+// and return entries that occur within the timestamps, rounded to the nearest day and inclusive.
+add_task(async function test_getEventsByDateRange() {
+ // This creates the schema.
+ await TrackingDBService.saveEvents(JSON.stringify({}));
+ let db = await Sqlite.openConnection({ path: DB_PATH });
+ await addEventsToDB(db);
+
+ let d = new Date(1521009000000);
+ let daysBefore1 = new Date(d - 24 * 60 * 60 * 1000);
+ let daysBefore4 = new Date(d - 4 * 24 * 60 * 60 * 1000);
+ let daysBefore9 = new Date(d - 9 * 24 * 60 * 60 * 1000);
+
+ let events = await TrackingDBService.getEventsByDateRange(daysBefore1, d);
+ equal(
+ events.length,
+ 1,
+ "There is 1 event entry between the date and one day before, inclusive"
+ );
+
+ events = await TrackingDBService.getEventsByDateRange(daysBefore4, d);
+ equal(
+ events.length,
+ 4,
+ "There is 4 event entries between the date and four days before, inclusive"
+ );
+
+ events = await TrackingDBService.getEventsByDateRange(
+ daysBefore9,
+ daysBefore4
+ );
+ equal(
+ events.length,
+ 2,
+ "There is 2 event entries between nine and four days before, inclusive"
+ );
+
+ await TrackingDBService.clearAll();
+ await db.close();
+});
+
+// This tests that TrackingDBService.sumAllEvents returns the number of
+// tracking events in the database, and can handle 0 entries.
+add_task(async function test_sumAllEvents() {
+ // This creates the schema.
+ await TrackingDBService.saveEvents(JSON.stringify({}));
+ let db = await Sqlite.openConnection({ path: DB_PATH });
+
+ let sum = await TrackingDBService.sumAllEvents();
+ equal(sum, 0, "There have been 0 events recorded");
+
+ // populate the database
+ await addEventsToDB(db);
+
+ sum = await TrackingDBService.sumAllEvents();
+ equal(sum, 11, "There have been 11 events recorded");
+
+ await TrackingDBService.clearAll();
+ await db.close();
+});
+
+// This tests that TrackingDBService.getEarliestRecordedDate returns the
+// earliest date recorded and can handle 0 entries.
+add_task(async function test_getEarliestRecordedDate() {
+ // This creates the schema.
+ await TrackingDBService.saveEvents(JSON.stringify({}));
+ let db = await Sqlite.openConnection({ path: DB_PATH });
+
+ let timestamp = await TrackingDBService.getEarliestRecordedDate();
+ equal(timestamp, null, "There is no earliest recorded date");
+
+ // populate the database
+ await addEventsToDB(db);
+ let d = new Date(1521009000000);
+ let daysBefore9 = new Date(d - 9 * 24 * 60 * 60 * 1000)
+ .toISOString()
+ .split("T")[0];
+
+ timestamp = await TrackingDBService.getEarliestRecordedDate();
+ let date = new Date(timestamp).toISOString().split("T")[0];
+ equal(date, daysBefore9, "The earliest recorded event is nine days before.");
+
+ await TrackingDBService.clearAll();
+ await db.close();
+});
+
+// This tests that a message to CFR is sent when the amount of saved trackers meets a milestone
+add_task(async function test_sendMilestoneNotification() {
+ let milestones = JSON.parse(
+ Services.prefs.getStringPref(
+ "browser.contentblocking.cfr-milestone.milestones"
+ )
+ );
+ // This creates the schema.
+ await TrackingDBService.saveEvents(JSON.stringify({}));
+ let db = await Sqlite.openConnection({ path: DB_PATH });
+ // save number of trackers equal to the first milestone
+ await db.execute(SQL.insertCustomTimeEvent, {
+ type: TrackingDBService.CRYPTOMINERS_ID,
+ count: milestones[0],
+ timestamp: new Date().toISOString(),
+ });
+
+ let awaitNotification = TestUtils.topicObserved(
+ "SiteProtection:ContentBlockingMilestone"
+ );
+
+ // trigger a "save" event to compare the trackers with the milestone.
+ await TrackingDBService.saveEvents(
+ JSON.stringify({
+ "https://1.example.com": [
+ [Ci.nsIWebProgressListener.STATE_BLOCKED_TRACKING_CONTENT, true, 1],
+ ],
+ })
+ );
+ await awaitNotification;
+
+ await TrackingDBService.clearAll();
+ await db.close();
+});
diff --git a/toolkit/components/antitracking/test/xpcshell/test_view_source.js b/toolkit/components/antitracking/test/xpcshell/test_view_source.js
new file mode 100644
index 0000000000..ebd70cf476
--- /dev/null
+++ b/toolkit/components/antitracking/test/xpcshell/test_view_source.js
@@ -0,0 +1,78 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+const { CookieXPCShellUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/CookieXPCShellUtils.sys.mjs"
+);
+
+CookieXPCShellUtils.init(this);
+
+let gCookieHits = 0;
+let gLoadingHits = 0;
+
+add_task(async function () {
+ do_get_profile();
+
+ info("Disable predictor and accept all");
+ Services.prefs.setBoolPref("network.predictor.enabled", false);
+ Services.prefs.setBoolPref("network.predictor.enable-prefetch", false);
+ Services.prefs.setBoolPref("network.http.rcwn.enabled", false);
+ Services.prefs.setIntPref(
+ "network.cookie.cookieBehavior",
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN
+ );
+
+ const server = CookieXPCShellUtils.createServer({
+ hosts: ["example.org"],
+ });
+ server.registerPathHandler("/test", (request, response) => {
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.setHeader("Content-Type", "text/html", false);
+ if (
+ request.hasHeader("Cookie") &&
+ request.getHeader("Cookie") == "foo=bar"
+ ) {
+ gCookieHits++;
+ } else {
+ response.setHeader("Set-Cookie", "foo=bar");
+ }
+
+ gLoadingHits++;
+ var body = "<html></html>";
+ response.bodyOutputStream.write(body, body.length);
+ });
+
+ info("Reset the hits count");
+ gCookieHits = 0;
+ gLoadingHits = 0;
+
+ info("Let's load a page");
+ let contentPage = await CookieXPCShellUtils.loadContentPage(
+ "http://example.org/test?1"
+ );
+ await contentPage.close();
+
+ Assert.equal(gCookieHits, 0, "The number of cookie hits match");
+ Assert.equal(gLoadingHits, 1, "The number of loading hits match");
+
+ info("Let's load the source of the page again to see if it loads from cache");
+ contentPage = await CookieXPCShellUtils.loadContentPage(
+ "view-source:http://example.org/test?1"
+ );
+ await contentPage.close();
+
+ Assert.equal(gCookieHits, 0, "The number of cookie hits match");
+ Assert.equal(gLoadingHits, 1, "The number of loading hits match");
+
+ info(
+ "Let's load the source of the page without hitting the cache to see if the cookie is sent properly"
+ );
+ contentPage = await CookieXPCShellUtils.loadContentPage(
+ "view-source:http://example.org/test?2"
+ );
+ await contentPage.close();
+
+ Assert.equal(gCookieHits, 1, "The number of cookie hits match");
+ Assert.equal(gLoadingHits, 2, "The number of loading hits match");
+});
diff --git a/toolkit/components/antitracking/test/xpcshell/xpcshell.ini b/toolkit/components/antitracking/test/xpcshell/xpcshell.ini
new file mode 100644
index 0000000000..b895aa8e07
--- /dev/null
+++ b/toolkit/components/antitracking/test/xpcshell/xpcshell.ini
@@ -0,0 +1,51 @@
+[DEFAULT]
+head = head.js ../../../../components/url-classifier/tests/unit/head_urlclassifier.js
+prefs =
+ dom.security.https_first=false #Disable https-first because of explicit http/https testing
+
+[test_cookie_behavior.js]
+[test_getPartitionKeyFromURL.js]
+skip-if =
+ socketprocess_networking # Bug 1759035
+[test_purge_trackers.js]
+skip-if =
+ win10_2004 # Bug 1718292
+ win10_2009 # Bug 1718292
+ win11_2009 # Bug 1797751
+ os == "win" && os_version == "6.1" # Skip on Azure - frequent failure
+run-sequentially = very high failure rate in parallel
+
+[test_purge_trackers_telemetry.js]
+[test_tracking_db_service.js]
+skip-if = toolkit == "android" # Bug 1697936
+[test_rejectForeignAllowList.js]
+skip-if =
+ socketprocess_networking # Bug 1759035
+[test_staticPartition_clientAuthRemember.js]
+[test_staticPartition_font.js]
+support-files =
+ data/font.woff
+skip-if =
+ os == "linux" && !debug # Bug 1760086
+ apple_silicon # bug 1729551
+ os == "mac" && bits == 64 && !debug # Bug 1652119
+ os == "win" && bits == 64 && !debug # Bug 1652119
+ os == "win" && os_version == "6.1" # Skip on Azure - frequent failure
+ socketprocess_networking # Bug 1759035
+run-sequentially = very high failure rate in parallel
+[test_staticPartition_image.js]
+skip-if =
+ socketprocess_networking # Bug 1759035
+[test_staticPartition_authhttp.js]
+skip-if =
+ socketprocess_networking # Bug 1759035
+[test_staticPartition_prefetch.js]
+skip-if =
+ socketprocess_networking # Bug 1759035
+[test_staticPartition_preload.js]
+skip-if =
+ socketprocess_networking # Bug 1759035
+[test_ExceptionListService.js]
+[test_view_source.js]
+skip-if =
+ socketprocess_networking # Bug 1759035 (not as common on win, perma on linux/osx)