Exempt app-bound domains from ITP's website data deletion and third-party cookie...
authorwilander@apple.com <wilander@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Wed, 6 May 2020 18:44:26 +0000 (18:44 +0000)
committerwilander@apple.com <wilander@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Wed, 6 May 2020 18:44:26 +0000 (18:44 +0000)
https://bugs.webkit.org/show_bug.cgi?id=210674
<rdar://problem/61950767>

Reviewed by Chris Dumez.

Source/WebCore:

This change adds functionality to NetworkStorageSession to allow it to exempt
app-bound domains from third-party cookie blocking.

Tests: http/tests/resourceLoadStatistics/exemptDomains/app-bound-domains-exempt-from-cookie-blocking-between-each-other.html
       http/tests/resourceLoadStatistics/exemptDomains/app-bound-domains-exempt-from-website-data-deletion-database.html
       http/tests/resourceLoadStatistics/exemptDomains/app-bound-domains-exempt-from-website-data-deletion.html

* platform/network/NetworkStorageSession.cpp:
(WebCore::NetworkStorageSession::shouldBlockCookies const):
(WebCore::NetworkStorageSession::shouldExemptDomainPairFromThirdPartyCookieBlocking const):
(WebCore::NetworkStorageSession::setAppBoundDomains):
(WebCore::NetworkStorageSession::resetAppBoundDomains):
* platform/network/NetworkStorageSession.h:

Source/WebKit:

This change forwards information about app-bound domains to ITP and web
processes so that they can be exempt from website data deletion and
third-party cookie blocking between themselves.

App-bound domains are configured statically and apply to all website
data stores. Therefore the setting needs to be forwarded to all
website data stores and ITP functionality in all network and web
content processes. This is done through the new static function
WebsiteDataStore::setAppBoundDomainsForITPIfInitialized().

Since app-bound domains are loaded lazily from disk and on a background
thread, this patch forwards them in ResourceLoadStatisticsParameters if
they've already been loaded. Then every time app-bound domains are
updated, they are forwarded to ITP. This ensures that ITP will have them
as soon as possible.

Setting app-bound domains for the purposes of ITP automatically switches
ITP's cookie blocking policy to the new
WebCore::ThirdPartyCookieBlockingMode::AllExceptBetweenAppBoundDomains.
This is done in WebResourceLoadStatisticsStore::setAppBoundDomains().

The C API changes are for test purposes.

* NetworkProcess/Classifier/ResourceLoadStatisticsDatabaseStore.cpp:
* NetworkProcess/Classifier/ResourceLoadStatisticsMemoryStore.cpp:
(WebKit::ResourceLoadStatisticsMemoryStore::registrableDomainsToDeleteOrRestrictWebsiteDataFor):
* NetworkProcess/Classifier/ResourceLoadStatisticsStore.cpp:
(WebKit::ResourceLoadStatisticsStore::setAppBoundDomains):
(WebKit::ResourceLoadStatisticsStore::resetParametersToDefaultValues):
(WebKit::ResourceLoadStatisticsStore::shouldExemptFromWebsiteDataDeletion const):
* NetworkProcess/Classifier/ResourceLoadStatisticsStore.h:
(WebKit::ResourceLoadStatisticsStore::standaloneApplicationDomain const): Deleted.
* NetworkProcess/Classifier/WebResourceLoadStatisticsStore.cpp:
(WebKit::WebResourceLoadStatisticsStore::setAppBoundDomains):
(WebKit::WebResourceLoadStatisticsStore::resetParametersToDefaultValues):
* NetworkProcess/Classifier/WebResourceLoadStatisticsStore.h:
* NetworkProcess/NetworkProcess.cpp:
(WebKit::NetworkProcess::setAppBoundDomainsForResourceLoadStatistics):
(WebKit::NetworkProcess::setThirdPartyCookieBlockingMode):
(WebKit::NetworkProcess::setShouldBlockThirdPartyCookiesForTesting): Deleted.
    Renamed setThirdPartyCookieBlockingMode.
* NetworkProcess/NetworkProcess.h:
* NetworkProcess/NetworkProcess.messages.in:
* NetworkProcess/cocoa/NetworkSessionCocoa.mm:
(WebKit::NetworkSessionCocoa::NetworkSessionCocoa):
* Shared/ResourceLoadStatisticsParameters.h:
(WebKit::ResourceLoadStatisticsParameters::encode const):
(WebKit::ResourceLoadStatisticsParameters::decode):
* UIProcess/API/C/WKWebsiteDataStoreRef.cpp:
(WKWebsiteDataStoreSetAppBoundDomainsForTesting):
* UIProcess/API/C/WKWebsiteDataStoreRef.h:
* UIProcess/Network/NetworkProcessProxy.cpp:
(WebKit::NetworkProcessProxy::setAppBoundDomainsForResourceLoadStatistics):
(WebKit::NetworkProcessProxy::setThirdPartyCookieBlockingMode):
(WebKit::NetworkProcessProxy::setShouldBlockThirdPartyCookiesForTesting): Deleted.
    Renamed setThirdPartyCookieBlockingMode.
* UIProcess/Network/NetworkProcessProxy.h:
* UIProcess/WebProcessPool.cpp:
(WebKit::WebProcessPool::ensureNetworkProcess):
* UIProcess/WebProcessProxy.cpp:
(WebKit::WebProcessProxy::setThirdPartyCookieBlockingMode):
(WebKit::WebProcessProxy::setShouldBlockThirdPartyCookiesForTesting): Deleted.
    Renamed setThirdPartyCookieBlockingMode.
* UIProcess/WebProcessProxy.h:
* UIProcess/WebsiteData/Cocoa/WebsiteDataStoreCocoa.mm:
(WebKit::WebsiteDataStore::initializeAppBoundDomains):
(WebKit::WebsiteDataStore::ensureAppBoundDomains const):
(WebKit::WebsiteDataStore::appBoundDomainsIfInitialized):
    This function allows fetching of app-bound domains without triggering
    the lazy loading. This is just to allow speculative configuration of ITP
    right when it's created — if any app-bound domains are already configured,
    forward them to ITP via ResourceLoadStatisticsParameters.
(WebKit::WebsiteDataStore::setAppBoundDomainsForTesting):
    This function is Cocoa-specific and only accepts localhost and 127.0.0.1
    to be configured as app-bound domains.
* UIProcess/WebsiteData/WebsiteDataStore.cpp:
(WebKit::WebsiteDataStore::setResourceLoadStatisticsShouldBlockThirdPartyCookiesForTesting):
(WebKit::WebsiteDataStore::setThirdPartyCookieBlockingMode):
(WebKit::WebsiteDataStore::parameters):
(WebKit::WebsiteDataStore::forwardAppBoundDomainsToITPIfInitialized):
(WebKit::WebsiteDataStore::setAppBoundDomainsForITP):
* UIProcess/WebsiteData/WebsiteDataStore.h:
* WebProcess/WebProcess.cpp:
(WebKit::WebProcess::setThirdPartyCookieBlockingMode):
(WebKit::WebProcess::setShouldBlockThirdPartyCookiesForTesting): Deleted.
* WebProcess/WebProcess.h:
* WebProcess/WebProcess.messages.in:

Tools:

This change adds a new TestRunner function
setAppBoundDomain() which takes an array of origin
strings and sets them to app-bound domains.

* WebKitTestRunner/InjectedBundle/Bindings/TestRunner.idl:
* WebKitTestRunner/InjectedBundle/InjectedBundle.cpp:
(WTR::InjectedBundle::didReceiveMessageToPage):
* WebKitTestRunner/InjectedBundle/TestRunner.cpp:
(WTR::TestRunner::setAppBoundDomains):
(WTR::TestRunner::didSetAppBoundDomainsCallback):
* WebKitTestRunner/InjectedBundle/TestRunner.h:
* WebKitTestRunner/TestController.cpp:
(WTR::AppBoundDomainsCallbackContext::AppBoundDomainsCallbackContext):
(WTR::didSetAppBoundDomainsCallback):
(WTR::TestController::setAppBoundDomains):
* WebKitTestRunner/TestController.h:
* WebKitTestRunner/TestInvocation.cpp:
(WTR::TestInvocation::didReceiveMessageFromInjectedBundle):
(WTR::TestInvocation::didSetAppBoundDomains):
* WebKitTestRunner/TestInvocation.h:

LayoutTests:

* http/tests/resourceLoadStatistics/exemptDomains/app-bound-domains-exempt-from-cookie-blocking-between-each-other-expected.txt: Added.
* http/tests/resourceLoadStatistics/exemptDomains/app-bound-domains-exempt-from-cookie-blocking-between-each-other.html: Added.
* http/tests/resourceLoadStatistics/exemptDomains/app-bound-domains-exempt-from-website-data-deletion-database-expected.txt: Added.
* http/tests/resourceLoadStatistics/exemptDomains/app-bound-domains-exempt-from-website-data-deletion-database.html: Added.
* http/tests/resourceLoadStatistics/exemptDomains/app-bound-domains-exempt-from-website-data-deletion-expected.txt: Added.
* http/tests/resourceLoadStatistics/exemptDomains/app-bound-domains-exempt-from-website-data-deletion.html: Added.

git-svn-id: https://svn.webkit.org/repository/webkit/trunk@261242 268f45cc-cd09-0410-ab3c-d52691b4dbfc

44 files changed:
LayoutTests/ChangeLog
LayoutTests/http/tests/resourceLoadStatistics/exemptDomains/app-bound-domains-exempt-from-cookie-blocking-between-each-other-expected.txt [new file with mode: 0644]
LayoutTests/http/tests/resourceLoadStatistics/exemptDomains/app-bound-domains-exempt-from-cookie-blocking-between-each-other.html [new file with mode: 0644]
LayoutTests/http/tests/resourceLoadStatistics/exemptDomains/app-bound-domains-exempt-from-website-data-deletion-database-expected.txt [new file with mode: 0644]
LayoutTests/http/tests/resourceLoadStatistics/exemptDomains/app-bound-domains-exempt-from-website-data-deletion-database.html [new file with mode: 0644]
LayoutTests/http/tests/resourceLoadStatistics/exemptDomains/app-bound-domains-exempt-from-website-data-deletion-expected.txt [new file with mode: 0644]
LayoutTests/http/tests/resourceLoadStatistics/exemptDomains/app-bound-domains-exempt-from-website-data-deletion.html [new file with mode: 0644]
Source/WebCore/ChangeLog
Source/WebCore/platform/network/NetworkStorageSession.cpp
Source/WebCore/platform/network/NetworkStorageSession.h
Source/WebKit/ChangeLog
Source/WebKit/NetworkProcess/Classifier/ResourceLoadStatisticsDatabaseStore.cpp
Source/WebKit/NetworkProcess/Classifier/ResourceLoadStatisticsMemoryStore.cpp
Source/WebKit/NetworkProcess/Classifier/ResourceLoadStatisticsStore.cpp
Source/WebKit/NetworkProcess/Classifier/ResourceLoadStatisticsStore.h
Source/WebKit/NetworkProcess/Classifier/WebResourceLoadStatisticsStore.cpp
Source/WebKit/NetworkProcess/Classifier/WebResourceLoadStatisticsStore.h
Source/WebKit/NetworkProcess/NetworkProcess.cpp
Source/WebKit/NetworkProcess/NetworkProcess.h
Source/WebKit/NetworkProcess/NetworkProcess.messages.in
Source/WebKit/NetworkProcess/cocoa/NetworkSessionCocoa.mm
Source/WebKit/Shared/ResourceLoadStatisticsParameters.h
Source/WebKit/UIProcess/API/C/WKWebsiteDataStoreRef.cpp
Source/WebKit/UIProcess/API/C/WKWebsiteDataStoreRef.h
Source/WebKit/UIProcess/Network/NetworkProcessProxy.cpp
Source/WebKit/UIProcess/Network/NetworkProcessProxy.h
Source/WebKit/UIProcess/WebProcessPool.cpp
Source/WebKit/UIProcess/WebProcessProxy.cpp
Source/WebKit/UIProcess/WebProcessProxy.h
Source/WebKit/UIProcess/WebsiteData/Cocoa/WebsiteDataStoreCocoa.mm
Source/WebKit/UIProcess/WebsiteData/WebsiteDataStore.cpp
Source/WebKit/UIProcess/WebsiteData/WebsiteDataStore.h
Source/WebKit/WebProcess/WebProcess.cpp
Source/WebKit/WebProcess/WebProcess.h
Source/WebKit/WebProcess/WebProcess.messages.in
Tools/ChangeLog
Tools/WebKitTestRunner/InjectedBundle/Bindings/TestRunner.idl
Tools/WebKitTestRunner/InjectedBundle/InjectedBundle.cpp
Tools/WebKitTestRunner/InjectedBundle/TestRunner.cpp
Tools/WebKitTestRunner/InjectedBundle/TestRunner.h
Tools/WebKitTestRunner/TestController.cpp
Tools/WebKitTestRunner/TestController.h
Tools/WebKitTestRunner/TestInvocation.cpp
Tools/WebKitTestRunner/TestInvocation.h

index a90f6d9..5e7d13e 100644 (file)
@@ -1,3 +1,18 @@
+2020-05-06  John Wilander  <wilander@apple.com>
+
+        Exempt app-bound domains from ITP's website data deletion and third-party cookie blocking between themselves
+        https://bugs.webkit.org/show_bug.cgi?id=210674
+        <rdar://problem/61950767>
+
+        Reviewed by Chris Dumez.
+
+        * http/tests/resourceLoadStatistics/exemptDomains/app-bound-domains-exempt-from-cookie-blocking-between-each-other-expected.txt: Added.
+        * http/tests/resourceLoadStatistics/exemptDomains/app-bound-domains-exempt-from-cookie-blocking-between-each-other.html: Added.
+        * http/tests/resourceLoadStatistics/exemptDomains/app-bound-domains-exempt-from-website-data-deletion-database-expected.txt: Added.
+        * http/tests/resourceLoadStatistics/exemptDomains/app-bound-domains-exempt-from-website-data-deletion-database.html: Added.
+        * http/tests/resourceLoadStatistics/exemptDomains/app-bound-domains-exempt-from-website-data-deletion-expected.txt: Added.
+        * http/tests/resourceLoadStatistics/exemptDomains/app-bound-domains-exempt-from-website-data-deletion.html: Added.
+
 2020-05-06  Daniel Bates  <dabates@apple.com>
 
         [iOS] ASSERTION FAILED: !(_keyboardFlags & WebEventKeyboardInputModifierFlagsChanged) in -[WebEvent charactersIgnoringModifiers] when pressing modifier on PDF
diff --git a/LayoutTests/http/tests/resourceLoadStatistics/exemptDomains/app-bound-domains-exempt-from-cookie-blocking-between-each-other-expected.txt b/LayoutTests/http/tests/resourceLoadStatistics/exemptDomains/app-bound-domains-exempt-from-cookie-blocking-between-each-other-expected.txt
new file mode 100644 (file)
index 0000000..62b2310
--- /dev/null
@@ -0,0 +1,33 @@
+Tests that app-bound domains are exempt from third-party cookie blocking between themselves.
+
+On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
+
+
+PASS successfullyParsed is true
+
+TEST COMPLETE
+
+
+--------
+Frame: '<!--frame1-->'
+--------
+Should not receive cookie.
+Did not receive cookie named 'firstPartyCookie'.
+Did not receive cookie named 'partitionedCookie'.
+Client-side document.cookie:
+
+--------
+Frame: '<!--frame2-->'
+--------
+Should not receive cookie.
+Did not receive cookie named 'firstPartyCookie'.
+Did not receive cookie named 'partitionedCookie'.
+Client-side document.cookie:
+
+--------
+Frame: '<!--frame3-->'
+--------
+Should receive cookie.
+Received cookie named 'firstPartyCookie'.
+Did not receive cookie named 'partitionedCookie'.
+Client-side document.cookie: firstPartyCookie=value
diff --git a/LayoutTests/http/tests/resourceLoadStatistics/exemptDomains/app-bound-domains-exempt-from-cookie-blocking-between-each-other.html b/LayoutTests/http/tests/resourceLoadStatistics/exemptDomains/app-bound-domains-exempt-from-cookie-blocking-between-each-other.html
new file mode 100644 (file)
index 0000000..cb585ff
--- /dev/null
@@ -0,0 +1,83 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <script src="/js-test-resources/js-test.js"></script>
+    <script src="../resources/util.js"></script>
+</head>
+<body>
+<script>
+    description("Tests that app-bound domains are exempt from third-party cookie blocking between themselves.");
+    jsTestIsAsync = true;
+
+    const firstPartyOrigin = "http://127.0.0.1:8000";
+    const thirdPartyOrigin = "http://localhost:8000";
+    const thirdPartyResourceUrl = thirdPartyOrigin + "/resourceLoadStatistics/resources";
+    const firstPartyCookieName = "firstPartyCookie";
+    const subPathToSetFirstPartyCookie = "/set-cookie.php?name=" + firstPartyCookieName + "&value=value";
+    const partitionedCookieName = "partitionedCookie";
+    const subPathToSetPartitionedCookie = "/set-cookie.php?name=" + partitionedCookieName + "&value=value";
+    const returnUrl = firstPartyOrigin + "/resourceLoadStatistics/exemptDomains/app-bound-domains-exempt-from-cookie-blocking-between-each-other.html";
+    const subPathToGetCookies = "/get-cookies.php?name1=" + firstPartyCookieName + "&name2=" + partitionedCookieName;
+    const redirectChainUrl = thirdPartyResourceUrl + "/redirect.php?redirectTo=" + thirdPartyResourceUrl + subPathToGetCookies;
+
+    function openIframe(url, onLoadHandler) {
+        const element = document.createElement("iframe");
+        element.src = url;
+        if (onLoadHandler) {
+            element.onload = onLoadHandler;
+        }
+        document.body.appendChild(element);
+    }
+
+    function runTest() {
+        switch (document.location.hash) {
+            case "#step1":
+                // Set first-party cookie for localhost.
+                document.location.href = thirdPartyResourceUrl + subPathToSetFirstPartyCookie + "#" + returnUrl + "#step2";
+                break;
+            case "#step2":
+                // Check that the cookie doesn't get sent for localhost under 127.0.0.1 since only localhost is configured as an app-bound domain.
+                document.location.hash = "step3";
+                testRunner.setAppBoundDomains([ thirdPartyOrigin ], function() {
+                    openIframe(thirdPartyResourceUrl + subPathToGetCookies + "&message=Should not receive cookie.", function() {
+                        runTest();
+                    });
+                });
+                break;
+            case "#step3":
+                // Check that the cookie doesn't get sent for localhost under 127.0.0.1 since only 127.0.0.1 is configured as an app-bound domain.
+                document.location.hash = "step4";
+                testRunner.setAppBoundDomains([ firstPartyOrigin ], function() {
+                    openIframe(thirdPartyResourceUrl + subPathToGetCookies + "&message=Should not receive cookie.", function() {
+                        runTest();
+                    });
+                });
+                break;
+            case "#step4":
+                // Check that the cookie gets sent for localhost under 127.0.0.1 since they are now both configured as app-bound domains.
+                document.location.hash = "step5";
+                testRunner.setAppBoundDomains([ firstPartyOrigin, thirdPartyOrigin ], function() {
+                    openIframe(thirdPartyResourceUrl + subPathToGetCookies + "&message=Should receive cookie.", runTest);
+                });
+                break;
+            case "#step5":
+                testRunner.setStatisticsShouldBlockThirdPartyCookies(false, function() {
+                    setEnableFeature(false, finishJSTest);
+                });
+                break;
+        }
+    }
+
+    if (document.location.hash === "") {
+        setEnableFeature(true, function () {
+            testRunner.dumpChildFramesAsText();
+            document.location.hash = "step1";
+            runTest();
+        });
+    } else {
+        runTest();
+    }
+</script>
+</body>
+</html>
diff --git a/LayoutTests/http/tests/resourceLoadStatistics/exemptDomains/app-bound-domains-exempt-from-website-data-deletion-database-expected.txt b/LayoutTests/http/tests/resourceLoadStatistics/exemptDomains/app-bound-domains-exempt-from-website-data-deletion-database-expected.txt
new file mode 100644 (file)
index 0000000..88e45d7
--- /dev/null
@@ -0,0 +1,15 @@
+Check that website data does not get removed after a period of no user interaction if the website is app-bound.
+
+Before deletion: Client-side cookie exists.
+Before deletion: HttpOnly cookie exists.
+Before deletion: Regular server-side cookie exists.
+Before deletion: LocalStorage entry does exist.
+Before deletion: IDB entry does exist.
+
+After deletion: HttpOnly cookie exists.
+After deletion: Client-side cookie exists.
+After deletion: Regular server-side cookie exists.
+After deletion: LocalStorage entry does exist.
+After deletion: IDB entry does exist.
+
+
diff --git a/LayoutTests/http/tests/resourceLoadStatistics/exemptDomains/app-bound-domains-exempt-from-website-data-deletion-database.html b/LayoutTests/http/tests/resourceLoadStatistics/exemptDomains/app-bound-domains-exempt-from-website-data-deletion-database.html
new file mode 100644 (file)
index 0000000..e4e671b
--- /dev/null
@@ -0,0 +1,256 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <script src="/cookies/resources/cookie-utilities.js"></script>
+    <script src="../resources/util.js"></script>
+</head>
+<body onload="setTimeout('runTest()', 0)">
+<div id="description">Check that website data does not get removed after a period of no user interaction if the website is app-bound.</div>
+<br>
+<div id="output"></div>
+<br>
+<script>
+    testRunner.waitUntilDone();
+    testRunner.dumpAsText();
+    testRunner.setUseITPDatabase(true);
+
+    const httpOnlyCookieName = "http-only-cookie";
+    const serverSideCookieName = "server-side-cookie";
+    const clientSideCookieName = "client-side-cookie";
+
+    function sortStringArray(a, b) {
+        a = a.toLowerCase();
+        b = b.toLowerCase();
+
+        return a > b ? 1 : b > a ? -1 : 0;
+    }
+
+    function addLinebreakToOutput() {
+        let element = document.createElement("br");
+        output.appendChild(element);
+    }
+
+    function addOutput(message) {
+        let element = document.createElement("div");
+        element.innerText = message;
+        output.appendChild(element);
+    }
+
+    function checkCookies(isAfterDeletion) {
+        let unsortedTestPassedMessages = [];
+        let cookies = internals.getCookies();
+        if (!cookies.length)
+            addOutput((isAfterDeletion ? "After" : "Before") + " script-accessible deletion: No cookies found.");
+        for (let cookie of cookies) {
+            switch (cookie.name) {
+                case httpOnlyCookieName:
+                    unsortedTestPassedMessages.push((isAfterDeletion ? "After" : "Before") + " deletion: " + (isAfterDeletion ? " " : "") + "HttpOnly cookie exists.");
+                    break;
+                case serverSideCookieName:
+                    unsortedTestPassedMessages.push((isAfterDeletion ? "After" : "Before") + " deletion: Regular server-side cookie exists.");
+                    break;
+                case clientSideCookieName:
+                    unsortedTestPassedMessages.push((isAfterDeletion ? "After" : "Before") + " deletion: Client-side cookie exists.");
+                    break;
+            }
+        }
+        let sortedTestPassedMessages = unsortedTestPassedMessages.sort(sortStringArray);
+        for (let testPassedMessage of sortedTestPassedMessages) {
+            addOutput(testPassedMessage);
+        }
+    }
+
+    const dbName = "TestDatabase";
+
+    function createIDBDataStore(callback) {
+        let request = indexedDB.open(dbName);
+        request.onerror = function() {
+            addOutput("Couldn't create indexedDB.");
+            finishTest();
+        };
+        request.onupgradeneeded = function(event) {
+            let db = event.target.result;
+            let objStore = db.createObjectStore("test", {autoIncrement: true});
+            objStore.add("value");
+            callback();
+        }
+    }
+
+    const maxIntervals = 20;
+
+    let intervalCounterIDB;
+    let checkIDBCallback;
+    let checkIDBIntervalID;
+    let semaphoreIDBCheck = false;
+    function checkIDBDataStoreExists(isAfterDeletion, callback) {
+        let request;
+        intervalCounterIDB = 0;
+        checkIDBCallback = callback;
+        if (!isAfterDeletion) {
+            // Check until there is a IDB.
+            checkIDBIntervalID = setInterval(function() {
+                if (semaphoreIDBCheck)
+                    return;
+                semaphoreIDBCheck = true;
+
+                if (++intervalCounterIDB >= maxIntervals) {
+                    clearInterval(checkIDBIntervalID);
+                    addOutput("Before deletion: IDB entry does not exist.");
+                    semaphoreIDBCheck = false;
+                    checkIDBCallback();
+                } else {
+                    request = indexedDB.open(dbName);
+                    request.onerror = function () {
+                        clearInterval(checkIDBIntervalID);
+                        addOutput("Couldn't open indexedDB.");
+                        semaphoreIDBCheck = false;
+                        finishTest();
+                    };
+                    request.onupgradeneeded = function () {
+                        // Let the next interval check again.
+                        semaphoreIDBCheck = false;
+                    };
+                    request.onsuccess = function () {
+                        clearInterval(checkIDBIntervalID);
+                        addOutput("Before deletion: IDB entry does exist.");
+                        semaphoreIDBCheck = false;
+                        checkIDBCallback();
+                    };
+                }
+            }, 200);
+        } else {
+            // Check until there is a IDB.
+            checkIDBIntervalID = setInterval(function () {
+                if (semaphoreIDBCheck)
+                    return;
+                semaphoreIDBCheck = true;
+
+                if (++intervalCounterIDB >= maxIntervals) {
+                    clearInterval(checkIDBIntervalID);
+                    addOutput("After deletion: IDB entry checks exhausted.");
+                    semaphoreIDBCheck = false;
+                    checkIDBCallback();
+                } else {
+                    request = indexedDB.open(dbName);
+                    request.onerror = function () {
+                        clearInterval(checkIDBIntervalID);
+                        addOutput("Couldn't open indexedDB.");
+                        semaphoreIDBCheck = false;
+                        finishTest();
+                    };
+                    request.onupgradeneeded = function () {
+                        // Let the next interval check again.
+                        semaphoreIDBCheck = false;
+                    };
+                    request.onsuccess = function () {
+                        clearInterval(checkIDBIntervalID);
+                        addOutput("After deletion: IDB entry does exist.");
+                        semaphoreIDBCheck = false;
+                        checkIDBCallback();
+                    };
+                }
+            }, 200);
+        }
+    }
+
+    let intervalCounterLocalStorage;
+    let checkLocalStorageCallback;
+    let checkLocalStorageIntervalID;
+    const localStorageName = "test";
+    const localStorageValue = "value";
+    function checkLocalStorageExists(isAfterDeletion, callback) {
+        intervalCounterLocalStorage = 0;
+        checkLocalStorageCallback = callback;
+        if (!isAfterDeletion) {
+            // Check until there is LocalStorage.
+            checkLocalStorageIntervalID = setInterval(function() {
+                if (++intervalCounterLocalStorage >= maxIntervals) {
+                    clearInterval(checkLocalStorageIntervalID);
+                    let value = localStorage.getItem(localStorageName);
+                    addOutput("Before deletion: LocalStorage entry " + (value === localStorageValue ? "does" : "does not") + " exist.");
+                    checkLocalStorageCallback();
+                } else if (testRunner.isStatisticsHasLocalStorage(originUnderTest)) {
+                    clearInterval(checkLocalStorageIntervalID);
+                    let value = localStorage.getItem(localStorageName);
+                    addOutput("Before deletion: LocalStorage entry " + (value === localStorageValue ? "does" : "does not") + " exist.");
+                    checkLocalStorageCallback();
+                }
+            }, 100);
+        } else {
+            // Check until there is no LocalStorage.
+            checkLocalStorageIntervalID = setInterval(function() {
+                if (++intervalCounterLocalStorage >= maxIntervals) {
+                    clearInterval(checkLocalStorageIntervalID);
+                    let value = localStorage.getItem(localStorageName);
+                    addOutput("After deletion: LocalStorage entry " + (value === localStorageValue ? "does" : "does not") + " exist.");
+                    checkLocalStorageCallback();
+                } else if (!testRunner.isStatisticsHasLocalStorage(originUnderTest)) {
+                    clearInterval(checkLocalStorageIntervalID);
+                    let value = localStorage.getItem(localStorageName);
+                    addOutput("After deletion: LocalStorage entry " + (value === localStorageValue ? "does" : "does not") + " exist.");
+                    checkLocalStorageCallback();
+                }
+            }, 100);
+        }
+    }
+
+    async function writeWebsiteDataAndContinue() {
+        // Write cookies.
+        await fetch("/cookies/resources/set-http-only-cookie.php?cookieName=" + httpOnlyCookieName, { credentials: "same-origin" });
+        await fetch("/cookies/resources/setCookies.cgi", { headers: { "Set-Cookie": serverSideCookieName + "=1; path=/;" }, credentials: "same-origin" });
+        document.cookie = clientSideCookieName + "=1";
+
+        checkCookies(false);
+
+        // Write LocalStorage
+        localStorage.setItem(localStorageName, localStorageValue);
+        checkLocalStorageExists(false, function() {
+
+            // Write IndexedDB.
+            createIDBDataStore(function () {
+                checkIDBDataStoreExists(false, function() {
+                    addLinebreakToOutput();
+                    processWebsiteDataAndContinue();
+                });
+            });
+        });
+    }
+
+    function processWebsiteDataAndContinue() {
+        testRunner.installStatisticsDidScanDataRecordsCallback(checkWebsiteDataAndContinue);
+        testRunner.statisticsProcessStatisticsAndDataRecords();
+    }
+
+    function checkWebsiteDataAndContinue() {
+        checkCookies(true);
+        checkLocalStorageExists(true, function () {
+            checkIDBDataStoreExists(true, finishTest);
+        });
+    }
+
+    function finishTest() {
+        resetCookies();
+        testRunner.setStatisticsFirstPartyWebsiteDataRemovalMode(false, function() {
+            setEnableFeature(false, function() {
+                testRunner.notifyDone();
+            });
+        });
+    }
+
+    const originUnderTest  = "http://127.0.0.1:8000";
+    function runTest() {
+        setEnableFeature(true, function () {
+            testRunner.setAppBoundDomains([ originUnderTest ], function() {
+                testRunner.setStatisticsFirstPartyWebsiteDataRemovalMode(true, function() {
+                    testRunner.setStatisticsPrevalentResource(originUnderTest, true, function() {
+                        if (!testRunner.isStatisticsPrevalentResource(originUnderTest))
+                            addOutput("FAIL: " + originUnderTest + " didn't get classified as prevalent.");
+                        writeWebsiteDataAndContinue();
+                    });
+                });
+            });
+        });
+    }
+</script>
+</body>
+</html>
diff --git a/LayoutTests/http/tests/resourceLoadStatistics/exemptDomains/app-bound-domains-exempt-from-website-data-deletion-expected.txt b/LayoutTests/http/tests/resourceLoadStatistics/exemptDomains/app-bound-domains-exempt-from-website-data-deletion-expected.txt
new file mode 100644 (file)
index 0000000..88e45d7
--- /dev/null
@@ -0,0 +1,15 @@
+Check that website data does not get removed after a period of no user interaction if the website is app-bound.
+
+Before deletion: Client-side cookie exists.
+Before deletion: HttpOnly cookie exists.
+Before deletion: Regular server-side cookie exists.
+Before deletion: LocalStorage entry does exist.
+Before deletion: IDB entry does exist.
+
+After deletion: HttpOnly cookie exists.
+After deletion: Client-side cookie exists.
+After deletion: Regular server-side cookie exists.
+After deletion: LocalStorage entry does exist.
+After deletion: IDB entry does exist.
+
+
diff --git a/LayoutTests/http/tests/resourceLoadStatistics/exemptDomains/app-bound-domains-exempt-from-website-data-deletion.html b/LayoutTests/http/tests/resourceLoadStatistics/exemptDomains/app-bound-domains-exempt-from-website-data-deletion.html
new file mode 100644 (file)
index 0000000..a904795
--- /dev/null
@@ -0,0 +1,255 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <script src="/cookies/resources/cookie-utilities.js"></script>
+    <script src="../resources/util.js"></script>
+</head>
+<body onload="setTimeout('runTest()', 0)">
+<div id="description">Check that website data does not get removed after a period of no user interaction if the website is app-bound.</div>
+<br>
+<div id="output"></div>
+<br>
+<script>
+    testRunner.waitUntilDone();
+    testRunner.dumpAsText();
+
+    const httpOnlyCookieName = "http-only-cookie";
+    const serverSideCookieName = "server-side-cookie";
+    const clientSideCookieName = "client-side-cookie";
+
+    function sortStringArray(a, b) {
+        a = a.toLowerCase();
+        b = b.toLowerCase();
+
+        return a > b ? 1 : b > a ? -1 : 0;
+    }
+
+    function addLinebreakToOutput() {
+        let element = document.createElement("br");
+        output.appendChild(element);
+    }
+
+    function addOutput(message) {
+        let element = document.createElement("div");
+        element.innerText = message;
+        output.appendChild(element);
+    }
+
+    function checkCookies(isAfterDeletion) {
+        let unsortedTestPassedMessages = [];
+        let cookies = internals.getCookies();
+        if (!cookies.length)
+            addOutput((isAfterDeletion ? "After" : "Before") + " script-accessible deletion: No cookies found.");
+        for (let cookie of cookies) {
+            switch (cookie.name) {
+                case httpOnlyCookieName:
+                    unsortedTestPassedMessages.push((isAfterDeletion ? "After" : "Before") + " deletion: " + (isAfterDeletion ? " " : "") + "HttpOnly cookie exists.");
+                    break;
+                case serverSideCookieName:
+                    unsortedTestPassedMessages.push((isAfterDeletion ? "After" : "Before") + " deletion: Regular server-side cookie exists.");
+                    break;
+                case clientSideCookieName:
+                    unsortedTestPassedMessages.push((isAfterDeletion ? "After" : "Before") + " deletion: Client-side cookie exists.");
+                    break;
+            }
+        }
+        let sortedTestPassedMessages = unsortedTestPassedMessages.sort(sortStringArray);
+        for (let testPassedMessage of sortedTestPassedMessages) {
+            addOutput(testPassedMessage);
+        }
+    }
+
+    const dbName = "TestDatabase";
+
+    function createIDBDataStore(callback) {
+        let request = indexedDB.open(dbName);
+        request.onerror = function() {
+            addOutput("Couldn't create indexedDB.");
+            finishTest();
+        };
+        request.onupgradeneeded = function(event) {
+            let db = event.target.result;
+            let objStore = db.createObjectStore("test", {autoIncrement: true});
+            objStore.add("value");
+            callback();
+        }
+    }
+
+    const maxIntervals = 20;
+
+    let intervalCounterIDB;
+    let checkIDBCallback;
+    let checkIDBIntervalID;
+    let semaphoreIDBCheck = false;
+    function checkIDBDataStoreExists(isAfterDeletion, callback) {
+        let request;
+        intervalCounterIDB = 0;
+        checkIDBCallback = callback;
+        if (!isAfterDeletion) {
+            // Check until there is a IDB.
+            checkIDBIntervalID = setInterval(function() {
+                if (semaphoreIDBCheck)
+                    return;
+                semaphoreIDBCheck = true;
+
+                if (++intervalCounterIDB >= maxIntervals) {
+                    clearInterval(checkIDBIntervalID);
+                    addOutput("Before deletion: IDB entry does not exist.");
+                    semaphoreIDBCheck = false;
+                    checkIDBCallback();
+                } else {
+                    request = indexedDB.open(dbName);
+                    request.onerror = function () {
+                        clearInterval(checkIDBIntervalID);
+                        addOutput("Couldn't open indexedDB.");
+                        semaphoreIDBCheck = false;
+                        finishTest();
+                    };
+                    request.onupgradeneeded = function () {
+                        // Let the next interval check again.
+                        semaphoreIDBCheck = false;
+                    };
+                    request.onsuccess = function () {
+                        clearInterval(checkIDBIntervalID);
+                        addOutput("Before deletion: IDB entry does exist.");
+                        semaphoreIDBCheck = false;
+                        checkIDBCallback();
+                    };
+                }
+            }, 200);
+        } else {
+            // Check until there is a IDB.
+            checkIDBIntervalID = setInterval(function () {
+                if (semaphoreIDBCheck)
+                    return;
+                semaphoreIDBCheck = true;
+
+                if (++intervalCounterIDB >= maxIntervals) {
+                    clearInterval(checkIDBIntervalID);
+                    addOutput("After deletion: IDB entry checks exhausted.");
+                    semaphoreIDBCheck = false;
+                    checkIDBCallback();
+                } else {
+                    request = indexedDB.open(dbName);
+                    request.onerror = function () {
+                        clearInterval(checkIDBIntervalID);
+                        addOutput("Couldn't open indexedDB.");
+                        semaphoreIDBCheck = false;
+                        finishTest();
+                    };
+                    request.onupgradeneeded = function () {
+                        // Let the next interval check again.
+                        semaphoreIDBCheck = false;
+                    };
+                    request.onsuccess = function () {
+                        clearInterval(checkIDBIntervalID);
+                        addOutput("After deletion: IDB entry does exist.");
+                        semaphoreIDBCheck = false;
+                        checkIDBCallback();
+                    };
+                }
+            }, 200);
+        }
+    }
+
+    let intervalCounterLocalStorage;
+    let checkLocalStorageCallback;
+    let checkLocalStorageIntervalID;
+    const localStorageName = "test";
+    const localStorageValue = "value";
+    function checkLocalStorageExists(isAfterDeletion, callback) {
+        intervalCounterLocalStorage = 0;
+        checkLocalStorageCallback = callback;
+        if (!isAfterDeletion) {
+            // Check until there is LocalStorage.
+            checkLocalStorageIntervalID = setInterval(function() {
+                if (++intervalCounterLocalStorage >= maxIntervals) {
+                    clearInterval(checkLocalStorageIntervalID);
+                    let value = localStorage.getItem(localStorageName);
+                    addOutput("Before deletion: LocalStorage entry " + (value === localStorageValue ? "does" : "does not") + " exist.");
+                    checkLocalStorageCallback();
+                } else if (testRunner.isStatisticsHasLocalStorage(originUnderTest)) {
+                    clearInterval(checkLocalStorageIntervalID);
+                    let value = localStorage.getItem(localStorageName);
+                    addOutput("Before deletion: LocalStorage entry " + (value === localStorageValue ? "does" : "does not") + " exist.");
+                    checkLocalStorageCallback();
+                }
+            }, 100);
+        } else {
+            // Check until there is no LocalStorage.
+            checkLocalStorageIntervalID = setInterval(function() {
+                if (++intervalCounterLocalStorage >= maxIntervals) {
+                    clearInterval(checkLocalStorageIntervalID);
+                    let value = localStorage.getItem(localStorageName);
+                    addOutput("After deletion: LocalStorage entry " + (value === localStorageValue ? "does" : "does not") + " exist.");
+                    checkLocalStorageCallback();
+                } else if (!testRunner.isStatisticsHasLocalStorage(originUnderTest)) {
+                    clearInterval(checkLocalStorageIntervalID);
+                    let value = localStorage.getItem(localStorageName);
+                    addOutput("After deletion: LocalStorage entry " + (value === localStorageValue ? "does" : "does not") + " exist.");
+                    checkLocalStorageCallback();
+                }
+            }, 100);
+        }
+    }
+
+    async function writeWebsiteDataAndContinue() {
+        // Write cookies.
+        await fetch("/cookies/resources/set-http-only-cookie.php?cookieName=" + httpOnlyCookieName, { credentials: "same-origin" });
+        await fetch("/cookies/resources/setCookies.cgi", { headers: { "Set-Cookie": serverSideCookieName + "=1; path=/;" }, credentials: "same-origin" });
+        document.cookie = clientSideCookieName + "=1";
+
+        checkCookies(false);
+
+        // Write LocalStorage
+        localStorage.setItem(localStorageName, localStorageValue);
+        checkLocalStorageExists(false, function() {
+
+            // Write IndexedDB.
+            createIDBDataStore(function () {
+                checkIDBDataStoreExists(false, function() {
+                    addLinebreakToOutput();
+                    processWebsiteDataAndContinue();
+                });
+            });
+        });
+    }
+
+    function processWebsiteDataAndContinue() {
+        testRunner.installStatisticsDidScanDataRecordsCallback(checkWebsiteDataAndContinue);
+        testRunner.statisticsProcessStatisticsAndDataRecords();
+    }
+
+    function checkWebsiteDataAndContinue() {
+        checkCookies(true);
+        checkLocalStorageExists(true, function () {
+            checkIDBDataStoreExists(true, finishTest);
+        });
+    }
+
+    function finishTest() {
+        resetCookies();
+        testRunner.setStatisticsFirstPartyWebsiteDataRemovalMode(false, function() {
+            setEnableFeature(false, function() {
+                testRunner.notifyDone();
+            });
+        });
+    }
+
+    const originUnderTest  = "http://127.0.0.1:8000";
+    function runTest() {
+        setEnableFeature(true, function () {
+            testRunner.setAppBoundDomains([ originUnderTest ], function() {
+                testRunner.setStatisticsFirstPartyWebsiteDataRemovalMode(true, function() {
+                    testRunner.setStatisticsPrevalentResource(originUnderTest, true, function() {
+                        if (!testRunner.isStatisticsPrevalentResource(originUnderTest))
+                            addOutput("FAIL: " + originUnderTest + " didn't get classified as prevalent.");
+                        writeWebsiteDataAndContinue();
+                    });
+                });
+            });
+        });
+    }
+</script>
+</body>
+</html>
index 9d85dbd..03f8f83 100644 (file)
@@ -1,3 +1,25 @@
+2020-05-06  John Wilander  <wilander@apple.com>
+
+        Exempt app-bound domains from ITP's website data deletion and third-party cookie blocking between themselves
+        https://bugs.webkit.org/show_bug.cgi?id=210674
+        <rdar://problem/61950767>
+
+        Reviewed by Chris Dumez.
+
+        This change adds functionality to NetworkStorageSession to allow it to exempt
+        app-bound domains from third-party cookie blocking.
+
+        Tests: http/tests/resourceLoadStatistics/exemptDomains/app-bound-domains-exempt-from-cookie-blocking-between-each-other.html
+               http/tests/resourceLoadStatistics/exemptDomains/app-bound-domains-exempt-from-website-data-deletion-database.html
+               http/tests/resourceLoadStatistics/exemptDomains/app-bound-domains-exempt-from-website-data-deletion.html
+
+        * platform/network/NetworkStorageSession.cpp:
+        (WebCore::NetworkStorageSession::shouldBlockCookies const):
+        (WebCore::NetworkStorageSession::shouldExemptDomainPairFromThirdPartyCookieBlocking const):
+        (WebCore::NetworkStorageSession::setAppBoundDomains):
+        (WebCore::NetworkStorageSession::resetAppBoundDomains):
+        * platform/network/NetworkStorageSession.h:
+
 2020-05-06  Antti Koivisto  <antti@apple.com>
 
         Add basic support for generating accurate wheel event listener region
index c58669f..49430af 100644 (file)
@@ -134,6 +134,8 @@ bool NetworkStorageSession::shouldBlockCookies(const URL& firstPartyForCookies,
     switch (m_thirdPartyCookieBlockingMode) {
     case ThirdPartyCookieBlockingMode::All:
         return true;
+    case ThirdPartyCookieBlockingMode::AllExceptBetweenAppBoundDomains:
+        return !shouldExemptDomainPairFromThirdPartyCookieBlocking(firstPartyDomain, resourceDomain);
     case ThirdPartyCookieBlockingMode::AllOnSitesWithoutUserInteraction:
         if (!hasHadUserInteractionAsFirstParty(firstPartyDomain))
             return true;
@@ -145,6 +147,15 @@ bool NetworkStorageSession::shouldBlockCookies(const URL& firstPartyForCookies,
     return false;
 }
 
+bool NetworkStorageSession::shouldExemptDomainPairFromThirdPartyCookieBlocking(const RegistrableDomain& topFrameDomain, const RegistrableDomain& resourceDomain) const
+{
+    ASSERT(topFrameDomain != resourceDomain);
+    if (topFrameDomain.isEmpty() || resourceDomain.isEmpty())
+        return false;
+
+    return topFrameDomain == resourceDomain || (m_appBoundDomains.contains(topFrameDomain) && m_appBoundDomains.contains(resourceDomain));
+}
+
 Optional<Seconds> NetworkStorageSession::maxAgeCacheCap(const ResourceRequest& request)
 {
     if (m_cacheMaxAgeCapForPrevalentResources && shouldBlockCookies(request, WTF::nullopt, WTF::nullopt))
@@ -292,6 +303,16 @@ void NetworkStorageSession::setThirdPartyCookieBlockingMode(ThirdPartyCookieBloc
     m_thirdPartyCookieBlockingMode = blockingMode;
 }
 
+void NetworkStorageSession::setAppBoundDomains(HashSet<RegistrableDomain>&& domains)
+{
+    m_appBoundDomains = WTFMove(domains);
+}
+
+void NetworkStorageSession::resetAppBoundDomains()
+{
+    m_appBoundDomains.clear();
+}
+
 Optional<Seconds> NetworkStorageSession::clientSideCookieCap(const RegistrableDomain& firstParty, Optional<PageIdentifier> pageID) const
 {
     if (!m_ageCapForClientSideCookies || !pageID || m_navigatedToWithLinkDecorationByPrevalentResource.isEmpty())
index 1391ded..19685f1 100644 (file)
@@ -77,7 +77,7 @@ struct SameSiteInfo;
 enum class HTTPCookieAcceptPolicy : uint8_t;
 enum class IncludeSecureCookies : bool;
 enum class IncludeHttpOnlyCookies : bool;
-enum class ThirdPartyCookieBlockingMode : uint8_t { All, AllOnSitesWithoutUserInteraction, OnlyAccordingToPerDomainPolicy };
+enum class ThirdPartyCookieBlockingMode : uint8_t { All, AllExceptBetweenAppBoundDomains, AllOnSitesWithoutUserInteraction, OnlyAccordingToPerDomainPolicy };
 enum class SameSiteStrictEnforcementEnabled : bool { Yes, No };
 enum class FirstPartyWebsiteDataRemovalMode : uint8_t { AllButCookies, None, AllButCookiesLiveOnTestingTimeout, AllButCookiesReproTestingTimeout };
 enum class ShouldAskITP : bool { No, Yes };
@@ -95,6 +95,9 @@ public:
 class NetworkStorageSession {
     WTF_MAKE_NONCOPYABLE(NetworkStorageSession); WTF_MAKE_FAST_ALLOCATED;
 public:
+    using TopFrameDomain = WebCore::RegistrableDomain;
+    using SubResourceDomain = WebCore::RegistrableDomain;
+
     WEBCORE_EXPORT static void permitProcessToUseCookieAPI(bool);
     WEBCORE_EXPORT static bool processMayUseCookieAPI();
 
@@ -196,6 +199,8 @@ public:
     WEBCORE_EXPORT void didCommitCrossSiteLoadWithDataTransferFromPrevalentResource(const RegistrableDomain& toDomain, PageIdentifier);
     WEBCORE_EXPORT void resetCrossSiteLoadsWithLinkDecorationForTesting();
     WEBCORE_EXPORT void setThirdPartyCookieBlockingMode(ThirdPartyCookieBlockingMode);
+    WEBCORE_EXPORT void setAppBoundDomains(HashSet<RegistrableDomain>&&);
+    WEBCORE_EXPORT void resetAppBoundDomains();
 #endif
 
 private:
@@ -241,7 +246,8 @@ private:
 
 #if ENABLE(RESOURCE_LOAD_STATISTICS)
     bool m_isResourceLoadStatisticsEnabled = false;
-    Optional<Seconds> clientSideCookieCap(const RegistrableDomain& firstParty, Optional<PageIdentifier>) const;
+    Optional<Seconds> clientSideCookieCap(const TopFrameDomain&, Optional<PageIdentifier>) const;
+    bool shouldExemptDomainPairFromThirdPartyCookieBlocking(const TopFrameDomain&, const SubResourceDomain&) const;
     HashSet<RegistrableDomain> m_registrableDomainsToBlockAndDeleteCookiesFor;
     HashSet<RegistrableDomain> m_registrableDomainsToBlockButKeepCookiesFor;
     HashSet<RegistrableDomain> m_registrableDomainsWithUserInteractionAsFirstParty;
@@ -253,6 +259,7 @@ private:
     HashMap<WebCore::PageIdentifier, RegistrableDomain> m_navigatedToWithLinkDecorationByPrevalentResource;
     bool m_navigationWithLinkDecorationTestMode = false;
     ThirdPartyCookieBlockingMode m_thirdPartyCookieBlockingMode { ThirdPartyCookieBlockingMode::All };
+    HashSet<RegistrableDomain> m_appBoundDomains;
 #endif
 
 #if PLATFORM(COCOA)
@@ -277,6 +284,7 @@ template<> struct EnumTraits<WebCore::ThirdPartyCookieBlockingMode> {
     using values = EnumValues<
         WebCore::ThirdPartyCookieBlockingMode,
         WebCore::ThirdPartyCookieBlockingMode::All,
+        WebCore::ThirdPartyCookieBlockingMode::AllExceptBetweenAppBoundDomains,
         WebCore::ThirdPartyCookieBlockingMode::AllOnSitesWithoutUserInteraction,
         WebCore::ThirdPartyCookieBlockingMode::OnlyAccordingToPerDomainPolicy
     >;
index c23f3cf..a410930 100644 (file)
@@ -1,3 +1,99 @@
+2020-05-06  John Wilander  <wilander@apple.com>
+
+        Exempt app-bound domains from ITP's website data deletion and third-party cookie blocking between themselves
+        https://bugs.webkit.org/show_bug.cgi?id=210674
+        <rdar://problem/61950767>
+
+        Reviewed by Chris Dumez.
+
+        This change forwards information about app-bound domains to ITP and web
+        processes so that they can be exempt from website data deletion and
+        third-party cookie blocking between themselves.
+
+        App-bound domains are configured statically and apply to all website
+        data stores. Therefore the setting needs to be forwarded to all
+        website data stores and ITP functionality in all network and web
+        content processes. This is done through the new static function
+        WebsiteDataStore::setAppBoundDomainsForITPIfInitialized().
+
+        Since app-bound domains are loaded lazily from disk and on a background
+        thread, this patch forwards them in ResourceLoadStatisticsParameters if
+        they've already been loaded. Then every time app-bound domains are
+        updated, they are forwarded to ITP. This ensures that ITP will have them
+        as soon as possible.
+
+        Setting app-bound domains for the purposes of ITP automatically switches
+        ITP's cookie blocking policy to the new
+        WebCore::ThirdPartyCookieBlockingMode::AllExceptBetweenAppBoundDomains.
+        This is done in WebResourceLoadStatisticsStore::setAppBoundDomains().
+
+        The C API changes are for test purposes.
+
+        * NetworkProcess/Classifier/ResourceLoadStatisticsDatabaseStore.cpp:
+        * NetworkProcess/Classifier/ResourceLoadStatisticsMemoryStore.cpp:
+        (WebKit::ResourceLoadStatisticsMemoryStore::registrableDomainsToDeleteOrRestrictWebsiteDataFor):
+        * NetworkProcess/Classifier/ResourceLoadStatisticsStore.cpp:
+        (WebKit::ResourceLoadStatisticsStore::setAppBoundDomains):
+        (WebKit::ResourceLoadStatisticsStore::resetParametersToDefaultValues):
+        (WebKit::ResourceLoadStatisticsStore::shouldExemptFromWebsiteDataDeletion const):
+        * NetworkProcess/Classifier/ResourceLoadStatisticsStore.h:
+        (WebKit::ResourceLoadStatisticsStore::standaloneApplicationDomain const): Deleted.
+        * NetworkProcess/Classifier/WebResourceLoadStatisticsStore.cpp:
+        (WebKit::WebResourceLoadStatisticsStore::setAppBoundDomains):
+        (WebKit::WebResourceLoadStatisticsStore::resetParametersToDefaultValues):
+        * NetworkProcess/Classifier/WebResourceLoadStatisticsStore.h:
+        * NetworkProcess/NetworkProcess.cpp:
+        (WebKit::NetworkProcess::setAppBoundDomainsForResourceLoadStatistics):
+        (WebKit::NetworkProcess::setThirdPartyCookieBlockingMode):
+        (WebKit::NetworkProcess::setShouldBlockThirdPartyCookiesForTesting): Deleted.
+            Renamed setThirdPartyCookieBlockingMode.
+        * NetworkProcess/NetworkProcess.h:
+        * NetworkProcess/NetworkProcess.messages.in:
+        * NetworkProcess/cocoa/NetworkSessionCocoa.mm:
+        (WebKit::NetworkSessionCocoa::NetworkSessionCocoa):
+        * Shared/ResourceLoadStatisticsParameters.h:
+        (WebKit::ResourceLoadStatisticsParameters::encode const):
+        (WebKit::ResourceLoadStatisticsParameters::decode):
+        * UIProcess/API/C/WKWebsiteDataStoreRef.cpp:
+        (WKWebsiteDataStoreSetAppBoundDomainsForTesting):
+        * UIProcess/API/C/WKWebsiteDataStoreRef.h:
+        * UIProcess/Network/NetworkProcessProxy.cpp:
+        (WebKit::NetworkProcessProxy::setAppBoundDomainsForResourceLoadStatistics):
+        (WebKit::NetworkProcessProxy::setThirdPartyCookieBlockingMode):
+        (WebKit::NetworkProcessProxy::setShouldBlockThirdPartyCookiesForTesting): Deleted.
+            Renamed setThirdPartyCookieBlockingMode.
+        * UIProcess/Network/NetworkProcessProxy.h:
+        * UIProcess/WebProcessPool.cpp:
+        (WebKit::WebProcessPool::ensureNetworkProcess):
+        * UIProcess/WebProcessProxy.cpp:
+        (WebKit::WebProcessProxy::setThirdPartyCookieBlockingMode):
+        (WebKit::WebProcessProxy::setShouldBlockThirdPartyCookiesForTesting): Deleted.
+            Renamed setThirdPartyCookieBlockingMode.
+        * UIProcess/WebProcessProxy.h:
+        * UIProcess/WebsiteData/Cocoa/WebsiteDataStoreCocoa.mm:
+        (WebKit::WebsiteDataStore::initializeAppBoundDomains):
+        (WebKit::WebsiteDataStore::ensureAppBoundDomains const):
+        (WebKit::WebsiteDataStore::appBoundDomainsIfInitialized):
+            This function allows fetching of app-bound domains without triggering
+            the lazy loading. This is just to allow speculative configuration of ITP
+            right when it's created — if any app-bound domains are already configured,
+            forward them to ITP via ResourceLoadStatisticsParameters.
+        (WebKit::WebsiteDataStore::setAppBoundDomainsForTesting):
+            This function is Cocoa-specific and only accepts localhost and 127.0.0.1
+            to be configured as app-bound domains.
+        * UIProcess/WebsiteData/WebsiteDataStore.cpp:
+        (WebKit::WebsiteDataStore::setResourceLoadStatisticsShouldBlockThirdPartyCookiesForTesting):
+        (WebKit::WebsiteDataStore::setThirdPartyCookieBlockingMode):
+        (WebKit::WebsiteDataStore::parameters):
+        (WebKit::WebsiteDataStore::forwardAppBoundDomainsToITPIfInitialized):
+        (WebKit::WebsiteDataStore::setAppBoundDomainsForITP):
+        * UIProcess/WebsiteData/WebsiteDataStore.h:
+        * WebProcess/WebProcess.cpp:
+        (WebKit::WebProcess::setThirdPartyCookieBlockingMode):
+        (WebKit::WebProcess::setShouldBlockThirdPartyCookiesForTesting): Deleted.
+        * WebProcess/WebProcess.h:
+        * WebProcess/WebProcess.messages.in:
+
 2020-05-06  Daniel Bates  <dabates@apple.com>
 
         [iOS] ASSERTION FAILED: !(_keyboardFlags & WebEventKeyboardInputModifierFlagsChanged) in -[WebEvent charactersIgnoringModifiers] when pressing modifier on PDF
index 1b7a7cf..31e9199 100644 (file)
@@ -2621,7 +2621,7 @@ RegistrableDomainsToDeleteOrRestrictWebsiteDataFor ResourceLoadStatisticsDatabas
     Vector<DomainData> domains = this->domains();
     Vector<unsigned> domainIDsToClearGrandfathering;
     for (auto& statistic : domains) {
-        if (statistic.registrableDomain == standaloneApplicationDomain())
+        if (shouldExemptFromWebsiteDataDeletion(statistic.registrableDomain))
             continue;
         oldestUserInteraction = std::min(oldestUserInteraction, statistic.mostRecentUserInteractionTime);
         if (shouldRemoveAllWebsiteDataFor(statistic, shouldCheckForGrandfathering)) {
index 6506797..af50041 100644 (file)
@@ -980,7 +980,7 @@ RegistrableDomainsToDeleteOrRestrictWebsiteDataFor ResourceLoadStatisticsMemoryS
     auto oldestUserInteraction = now;
     RegistrableDomainsToDeleteOrRestrictWebsiteDataFor toDeleteOrRestrictFor;
     for (auto& statistic : m_resourceStatisticsMap.values()) {
-        if (statistic.registrableDomain == standaloneApplicationDomain())
+        if (shouldExemptFromWebsiteDataDeletion(statistic.registrableDomain))
             continue;
         oldestUserInteraction = std::min(oldestUserInteraction, statistic.mostRecentUserInteractionTime);
         if (shouldRemoveAllWebsiteDataFor(statistic, shouldCheckForGrandfathering)) {
index 3c86a34..e34c5e9 100644 (file)
@@ -317,6 +317,11 @@ void ResourceLoadStatisticsStore::setPrevalentResourceForDebugMode(const Registr
     m_debugManualPrevalentResource = domain;
 }
 
+void ResourceLoadStatisticsStore::setAppBoundDomains(HashSet<RegistrableDomain>&& domains)
+{
+    m_appBoundDomains = WTFMove(domains);
+}
+
 void ResourceLoadStatisticsStore::scheduleStatisticsProcessingRequestIfNecessary()
 {
     ASSERT(!RunLoop::isMain());
@@ -568,6 +573,7 @@ void ResourceLoadStatisticsStore::resetParametersToDefaultValues()
     ASSERT(!RunLoop::isMain());
 
     m_parameters = { };
+    m_appBoundDomains.clear();
 }
 
 void ResourceLoadStatisticsStore::logTestingEvent(const String& event)
@@ -652,6 +658,11 @@ void ResourceLoadStatisticsStore::debugLogDomainsInBatches(const char* action, c
         RELEASE_LOG_INFO(ITPDebug, "%" PUBLIC_LOG_STRING " to (%{public}d of %u): %" PUBLIC_LOG_STRING ".", action, batchNumber, numberOfBatches, domainsToString(batch).utf8().data());
 }
 
+bool ResourceLoadStatisticsStore::shouldExemptFromWebsiteDataDeletion(const RegistrableDomain& domain) const
+{
+    return !domain.isEmpty() && (domain == m_standaloneApplicationDomain || m_appBoundDomains.contains(domain));
+}
+
 } // namespace WebKit
 
 #endif
index 448333c..2dbe9e5 100644 (file)
@@ -169,6 +169,7 @@ public:
     bool isSameSiteStrictEnforcementEnabled() const { return m_sameSiteStrictEnforcementEnabled == WebCore::SameSiteStrictEnforcementEnabled::Yes; };
     void setFirstPartyWebsiteDataRemovalMode(WebCore::FirstPartyWebsiteDataRemovalMode mode) { m_firstPartyWebsiteDataRemovalMode = mode; }
     void setStandaloneApplicationDomain(RegistrableDomain&& domain) { m_standaloneApplicationDomain = WTFMove(domain); }
+    void setAppBoundDomains(HashSet<RegistrableDomain>&&);
 
     virtual bool areAllThirdPartyCookiesBlockedUnder(const TopFrameDomain&) = 0;
     virtual void hasStorageAccess(const SubFrameDomain&, const TopFrameDomain&, Optional<WebCore::FrameIdentifier>, WebCore::PageIdentifier, CompletionHandler<void(bool)>&&) = 0;
@@ -249,7 +250,7 @@ protected:
     bool debugLoggingEnabled() const { return m_debugLoggingEnabled; };
     bool debugModeEnabled() const { return m_debugModeEnabled; }
     WebCore::FirstPartyWebsiteDataRemovalMode firstPartyWebsiteDataRemovalMode() const { return m_firstPartyWebsiteDataRemovalMode; }
-    RegistrableDomain standaloneApplicationDomain() const { return m_standaloneApplicationDomain; }
+    bool shouldExemptFromWebsiteDataDeletion(const RegistrableDomain&) const;
 
     static constexpr unsigned maxNumberOfRecursiveCallsInRedirectTraceBack { 50 };
     
@@ -291,6 +292,7 @@ private:
     ShouldIncludeLocalhost m_shouldIncludeLocalhost { ShouldIncludeLocalhost::Yes };
     WebCore::FirstPartyWebsiteDataRemovalMode m_firstPartyWebsiteDataRemovalMode { WebCore::FirstPartyWebsiteDataRemovalMode::AllButCookies };
     RegistrableDomain m_standaloneApplicationDomain;
+    HashSet<RegistrableDomain> m_appBoundDomains;
 };
 
 } // namespace WebKit
index 68f55c4..5387b76 100644 (file)
@@ -678,6 +678,33 @@ void WebResourceLoadStatisticsStore::setStandaloneApplicationDomain(const Regist
     });
 }
 
+void WebResourceLoadStatisticsStore::setAppBoundDomains(HashSet<RegistrableDomain>&& domains, CompletionHandler<void()>&& completionHandler)
+{
+    ASSERT(RunLoop::isMain());
+
+    if (isEphemeral() || domains.isEmpty()) {
+        completionHandler();
+        return;
+    }
+
+    auto domainsCopy = crossThreadCopy(domains);
+
+    if (m_networkSession) {
+        if (auto* storageSession = m_networkSession->networkStorageSession()) {
+            storageSession->setAppBoundDomains(WTFMove(domains));
+            storageSession->setThirdPartyCookieBlockingMode(ThirdPartyCookieBlockingMode::AllExceptBetweenAppBoundDomains);
+        }
+    }
+
+    postTask([this, domains = WTFMove(domainsCopy), completionHandler = WTFMove(completionHandler)]() mutable {
+        if (m_statisticsStore) {
+            m_statisticsStore->setAppBoundDomains(WTFMove(domains));
+            m_statisticsStore->setThirdPartyCookieBlockingMode(ThirdPartyCookieBlockingMode::AllExceptBetweenAppBoundDomains);
+        }
+        postTaskReply(WTFMove(completionHandler));
+    });
+}
+
 void WebResourceLoadStatisticsStore::didCreateNetworkProcess()
 {
     ASSERT(RunLoop::isMain());
@@ -1280,6 +1307,11 @@ void WebResourceLoadStatisticsStore::resetParametersToDefaultValues(CompletionHa
         return;
     }
 
+    if (m_networkSession) {
+        if (auto* storageSession = m_networkSession->networkStorageSession())
+            storageSession->resetAppBoundDomains();
+    }
+
     postTask([this, completionHandler = WTFMove(completionHandler)]() mutable {
         if (m_statisticsStore)
             m_statisticsStore->resetParametersToDefaultValues();
index 1359b3b..345fd28 100644 (file)
@@ -279,6 +279,7 @@ struct ThirdPartyData {
     void setSameSiteStrictEnforcementEnabled(WebCore::SameSiteStrictEnforcementEnabled);
     void setFirstPartyWebsiteDataRemovalMode(WebCore::FirstPartyWebsiteDataRemovalMode, CompletionHandler<void()>&&);
     void setStandaloneApplicationDomain(const RegistrableDomain&, CompletionHandler<void()>&&);
+    void setAppBoundDomains(HashSet<RegistrableDomain>&&, CompletionHandler<void()>&&);
     void didCreateNetworkProcess();
 
     void notifyResourceLoadStatisticsProcessed();
index 92e9fde..3729576 100644 (file)
@@ -1307,6 +1307,18 @@ void NetworkProcess::hasIsolatedSession(PAL::SessionID sessionID, const WebCore:
     completionHandler(result);
 }
 
+void NetworkProcess::setAppBoundDomainsForResourceLoadStatistics(PAL::SessionID sessionID, HashSet<WebCore::RegistrableDomain>&& appBoundDomains, CompletionHandler<void()>&& completionHandler)
+{
+    if (auto* networkSession = this->networkSession(sessionID)) {
+        if (auto* resourceLoadStatistics = networkSession->resourceLoadStatistics()) {
+            resourceLoadStatistics->setAppBoundDomains(WTFMove(appBoundDomains), WTFMove(completionHandler));
+            return;
+        }
+    }
+    ASSERT_NOT_REACHED();
+    completionHandler();
+}
+
 void NetworkProcess::setShouldDowngradeReferrerForTesting(bool enabled, CompletionHandler<void()>&& completionHandler)
 {
     forEachNetworkSession([enabled](auto& networkSession) {
@@ -1315,7 +1327,7 @@ void NetworkProcess::setShouldDowngradeReferrerForTesting(bool enabled, Completi
     completionHandler();
 }
 
-void NetworkProcess::setShouldBlockThirdPartyCookiesForTesting(PAL::SessionID sessionID, WebCore::ThirdPartyCookieBlockingMode blockingMode, CompletionHandler<void()>&& completionHandler)
+void NetworkProcess::setThirdPartyCookieBlockingMode(PAL::SessionID sessionID, WebCore::ThirdPartyCookieBlockingMode blockingMode, CompletionHandler<void()>&& completionHandler)
 {
     if (auto* networkSession = this->networkSession(sessionID))
         networkSession->setThirdPartyCookieBlockingMode(blockingMode);
index 87ee467..012dd65 100644 (file)
@@ -261,9 +261,10 @@ public:
     void setCrossSiteLoadWithLinkDecorationForTesting(PAL::SessionID, const RegistrableDomain& fromDomain, const RegistrableDomain& toDomain, CompletionHandler<void()>&&);
     void resetCrossSiteLoadsWithLinkDecorationForTesting(PAL::SessionID, CompletionHandler<void()>&&);
     void hasIsolatedSession(PAL::SessionID, const WebCore::RegistrableDomain&, CompletionHandler<void(bool)>&&) const;
+    void setAppBoundDomainsForResourceLoadStatistics(PAL::SessionID, HashSet<WebCore::RegistrableDomain>&&, CompletionHandler<void()>&&);
     bool isITPDatabaseEnabled() const { return m_isITPDatabaseEnabled; }
     void setShouldDowngradeReferrerForTesting(bool, CompletionHandler<void()>&&);
-    void setShouldBlockThirdPartyCookiesForTesting(PAL::SessionID, WebCore::ThirdPartyCookieBlockingMode, CompletionHandler<void()>&&);
+    void setThirdPartyCookieBlockingMode(PAL::SessionID, WebCore::ThirdPartyCookieBlockingMode, CompletionHandler<void()>&&);
     void setShouldEnbleSameSiteStrictEnforcementForTesting(PAL::SessionID, WebCore::SameSiteStrictEnforcementEnabled, CompletionHandler<void()>&&);
     void setFirstPartyWebsiteDataRemovalModeForTesting(PAL::SessionID, WebCore::FirstPartyWebsiteDataRemovalMode, CompletionHandler<void()>&&);
     void setToSameSiteStrictCookiesForTesting(PAL::SessionID, const WebCore::RegistrableDomain&, CompletionHandler<void()>&&);
index 15e1764..50bae43 100644 (file)
@@ -141,8 +141,9 @@ messages -> NetworkProcess LegacyReceiver {
     ResetCrossSiteLoadsWithLinkDecorationForTesting(PAL::SessionID sessionID) -> () Async
     DeleteCookiesForTesting(PAL::SessionID sessionID, WebCore::RegistrableDomain domain, bool includeHttpOnlyCookies) -> () Async
     HasIsolatedSession(PAL::SessionID sessionID, WebCore::RegistrableDomain domain) -> (bool hasIsolatedSession) Async
+    SetAppBoundDomainsForResourceLoadStatistics(PAL::SessionID sessionID, HashSet<WebCore::RegistrableDomain> appBoundDomains) -> () Async
     SetShouldDowngradeReferrerForTesting(bool enabled) -> () Async
-    SetShouldBlockThirdPartyCookiesForTesting(PAL::SessionID sessionID, enum:uint8_t WebCore::ThirdPartyCookieBlockingMode blockingMode) -> () Async
+    SetThirdPartyCookieBlockingMode(PAL::SessionID sessionID, enum:uint8_t WebCore::ThirdPartyCookieBlockingMode blockingMode) -> () Async
     SetShouldEnbleSameSiteStrictEnforcementForTesting(PAL::SessionID sessionID, enum:bool WebCore::SameSiteStrictEnforcementEnabled enabled) -> () Async
     SetFirstPartyWebsiteDataRemovalModeForTesting(PAL::SessionID sessionID, enum:uint8_t WebCore::FirstPartyWebsiteDataRemovalMode mode) -> () Async
     SetToSameSiteStrictCookiesForTesting(PAL::SessionID sessionID, WebCore::RegistrableDomain domain) -> () Async
index a2cdb43..61f10cc 100644 (file)
@@ -1227,6 +1227,8 @@ NetworkSessionCocoa::NetworkSessionCocoa(NetworkProcess& networkProcess, Network
     m_deviceManagementRestrictionsEnabled = parameters.deviceManagementRestrictionsEnabled;
     m_allLoadsBlockedByDeviceManagementRestrictionsForTesting = parameters.allLoadsBlockedByDeviceManagementRestrictionsForTesting;
 
+    if (m_resourceLoadStatistics && !parameters.resourceLoadStatisticsParameters.appBoundDomains.isEmpty())
+        m_resourceLoadStatistics->setAppBoundDomains(WTFMove(parameters.resourceLoadStatisticsParameters.appBoundDomains), [] { });
 #if HAVE(SESSION_CLEANUP)
     activateSessionCleanup(*this, parameters);
 #endif
index 927aec3..60de35f 100644 (file)
@@ -47,8 +47,9 @@ struct ResourceLoadStatisticsParameters {
     WebCore::SameSiteStrictEnforcementEnabled sameSiteStrictEnforcementEnabled { WebCore::SameSiteStrictEnforcementEnabled::No };
 #endif
     WebCore::FirstPartyWebsiteDataRemovalMode firstPartyWebsiteDataRemovalMode { WebCore::FirstPartyWebsiteDataRemovalMode::AllButCookies };
-    WebCore::RegistrableDomain standaloneApplicationDomain { };
-    WebCore::RegistrableDomain manualPrevalentResource { };
+    WebCore::RegistrableDomain standaloneApplicationDomain;
+    HashSet<WebCore::RegistrableDomain> appBoundDomains;
+    WebCore::RegistrableDomain manualPrevalentResource;
     
     void encode(IPC::Encoder& encoder) const
     {
@@ -65,6 +66,7 @@ struct ResourceLoadStatisticsParameters {
 #endif
         encoder << firstPartyWebsiteDataRemovalMode;
         encoder << standaloneApplicationDomain;
+        encoder << appBoundDomains;
         encoder << manualPrevalentResource;
     }
 
@@ -127,6 +129,11 @@ struct ResourceLoadStatisticsParameters {
         if (!standaloneApplicationDomain)
             return WTF::nullopt;
 
+        Optional<HashSet<WebCore::RegistrableDomain>> appBoundDomains;
+        decoder >> appBoundDomains;
+        if (!appBoundDomains)
+            return WTF::nullopt;
+
         Optional<WebCore::RegistrableDomain> manualPrevalentResource;
         decoder >> manualPrevalentResource;
         if (!manualPrevalentResource)
@@ -146,6 +153,7 @@ struct ResourceLoadStatisticsParameters {
 #endif
             WTFMove(*firstPartyWebsiteDataRemovalMode),
             WTFMove(*standaloneApplicationDomain),
+            WTFMove(*appBoundDomains),
             WTFMove(*manualPrevalentResource),
         }};
     }
index f0f5a14..2cfd3a1 100644 (file)
@@ -42,6 +42,7 @@
 #include "WebsiteDataRecord.h"
 #include "WebsiteDataStore.h"
 #include "WebsiteDataType.h"
+#include <WebCore/RegistrableDomain.h>
 #include <wtf/CallbackAggregator.h>
 #include <wtf/URL.h>
 
@@ -617,6 +618,31 @@ void WKWebsiteDataStoreSetResourceLoadStatisticsToSameSiteStrictCookiesForTestin
 #endif
 }
 
+void WKWebsiteDataStoreSetAppBoundDomainsForTesting(WKArrayRef originURLsRef, void* context, WKWebsiteDataStoreSetAppBoundDomainsForTestingFunction completionHandler)
+{
+#if PLATFORM(COCOA)
+    RefPtr<API::Array> originURLsArray = toImpl(originURLsRef);
+    size_t newSize = originURLsArray ? originURLsArray->size() : 0;
+    HashSet<WebCore::RegistrableDomain> domains;
+    domains.reserveInitialCapacity(newSize);
+    for (size_t i = 0; i < newSize; ++i) {
+        auto* originURL = originURLsArray->at<API::URL>(i);
+        if (!originURL)
+            continue;
+        
+        domains.add(WebCore::RegistrableDomain { URL(URL(), originURL->string()) });
+    }
+
+    WebKit::WebsiteDataStore::setAppBoundDomainsForTesting(WTFMove(domains), [context, completionHandler] {
+        completionHandler(context);
+    });
+#else
+    UNUSED_PARAM(originURLsRef);
+    UNUSED_PARAM(context);
+    UNUSED_PARAM(completionHandler);
+#endif
+}
+
 void WKWebsiteDataStoreStatisticsResetToConsistentState(WKWebsiteDataStoreRef dataStoreRef, void* context, WKWebsiteDataStoreStatisticsResetToConsistentStateFunction completionHandler)
 {
 #if ENABLE(RESOURCE_LOAD_STATISTICS)
index f9a3823..71b97f3 100644 (file)
@@ -129,6 +129,8 @@ typedef void (*WKWebsiteDataStoreSetResourceLoadStatisticsFirstPartyWebsiteDataR
 WK_EXPORT void WKWebsiteDataStoreSetResourceLoadStatisticsFirstPartyWebsiteDataRemovalModeForTesting(WKWebsiteDataStoreRef dataStoreRef, bool enabled, void* context, WKWebsiteDataStoreSetResourceLoadStatisticsFirstPartyWebsiteDataRemovalModeForTestingFunction completionHandler);
 typedef void (*WKWebsiteDataStoreSetResourceLoadStatisticsToSameSiteStrictCookiesForTestingFunction)(void* functionContext);
 WK_EXPORT void WKWebsiteDataStoreSetResourceLoadStatisticsToSameSiteStrictCookiesForTesting(WKWebsiteDataStoreRef dataStoreRef, WKStringRef hostName, void* context, WKWebsiteDataStoreSetResourceLoadStatisticsToSameSiteStrictCookiesForTestingFunction completionHandler);
+typedef void (*WKWebsiteDataStoreSetAppBoundDomainsForTestingFunction)(void* functionContext);
+WK_EXPORT void WKWebsiteDataStoreSetAppBoundDomainsForTesting(WKArrayRef originURLsRef, void* context, WKWebsiteDataStoreSetAppBoundDomainsForTestingFunction completionHandler);
 typedef void (*WKWebsiteDataStoreStatisticsResetToConsistentStateFunction)(void* functionContext);
 WK_EXPORT void WKWebsiteDataStoreStatisticsResetToConsistentState(WKWebsiteDataStoreRef dataStoreRef, void* context, WKWebsiteDataStoreStatisticsResetToConsistentStateFunction completionHandler);
 
index c1577b8..502988a 100644 (file)
@@ -1136,6 +1136,13 @@ void NetworkProcessProxy::hasIsolatedSession(PAL::SessionID sessionID, const Reg
     sendWithAsyncReply(Messages::NetworkProcess::HasIsolatedSession(sessionID, domain), WTFMove(completionHandler));
 }
 
+void NetworkProcessProxy::setAppBoundDomainsForResourceLoadStatistics(PAL::SessionID sessionID, const HashSet<RegistrableDomain>& appBoundDomains, CompletionHandler<void()>&& completionHandler)
+{
+    sendWithAsyncReply(Messages::NetworkProcess::SetAppBoundDomainsForResourceLoadStatistics(sessionID, appBoundDomains), [activity = throttler().backgroundActivity("NetworkProcessProxy::setAppBoundDomainsForResourceLoadStatistics"_s), completionHandler = WTFMove(completionHandler)]() mutable {
+        completionHandler();
+    });
+}
+
 void NetworkProcessProxy::setShouldDowngradeReferrerForTesting(bool enabled, CompletionHandler<void()>&& completionHandler)
 {
     if (!canSendMessage()) {
@@ -1148,9 +1155,9 @@ void NetworkProcessProxy::setShouldDowngradeReferrerForTesting(bool enabled, Com
     });
 }
 
-void NetworkProcessProxy::setShouldBlockThirdPartyCookiesForTesting(PAL::SessionID sessionID, ThirdPartyCookieBlockingMode blockingMode, CompletionHandler<void()>&& completionHandler)
+void NetworkProcessProxy::setThirdPartyCookieBlockingMode(PAL::SessionID sessionID, ThirdPartyCookieBlockingMode blockingMode, CompletionHandler<void()>&& completionHandler)
 {
-    sendWithAsyncReply(Messages::NetworkProcess::SetShouldBlockThirdPartyCookiesForTesting(sessionID, blockingMode), [activity = throttler().backgroundActivity("NetworkProcessProxy::setShouldBlockThirdPartyCookiesForTesting"_s), completionHandler = WTFMove(completionHandler)]() mutable {
+    sendWithAsyncReply(Messages::NetworkProcess::SetThirdPartyCookieBlockingMode(sessionID, blockingMode), [activity = throttler().backgroundActivity("NetworkProcessProxy::setThirdPartyCookieBlockingMode"_s), completionHandler = WTFMove(completionHandler)]() mutable {
         completionHandler();
     });
 }
index e07cf99..71ee3bf 100644 (file)
@@ -175,8 +175,9 @@ public:
     void deleteCookiesForTesting(PAL::SessionID, const RegistrableDomain&, bool includeHttpOnlyCookies, CompletionHandler<void()>&&);
     void deleteWebsiteDataInUIProcessForRegistrableDomains(PAL::SessionID, OptionSet<WebsiteDataType>, OptionSet<WebsiteDataFetchOption>, Vector<RegistrableDomain>, CompletionHandler<void(HashSet<WebCore::RegistrableDomain>&&)>&&);
     void hasIsolatedSession(PAL::SessionID, const RegistrableDomain&, CompletionHandler<void(bool)>&&);
+    void setAppBoundDomainsForResourceLoadStatistics(PAL::SessionID, const HashSet<RegistrableDomain>&, CompletionHandler<void()>&&);
     void setShouldDowngradeReferrerForTesting(bool, CompletionHandler<void()>&&);
-    void setShouldBlockThirdPartyCookiesForTesting(PAL::SessionID, WebCore::ThirdPartyCookieBlockingMode, CompletionHandler<void()>&&);
+    void setThirdPartyCookieBlockingMode(PAL::SessionID, WebCore::ThirdPartyCookieBlockingMode, CompletionHandler<void()>&&);
     void setShouldEnbleSameSiteStrictEnforcementForTesting(PAL::SessionID, WebCore::SameSiteStrictEnforcementEnabled, CompletionHandler<void()>&&);
     void setFirstPartyWebsiteDataRemovalModeForTesting(PAL::SessionID, WebCore::FirstPartyWebsiteDataRemovalMode, CompletionHandler<void()>&&);
     void setToSameSiteStrictCookiesForTesting(PAL::SessionID, const RegistrableDomain&, CompletionHandler<void()>&&);
index 224a5a2..8eb7f42 100644 (file)
@@ -605,9 +605,10 @@ NetworkProcessProxy& WebProcessPool::ensureNetworkProcess(WebsiteDataStore* with
     WebCore::ThirdPartyCookieBlockingMode thirdPartyCookieBlockingMode = WebCore::ThirdPartyCookieBlockingMode::All;
     WebCore::SameSiteStrictEnforcementEnabled sameSiteStrictEnforcementEnabled = WebCore::SameSiteStrictEnforcementEnabled::No;
 #endif
-    WebCore::RegistrableDomain standaloneApplicationDomain { };
     WebCore::FirstPartyWebsiteDataRemovalMode firstPartyWebsiteDataRemovalMode = WebCore::FirstPartyWebsiteDataRemovalMode::AllButCookies;
-    WebCore::RegistrableDomain manualPrevalentResource { };
+    WebCore::RegistrableDomain standaloneApplicationDomain;
+    HashSet<WebCore::RegistrableDomain> appBoundDomains;
+    WebCore::RegistrableDomain manualPrevalentResource;
     WEB_PROCESS_POOL_ADDITIONS_2
     if (withWebsiteDataStore) {
         enableResourceLoadStatistics = withWebsiteDataStore->resourceLoadStatisticsEnabled();
@@ -624,6 +625,7 @@ NetworkProcessProxy& WebProcessPool::ensureNetworkProcess(WebsiteDataStore* with
 #endif
             firstPartyWebsiteDataRemovalMode = networkSessionParameters.resourceLoadStatisticsParameters.firstPartyWebsiteDataRemovalMode;
             standaloneApplicationDomain = networkSessionParameters.resourceLoadStatisticsParameters.standaloneApplicationDomain;
+            appBoundDomains = networkSessionParameters.resourceLoadStatisticsParameters.appBoundDomains;
             manualPrevalentResource = networkSessionParameters.resourceLoadStatisticsParameters.manualPrevalentResource;
         }
 
@@ -650,6 +652,7 @@ NetworkProcessProxy& WebProcessPool::ensureNetworkProcess(WebsiteDataStore* with
 #endif
             firstPartyWebsiteDataRemovalMode = networkSessionParameters.resourceLoadStatisticsParameters.firstPartyWebsiteDataRemovalMode;
             standaloneApplicationDomain = networkSessionParameters.resourceLoadStatisticsParameters.standaloneApplicationDomain;
+            appBoundDomains = networkSessionParameters.resourceLoadStatisticsParameters.appBoundDomains;
             manualPrevalentResource = networkSessionParameters.resourceLoadStatisticsParameters.manualPrevalentResource;
         }
 
@@ -689,6 +692,8 @@ NetworkProcessProxy& WebProcessPool::ensureNetworkProcess(WebsiteDataStore* with
         sameSiteStrictEnforcementEnabled,
 #endif
         firstPartyWebsiteDataRemovalMode,
+        standaloneApplicationDomain,
+        appBoundDomains,
         manualPrevalentResource,
     };
 
index 50c0bf9..3aa994a 100644 (file)
@@ -458,9 +458,9 @@ void WebProcessProxy::notifyPageStatisticsTelemetryFinished(API::Object* message
         page.value->postMessageToInjectedBundle("ResourceLoadStatisticsTelemetryFinished", messageBody);
 }
 
-void WebProcessProxy::setShouldBlockThirdPartyCookiesForTesting(ThirdPartyCookieBlockingMode thirdPartyCookieBlockingMode, CompletionHandler<void()>&& completionHandler)
+void WebProcessProxy::setThirdPartyCookieBlockingMode(ThirdPartyCookieBlockingMode thirdPartyCookieBlockingMode, CompletionHandler<void()>&& completionHandler)
 {
-    sendWithAsyncReply(Messages::WebProcess::SetShouldBlockThirdPartyCookiesForTesting(thirdPartyCookieBlockingMode), [activity = throttler().backgroundActivity("WebProcessProxy::setShouldBlockThirdPartyCookiesForTesting"_s), completionHandler = WTFMove(completionHandler)]() mutable {
+    sendWithAsyncReply(Messages::WebProcess::SetThirdPartyCookieBlockingMode(thirdPartyCookieBlockingMode), [activity = throttler().backgroundActivity("WebProcessProxy::setThirdPartyCookieBlockingMode"_s), completionHandler = WTFMove(completionHandler)]() mutable {
         completionHandler();
     });
 }
index c1e3a14..51bf9fd 100644 (file)
@@ -219,7 +219,7 @@ public:
     static void notifyWebsiteDataDeletionForRegistrableDomainsFinished();
     static void notifyWebsiteDataScanForRegistrableDomainsFinished();
 
-    void setShouldBlockThirdPartyCookiesForTesting(WebCore::ThirdPartyCookieBlockingMode, CompletionHandler<void()>&&);
+    void setThirdPartyCookieBlockingMode(WebCore::ThirdPartyCookieBlockingMode, CompletionHandler<void()>&&);
 #endif
 
     void enableSuddenTermination();
index ce8c3ee..1bd5351 100644 (file)
@@ -413,6 +413,9 @@ void WebsiteDataStore::initializeAppBoundDomains(ForceReinitialization forceRein
         keyExists = domains ? true : false;
         
         RunLoop::main().dispatch([isInAppBrowserPrivacyEnabled, forceReinitialization, domains = retainPtr(domains)] {
+            if (hasInitializedAppBoundDomains && forceReinitialization != ForceReinitialization::Yes)
+                return;
+
             if (forceReinitialization == ForceReinitialization::Yes)
                 appBoundDomains().clear();
 
@@ -433,6 +436,8 @@ void WebsiteDataStore::initializeAppBoundDomains(ForceReinitialization forceRein
                 WEBSITE_DATA_STORE_ADDITIONS
             }
             hasInitializedAppBoundDomains = true;
+            if (isAppBoundITPRelaxationEnabled)
+                forwardAppBoundDomainsToITPIfInitialized([] { });
         });
     });
 }
@@ -444,6 +449,8 @@ void WebsiteDataStore::ensureAppBoundDomains(CompletionHandler<void(const HashSe
         return;
     }
 
+    // Hopping to the background thread then back to the main thread
+    // ensures that initializeAppBoundDomains() has finished.
     appBoundDomainQueue().dispatch([completionHandler = WTFMove(completionHandler)] () mutable {
         RunLoop::main().dispatch([completionHandler = WTFMove(completionHandler)] () mutable {
             ASSERT(hasInitializedAppBoundDomains);
@@ -486,6 +493,24 @@ void WebsiteDataStore::getAppBoundDomains(CompletionHandler<void(const HashSet<W
     });
 }
 
+Optional<HashSet<WebCore::RegistrableDomain>> WebsiteDataStore::appBoundDomainsIfInitialized()
+{
+    ASSERT(RunLoop::isMain());
+    if (!hasInitializedAppBoundDomains)
+        return WTF::nullopt;
+    return appBoundDomains();
+}
+
+void WebsiteDataStore::setAppBoundDomainsForTesting(HashSet<WebCore::RegistrableDomain>&& domains, CompletionHandler<void()>&& completionHandler)
+{
+    for (auto& domain : domains)
+        RELEASE_ASSERT(domain == "localhost"_s || domain == "127.0.0.1"_s);
+
+    appBoundDomains() = WTFMove(domains);
+    hasInitializedAppBoundDomains = true;
+    forwardAppBoundDomainsToITPIfInitialized(WTFMove(completionHandler));
+}
+
 void WebsiteDataStore::reinitializeAppBoundDomains()
 {
     hasInitializedAppBoundDomains = false;
index e36376c..a0b4bd7 100644 (file)
@@ -1862,16 +1862,22 @@ void WebsiteDataStore::setResourceLoadStatisticsShouldBlockThirdPartyCookiesForT
     WebCore::ThirdPartyCookieBlockingMode blockingMode = WebCore::ThirdPartyCookieBlockingMode::OnlyAccordingToPerDomainPolicy;
     if (enabled)
         blockingMode = onlyOnSitesWithoutUserInteraction ? WebCore::ThirdPartyCookieBlockingMode::AllOnSitesWithoutUserInteraction : WebCore::ThirdPartyCookieBlockingMode::All;
+    setThirdPartyCookieBlockingMode(blockingMode, WTFMove(completionHandler));
+}
+
+void WebsiteDataStore::setThirdPartyCookieBlockingMode(WebCore::ThirdPartyCookieBlockingMode blockingMode, CompletionHandler<void()>&& completionHandler)
+{
+    auto callbackAggregator = CallbackAggregator::create(WTFMove(completionHandler));
 
     if (thirdPartyCookieBlockingMode() != blockingMode) {
         m_thirdPartyCookieBlockingMode = blockingMode;
         for (auto& webProcess : processes())
-            webProcess.setShouldBlockThirdPartyCookiesForTesting(blockingMode, [callbackAggregator = callbackAggregator.copyRef()] { });
+            webProcess.setThirdPartyCookieBlockingMode(blockingMode, [callbackAggregator = callbackAggregator.copyRef()] { });
     }
 
     for (auto& processPool : processPools()) {
         if (auto* networkProcess = processPool->networkProcess())
-            networkProcess->setShouldBlockThirdPartyCookiesForTesting(m_sessionID, blockingMode, [callbackAggregator = callbackAggregator.copyRef()] { });
+            networkProcess->setThirdPartyCookieBlockingMode(m_sessionID, blockingMode, [callbackAggregator = callbackAggregator.copyRef()] { });
     }
 }
 
@@ -2225,8 +2231,13 @@ WebsiteDataStoreParameters WebsiteDataStore::parameters()
     bool shouldIncludeLocalhostInResourceLoadStatistics = false;
     bool enableResourceLoadStatisticsDebugMode = false;
     auto firstPartyWebsiteDataRemovalMode = WebCore::FirstPartyWebsiteDataRemovalMode::AllButCookies;
-    WebCore::RegistrableDomain resourceLoadStatisticsManualPrevalentResource { };
-
+    WebCore::RegistrableDomain standaloneApplicationDomain;
+    HashSet<WebCore::RegistrableDomain> appBoundDomains;
+#if PLATFORM(COCOA)
+    if (isAppBoundITPRelaxationEnabled)
+        appBoundDomains = appBoundDomainsIfInitialized().valueOr(HashSet<WebCore::RegistrableDomain> { });
+#endif
+    WebCore::RegistrableDomain resourceLoadStatisticsManualPrevalentResource;
     ResourceLoadStatisticsParameters resourceLoadStatisticsParameters = {
         WTFMove(resourceLoadStatisticsDirectory),
         WTFMove(resourceLoadStatisticsDirectoryHandle),
@@ -2245,6 +2256,8 @@ WebsiteDataStoreParameters WebsiteDataStore::parameters()
         WebCore::SameSiteStrictEnforcementEnabled::No,
 #endif
         firstPartyWebsiteDataRemovalMode,
+        WTFMove(standaloneApplicationDomain),
+        WTFMove(appBoundDomains),
         WTFMove(resourceLoadStatisticsManualPrevalentResource),
     };
 
@@ -2424,4 +2437,39 @@ void WebsiteDataStore::renameOriginInWebsiteData(URL&& oldName, URL&& newName, O
     }
 }
 
+#if PLATFORM(COCOA)
+void WebsiteDataStore::forwardAppBoundDomainsToITPIfInitialized(CompletionHandler<void()>&& completionHandler)
+{
+    auto callbackAggregator = CallbackAggregator::create(WTFMove(completionHandler));
+    auto appBoundDomains = appBoundDomainsIfInitialized();
+    if (!appBoundDomains)
+        return;
+
+    auto propagateAppBoundDomains = [callbackAggregator = callbackAggregator.copyRef()] (WebsiteDataStore* store, const HashSet<WebCore::RegistrableDomain>& domains) {
+        if (!store)
+            return;
+
+        if (store->thirdPartyCookieBlockingMode() != WebCore::ThirdPartyCookieBlockingMode::AllExceptBetweenAppBoundDomains)
+            store->setThirdPartyCookieBlockingMode(WebCore::ThirdPartyCookieBlockingMode::AllExceptBetweenAppBoundDomains, [callbackAggregator = callbackAggregator.copyRef()] { });
+
+        store->setAppBoundDomainsForITP(domains, [callbackAggregator = callbackAggregator.copyRef()] { });
+    };
+
+    propagateAppBoundDomains(globalDefaultDataStore().get(), *appBoundDomains);
+
+    for (auto* store : nonDefaultDataStores().values())
+        propagateAppBoundDomains(store, *appBoundDomains);
+}
+
+void WebsiteDataStore::setAppBoundDomainsForITP(const HashSet<WebCore::RegistrableDomain>& domains, CompletionHandler<void()>&& completionHandler)
+{
+    auto callbackAggregator = CallbackAggregator::create(WTFMove(completionHandler));
+
+    for (auto& processPool : processPools()) {
+        if (auto* networkProcess = processPool->networkProcess())
+            networkProcess->setAppBoundDomainsForResourceLoadStatistics(m_sessionID, domains, [callbackAggregator = callbackAggregator.copyRef()] { });
+    }
+}
+#endif
+
 }
index cd67eb5..a35b697 100644 (file)
@@ -199,6 +199,7 @@ public:
     void hasIsolatedSessionForTesting(const URL&, CompletionHandler<void(bool)>&&) const;
     void setResourceLoadStatisticsShouldDowngradeReferrerForTesting(bool, CompletionHandler<void()>&&);
     void setResourceLoadStatisticsShouldBlockThirdPartyCookiesForTesting(bool enabled, bool onlyOnSitesWithoutUserInteraction, CompletionHandler<void()>&&);
+    void setThirdPartyCookieBlockingMode(WebCore::ThirdPartyCookieBlockingMode, CompletionHandler<void()>&&);
     void setResourceLoadStatisticsShouldEnbleSameSiteStrictEnforcementForTesting(bool enabled, CompletionHandler<void()>&&);
     void setResourceLoadStatisticsFirstPartyWebsiteDataRemovalModeForTesting(bool enabled, CompletionHandler<void()>&&);
     void setResourceLoadStatisticsToSameSiteStrictCookiesForTesting(const URL&, CompletionHandler<void()>&&);
@@ -296,6 +297,7 @@ public:
     void getAppBoundDomains(CompletionHandler<void(const HashSet<WebCore::RegistrableDomain>&)>&&) const;
     void ensureAppBoundDomains(CompletionHandler<void(const HashSet<WebCore::RegistrableDomain>&)>&&) const;
     void reinitializeAppBoundDomains();
+    static void setAppBoundDomainsForTesting(HashSet<WebCore::RegistrableDomain>&&, CompletionHandler<void()>&&);
 
 private:
     enum class ForceReinitialization : bool { No, Yes };
@@ -331,6 +333,13 @@ private:
 
     void maybeRegisterWithSessionIDMap();
 
+#if PLATFORM(COCOA)
+    static Optional<HashSet<WebCore::RegistrableDomain>> appBoundDomainsIfInitialized();
+    constexpr static const std::atomic<bool> isAppBoundITPRelaxationEnabled = false;
+    static void forwardAppBoundDomainsToITPIfInitialized(CompletionHandler<void()>&&);
+    void setAppBoundDomainsForITP(const HashSet<WebCore::RegistrableDomain>&, CompletionHandler<void()>&&);
+#endif
+
     const PAL::SessionID m_sessionID;
 
     Ref<WebsiteDataStoreConfiguration> m_resolvedConfiguration;
index 82c3c9b..54658e1 100644 (file)
@@ -1895,7 +1895,7 @@ bool WebProcess::areAllPagesThrottleable() const
 }
 
 #if ENABLE(RESOURCE_LOAD_STATISTICS)
-void WebProcess::setShouldBlockThirdPartyCookiesForTesting(ThirdPartyCookieBlockingMode thirdPartyCookieBlockingMode, CompletionHandler<void()>&& completionHandler)
+void WebProcess::setThirdPartyCookieBlockingMode(ThirdPartyCookieBlockingMode thirdPartyCookieBlockingMode, CompletionHandler<void()>&& completionHandler)
 {
     m_thirdPartyCookieBlockingMode = thirdPartyCookieBlockingMode;
     completionHandler();
index 189b34a..b8c9e7c 100644 (file)
@@ -458,7 +458,7 @@ private:
 #endif
 
 #if ENABLE(RESOURCE_LOAD_STATISTICS)
-    void setShouldBlockThirdPartyCookiesForTesting(WebCore::ThirdPartyCookieBlockingMode, CompletionHandler<void()>&&);
+    void setThirdPartyCookieBlockingMode(WebCore::ThirdPartyCookieBlockingMode, CompletionHandler<void()>&&);
 #endif
 
     void platformInitializeProcess(const AuxiliaryProcessInitializationParameters&);
index e0fee0b..1149bf0 100644 (file)
@@ -157,7 +157,7 @@ messages -> WebProcess LegacyReceiver NotRefCounted {
 
 #if ENABLE(RESOURCE_LOAD_STATISTICS)
     SeedResourceLoadStatisticsForTesting(WebCore::RegistrableDomain firstPartyDomain, WebCore::RegistrableDomain thirdPartyDomain, bool shouldScheduleNotification) -> () Async
-    SetShouldBlockThirdPartyCookiesForTesting(enum:uint8_t WebCore::ThirdPartyCookieBlockingMode blockingMode) -> () Async
+    SetThirdPartyCookieBlockingMode(enum:uint8_t WebCore::ThirdPartyCookieBlockingMode blockingMode) -> () Async
 #endif
 
 #if PLATFORM(IOS)
index e13daf1..962eb53 100644 (file)
@@ -1,3 +1,32 @@
+2020-05-06  John Wilander  <wilander@apple.com>
+
+        Exempt app-bound domains from ITP's website data deletion and third-party cookie blocking between themselves
+        https://bugs.webkit.org/show_bug.cgi?id=210674
+        <rdar://problem/61950767>
+
+        Reviewed by Chris Dumez.
+
+        This change adds a new TestRunner function
+        setAppBoundDomain() which takes an array of origin
+        strings and sets them to app-bound domains.
+
+        * WebKitTestRunner/InjectedBundle/Bindings/TestRunner.idl:
+        * WebKitTestRunner/InjectedBundle/InjectedBundle.cpp:
+        (WTR::InjectedBundle::didReceiveMessageToPage):
+        * WebKitTestRunner/InjectedBundle/TestRunner.cpp:
+        (WTR::TestRunner::setAppBoundDomains):
+        (WTR::TestRunner::didSetAppBoundDomainsCallback):
+        * WebKitTestRunner/InjectedBundle/TestRunner.h:
+        * WebKitTestRunner/TestController.cpp:
+        (WTR::AppBoundDomainsCallbackContext::AppBoundDomainsCallbackContext):
+        (WTR::didSetAppBoundDomainsCallback):
+        (WTR::TestController::setAppBoundDomains):
+        * WebKitTestRunner/TestController.h:
+        * WebKitTestRunner/TestInvocation.cpp:
+        (WTR::TestInvocation::didReceiveMessageFromInjectedBundle):
+        (WTR::TestInvocation::didSetAppBoundDomains):
+        * WebKitTestRunner/TestInvocation.h:
+
 2020-05-06  Aakash Jain  <aakash_jain@apple.com>
 
         Delete code for feeder queue
index 5cbe951..a19df95 100644 (file)
@@ -393,7 +393,9 @@ interface TestRunner {
     void resetMockMediaDevices();
     void setMockCameraOrientation(unsigned long orientation);
     boolean isMockRealtimeMediaSourceCenterEnabled();
+
     boolean hasAppBoundSession();
+    void setAppBoundDomains(object originsArray, object callback);
 
     void injectUserScript(DOMString string);
     readonly attribute unsigned long userScriptInjectedCount;
index 10e6ee3..fe5d757 100644 (file)
@@ -518,7 +518,12 @@ void InjectedBundle::didReceiveMessageToPage(WKBundlePageRef page, WKStringRef m
         m_testRunner->performCustomMenuAction();
         return;
     }
-    
+
+    if (WKStringIsEqualToUTF8CString(messageName, "CallDidSetAppBoundDomains")) {
+        m_testRunner->didSetAppBoundDomainsCallback();
+        return;
+    }
+
     WKRetainPtr<WKStringRef> errorMessageName = adoptWK(WKStringCreateWithUTF8CString("Error"));
     WKRetainPtr<WKStringRef> errorMessageBody = adoptWK(WKStringCreateWithUTF8CString("Unknown"));
     WKBundlePagePostMessage(page, errorMessageName.get(), errorMessageBody.get());
index 53d11ad..4a18c18 100644 (file)
@@ -763,6 +763,7 @@ enum {
     TextFieldDidBeginEditingCallbackID,
     TextFieldDidEndEditingCallbackID,
     CustomMenuActionCallbackID,
+    DidSetAppBoundDomainsCallbackID,
     FirstUIScriptCallbackID = 100
 };
 
@@ -2975,4 +2976,43 @@ bool TestRunner::hasAppBoundSession()
     return WKBooleanGetValue(adoptWK(static_cast<WKBooleanRef>(returnData)).get());
 }
 
+void TestRunner::setAppBoundDomains(JSValueRef originArray, JSValueRef completionHandler)
+{
+    cacheTestRunnerCallback(DidSetAppBoundDomainsCallbackID, completionHandler);
+
+    JSContextRef context = WKBundleFrameGetJavaScriptContext(WKBundlePageGetMainFrame(InjectedBundle::singleton().page()->page()));
+
+    if (!JSValueIsArray(context, originArray))
+        return;
+
+    JSObjectRef origins = JSValueToObject(context, originArray, nullptr);
+    static auto lengthProperty = adopt(JSStringCreateWithUTF8CString("length"));
+    JSValueRef originsLengthValue = JSObjectGetProperty(context, origins, lengthProperty.get(), nullptr);
+    if (!JSValueIsNumber(context, originsLengthValue))
+        return;
+
+    auto originURLs = adoptWK(WKMutableArrayCreate());
+    auto originsLength = static_cast<size_t>(JSValueToNumber(context, originsLengthValue, nullptr));
+    for (size_t i = 0; i < originsLength; ++i) {
+        JSValueRef originValue = JSObjectGetPropertyAtIndex(context, origins, i, nullptr);
+        if (!JSValueIsString(context, originValue))
+            continue;
+
+        auto origin = adopt(JSValueToStringCopy(context, originValue, nullptr));
+        size_t originBufferSize = JSStringGetMaximumUTF8CStringSize(origin.get()) + 1;
+        auto originBuffer = makeUniqueArray<char>(originBufferSize);
+        JSStringGetUTF8CString(origin.get(), originBuffer.get(), originBufferSize);
+
+        WKArrayAppendItem(originURLs.get(), adoptWK(WKURLCreateWithUTF8CString(originBuffer.get())).get());
+    }
+
+    auto messageName = adoptWK(WKStringCreateWithUTF8CString("SetAppBoundDomains"));
+    WKBundlePostMessage(InjectedBundle::singleton().bundle(), messageName.get(), originURLs.get());
+}
+
+void TestRunner::didSetAppBoundDomainsCallback()
+{
+    callTestRunnerCallback(DidSetAppBoundDomainsCallbackID);
+}
+
 } // namespace WTR
index f5b636e..ce801a3 100644 (file)
@@ -500,7 +500,10 @@ public:
     void resetMockMediaDevices();
     void setMockCameraOrientation(unsigned);
     bool isMockRealtimeMediaSourceCenterEnabled();
+
     bool hasAppBoundSession();
+    void setAppBoundDomains(JSValueRef originArray, JSValueRef callback);
+    void didSetAppBoundDomainsCallback();
 
     size_t userScriptInjectedCount() const;
     void injectUserScript(JSStringRef);
index c4d57e1..4746032 100644 (file)
@@ -3777,6 +3777,30 @@ void TestController::setStatisticsToSameSiteStrictCookies(WKStringRef hostName)
     m_currentInvocation->didSetToSameSiteStrictCookies();
 }
 
+struct AppBoundDomainsCallbackContext {
+    explicit AppBoundDomainsCallbackContext(TestController& controller)
+        : testController(controller)
+    {
+    }
+
+    bool done { false };
+    TestController& testController;
+};
+
+static void didSetAppBoundDomainsCallback(void* callbackContext)
+{
+    auto* context = static_cast<AppBoundDomainsCallbackContext*>(callbackContext);
+    context->done = true;
+}
+
+void TestController::setAppBoundDomains(WKArrayRef originURLs)
+{
+    AppBoundDomainsCallbackContext context(*this);
+    WKWebsiteDataStoreSetAppBoundDomainsForTesting(originURLs, &context, didSetAppBoundDomainsCallback);
+    runUntil(context.done, noTimeout);
+    m_currentInvocation->didSetAppBoundDomains();
+}
+
 void TestController::statisticsResetToConsistentState()
 {
     ResourceStatisticsCallbackContext context(*this);
index 337ef6e..05461c4 100644 (file)
@@ -266,6 +266,7 @@ public:
     void setStatisticsShouldBlockThirdPartyCookies(bool value, bool onlyOnSitesWithoutUserInteraction);
     void setStatisticsFirstPartyWebsiteDataRemovalMode(bool value);
     void setStatisticsToSameSiteStrictCookies(WKStringRef hostName);
+    void setAppBoundDomains(WKArrayRef originURLs);
     void statisticsResetToConsistentState();
 
     void getAllStorageAccessEntries();
@@ -352,6 +353,8 @@ public:
     void setAdClickAttributionConversionURLForTesting(WKURLRef);
     void markAdClickAttributionsAsExpiredForTesting();
 
+    void didSetAppBoundDomains() const;
+
 private:
     WKRetainPtr<WKPageConfigurationRef> generatePageConfiguration(const TestOptions&);
     WKRetainPtr<WKContextConfigurationRef> generateContextConfiguration(const TestOptions::ContextOptions&) const;
index 7ea7d20..ea1a9f7 100644 (file)
@@ -994,6 +994,13 @@ void TestInvocation::didReceiveMessageFromInjectedBundle(WKStringRef messageName
         return;
     }
 
+    if (WKStringIsEqualToUTF8CString(messageName, "SetAppBoundDomains")) {
+        ASSERT(WKGetTypeID(messageBody) == WKArrayGetTypeID());
+        WKArrayRef originURLs = static_cast<WKArrayRef>(messageBody);
+        TestController::singleton().setAppBoundDomains(originURLs);
+        return;
+    }
+
     ASSERT_NOT_REACHED();
 }
 
@@ -2011,6 +2018,12 @@ void TestInvocation::didRemoveAllSessionCredentials()
     WKPagePostMessageToInjectedBundle(TestController::singleton().mainWebView()->page(), messageName.get(), 0);
 }
 
+void TestInvocation::didSetAppBoundDomains()
+{
+    WKRetainPtr<WKStringRef> messageName = adoptWK(WKStringCreateWithUTF8CString("CallDidSetAppBoundDomains"));
+    WKPagePostMessageToInjectedBundle(TestController::singleton().mainWebView()->page(), messageName.get(), nullptr);
+}
+
 void TestInvocation::dumpResourceLoadStatistics()
 {
     m_shouldDumpResourceLoadStatistics = true;
index 1e577da..293440c 100644 (file)
@@ -90,7 +90,9 @@ public:
     void didReceiveLoadedThirdPartyDomains(Vector<String>&& domains);
 
     void didRemoveAllSessionCredentials();
-    
+
+    void didSetAppBoundDomains();
+
     void dumpResourceLoadStatistics();
 
     bool canOpenWindows() const { return m_canOpenWindows; }