Add SPI to configure WebsiteDataStores with a URL for standalone web applications and use it to disable first-party website data removal in ITP
https://bugs.webkit.org/show_bug.cgi?id=209634
<rdar://problem/60943970>

Reviewed by Alex Christensen.

Source/WebKit:

This change adds a new property to _WKWebsiteDataStoreConfiguration.h called
standaloneApplicationURL with which the hosting application can inform the
website data store that it's running as a standalone web application.

This change also forwards an existing standaloneApplicationURL as a
WebCore::RegistrableDomain into ITP so that explicit exemptions can be made
to first parties of standalone web applications. The exemptions made here
all for all of ITP's website data removal. This part of the change is
covered by the new layout tests.

Tests: http/tests/resourceLoadStatistics/standalone-web-application-exempt-from-website-data-deletion-database.html
       http/tests/resourceLoadStatistics/standalone-web-application-exempt-from-website-data-deletion.html

* NetworkProcess/Classifier/ResourceLoadStatisticsDatabaseStore.cpp:
* NetworkProcess/Classifier/ResourceLoadStatisticsMemoryStore.cpp:
(WebKit::ResourceLoadStatisticsMemoryStore::registrableDomainsToDeleteOrRestrictWebsiteDataFor):
* NetworkProcess/Classifier/ResourceLoadStatisticsStore.h:
(WebKit::ResourceLoadStatisticsStore::setStandaloneApplicationDomain):
(WebKit::ResourceLoadStatisticsStore::standaloneApplicationDomain const):
* NetworkProcess/Classifier/WebResourceLoadStatisticsStore.cpp:
(WebKit::WebResourceLoadStatisticsStore::setStandaloneApplicationDomain):
* NetworkProcess/Classifier/WebResourceLoadStatisticsStore.h:
* NetworkProcess/NetworkSession.cpp:
(WebKit::NetworkSession::NetworkSession):
(WebKit::NetworkSession::forwardResourceLoadStatisticsSettings):
* NetworkProcess/NetworkSession.h:
* Shared/ResourceLoadStatisticsParameters.h:
(WebKit::ResourceLoadStatisticsParameters::encode const):
(WebKit::ResourceLoadStatisticsParameters::decode):
* UIProcess/API/Cocoa/_WKWebsiteDataStoreConfiguration.h:
* UIProcess/API/Cocoa/_WKWebsiteDataStoreConfiguration.mm:
(-[_WKWebsiteDataStoreConfiguration standaloneApplicationURL]):
(-[_WKWebsiteDataStoreConfiguration setStandaloneApplication:]):
* UIProcess/WebProcessPool.cpp:
(WebKit::WebProcessPool::ensureNetworkProcess):
* UIProcess/WebsiteData/Cocoa/WebsiteDataStoreCocoa.mm:
(WebKit::WebsiteDataStore::platformSetNetworkParameters):
* UIProcess/WebsiteData/WebsiteDataStoreConfiguration.cpp:
(WebKit::WebsiteDataStoreConfiguration::copy const):
* UIProcess/WebsiteData/WebsiteDataStoreConfiguration.h:
(WebKit::WebsiteDataStoreConfiguration::standaloneApplicationURL const):
(WebKit::WebsiteDataStoreConfiguration::setStandaloneApplicationURL):

Tools:

Added a new test option called standaloneWebApplicationURL so that layout tests can
configure the website data store accordingly. Picking it up and using it requires
creating a new website data store with a configuration that has the standalone web
application URL.

* WebKitTestRunner/TestController.cpp:
(WTR::parseStringTestHeaderValueAsURL):
(WTR::updateTestOptionsFromTestHeader):
* WebKitTestRunner/TestOptions.h:
* WebKitTestRunner/cocoa/TestControllerCocoa.mm:
(WTR::TestController::platformCreateWebView):

LayoutTests:

* http/tests/resourceLoadStatistics/standalone-web-application-exempt-from-website-data-deletion-database-expected.txt: Added.
* http/tests/resourceLoadStatistics/standalone-web-application-exempt-from-website-data-deletion-database.html: Added.
* http/tests/resourceLoadStatistics/standalone-web-application-exempt-from-website-data-deletion-expected.txt: Added.
* http/tests/resourceLoadStatistics/standalone-web-application-exempt-from-website-data-deletion.html: Added.


git-svn-id: http://svn.webkit.org/repository/webkit/trunk@259440 268f45cc-cd09-0410-ab3c-d52691b4dbfc
diff --git a/LayoutTests/ChangeLog b/LayoutTests/ChangeLog
index 62004e0..8e1f07a 100644
--- a/LayoutTests/ChangeLog
+++ b/LayoutTests/ChangeLog
@@ -1,5 +1,18 @@
 2020-04-02  John Wilander  <wilander@apple.com>
 
+        Add SPI to configure WebsiteDataStores with a URL for standalone web applications and use it to disable first-party website data removal in ITP
+        https://bugs.webkit.org/show_bug.cgi?id=209634
+        <rdar://problem/60943970>
+
+        Reviewed by Alex Christensen.
+
+        * http/tests/resourceLoadStatistics/standalone-web-application-exempt-from-website-data-deletion-database-expected.txt: Added.
+        * http/tests/resourceLoadStatistics/standalone-web-application-exempt-from-website-data-deletion-database.html: Added.
+        * http/tests/resourceLoadStatistics/standalone-web-application-exempt-from-website-data-deletion-expected.txt: Added.
+        * http/tests/resourceLoadStatistics/standalone-web-application-exempt-from-website-data-deletion.html: Added.
+
+2020-04-02  John Wilander  <wilander@apple.com>
+
         Rebase expectation files for anchor tag tests with line number output
         https://bugs.webkit.org/show_bug.cgi?id=209945
         <rdar://problem/61237662>
diff --git a/LayoutTests/http/tests/resourceLoadStatistics/standalone-web-application-exempt-from-website-data-deletion-database-expected.txt b/LayoutTests/http/tests/resourceLoadStatistics/standalone-web-application-exempt-from-website-data-deletion-database-expected.txt
new file mode 100644
index 0000000..5b6a435
--- /dev/null
+++ b/LayoutTests/http/tests/resourceLoadStatistics/standalone-web-application-exempt-from-website-data-deletion-database-expected.txt
@@ -0,0 +1,15 @@
+Check that non-cookie website data does not get removed after a period of no user interaction if the website is a standalone web application.
+
+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/standalone-web-application-exempt-from-website-data-deletion-database.html b/LayoutTests/http/tests/resourceLoadStatistics/standalone-web-application-exempt-from-website-data-deletion-database.html
new file mode 100644
index 0000000..64f6423
--- /dev/null
+++ b/LayoutTests/http/tests/resourceLoadStatistics/standalone-web-application-exempt-from-website-data-deletion-database.html
@@ -0,0 +1,255 @@
+<!-- webkit-test-runner [ standaloneWebApplicationURL=http://127.0.0.1 ] -->
+<!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 non-cookie website data does not get removed after a period of no user interaction if the website is a standalone web application.</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.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/standalone-web-application-exempt-from-website-data-deletion-expected.txt b/LayoutTests/http/tests/resourceLoadStatistics/standalone-web-application-exempt-from-website-data-deletion-expected.txt
new file mode 100644
index 0000000..5b6a435
--- /dev/null
+++ b/LayoutTests/http/tests/resourceLoadStatistics/standalone-web-application-exempt-from-website-data-deletion-expected.txt
@@ -0,0 +1,15 @@
+Check that non-cookie website data does not get removed after a period of no user interaction if the website is a standalone web application.
+
+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/standalone-web-application-exempt-from-website-data-deletion.html b/LayoutTests/http/tests/resourceLoadStatistics/standalone-web-application-exempt-from-website-data-deletion.html
new file mode 100644
index 0000000..db844c0
--- /dev/null
+++ b/LayoutTests/http/tests/resourceLoadStatistics/standalone-web-application-exempt-from-website-data-deletion.html
@@ -0,0 +1,254 @@
+<!-- webkit-test-runner [ standaloneWebApplicationURL=http://127.0.0.1 ] -->
+<!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 non-cookie website data does not get removed after a period of no user interaction if the website is a standalone web application.</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.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/Source/WebKit/ChangeLog b/Source/WebKit/ChangeLog
index 150fb0d..4adb1c8 100644
--- a/Source/WebKit/ChangeLog
+++ b/Source/WebKit/ChangeLog
@@ -1,3 +1,54 @@
+2020-04-02  John Wilander  <wilander@apple.com>
+
+        Add SPI to configure WebsiteDataStores with a URL for standalone web applications and use it to disable first-party website data removal in ITP
+        https://bugs.webkit.org/show_bug.cgi?id=209634
+        <rdar://problem/60943970>
+
+        Reviewed by Alex Christensen.
+
+        This change adds a new property to _WKWebsiteDataStoreConfiguration.h called
+        standaloneApplicationURL with which the hosting application can inform the
+        website data store that it's running as a standalone web application.
+
+        This change also forwards an existing standaloneApplicationURL as a
+        WebCore::RegistrableDomain into ITP so that explicit exemptions can be made
+        to first parties of standalone web applications. The exemptions made here
+        all for all of ITP's website data removal. This part of the change is
+        covered by the new layout tests.
+
+        Tests: http/tests/resourceLoadStatistics/standalone-web-application-exempt-from-website-data-deletion-database.html
+               http/tests/resourceLoadStatistics/standalone-web-application-exempt-from-website-data-deletion.html
+
+        * NetworkProcess/Classifier/ResourceLoadStatisticsDatabaseStore.cpp:
+        * NetworkProcess/Classifier/ResourceLoadStatisticsMemoryStore.cpp:
+        (WebKit::ResourceLoadStatisticsMemoryStore::registrableDomainsToDeleteOrRestrictWebsiteDataFor):
+        * NetworkProcess/Classifier/ResourceLoadStatisticsStore.h:
+        (WebKit::ResourceLoadStatisticsStore::setStandaloneApplicationDomain):
+        (WebKit::ResourceLoadStatisticsStore::standaloneApplicationDomain const):
+        * NetworkProcess/Classifier/WebResourceLoadStatisticsStore.cpp:
+        (WebKit::WebResourceLoadStatisticsStore::setStandaloneApplicationDomain):
+        * NetworkProcess/Classifier/WebResourceLoadStatisticsStore.h:
+        * NetworkProcess/NetworkSession.cpp:
+        (WebKit::NetworkSession::NetworkSession):
+        (WebKit::NetworkSession::forwardResourceLoadStatisticsSettings):
+        * NetworkProcess/NetworkSession.h:
+        * Shared/ResourceLoadStatisticsParameters.h:
+        (WebKit::ResourceLoadStatisticsParameters::encode const):
+        (WebKit::ResourceLoadStatisticsParameters::decode):
+        * UIProcess/API/Cocoa/_WKWebsiteDataStoreConfiguration.h:
+        * UIProcess/API/Cocoa/_WKWebsiteDataStoreConfiguration.mm:
+        (-[_WKWebsiteDataStoreConfiguration standaloneApplicationURL]):
+        (-[_WKWebsiteDataStoreConfiguration setStandaloneApplication:]):
+        * UIProcess/WebProcessPool.cpp:
+        (WebKit::WebProcessPool::ensureNetworkProcess):
+        * UIProcess/WebsiteData/Cocoa/WebsiteDataStoreCocoa.mm:
+        (WebKit::WebsiteDataStore::platformSetNetworkParameters):
+        * UIProcess/WebsiteData/WebsiteDataStoreConfiguration.cpp:
+        (WebKit::WebsiteDataStoreConfiguration::copy const):
+        * UIProcess/WebsiteData/WebsiteDataStoreConfiguration.h:
+        (WebKit::WebsiteDataStoreConfiguration::standaloneApplicationURL const):
+        (WebKit::WebsiteDataStoreConfiguration::setStandaloneApplicationURL):
+
 2020-04-02  Per Arne Vollan  <pvollan@apple.com>
 
         Unreviewed build fix after r259396.
diff --git a/Source/WebKit/NetworkProcess/Classifier/ResourceLoadStatisticsDatabaseStore.cpp b/Source/WebKit/NetworkProcess/Classifier/ResourceLoadStatisticsDatabaseStore.cpp
index 72db99b..618c924 100644
--- a/Source/WebKit/NetworkProcess/Classifier/ResourceLoadStatisticsDatabaseStore.cpp
+++ b/Source/WebKit/NetworkProcess/Classifier/ResourceLoadStatisticsDatabaseStore.cpp
@@ -2454,6 +2454,8 @@
     Vector<DomainData> domains = this->domains();
     Vector<unsigned> domainIDsToClearGrandfathering;
     for (auto& statistic : domains) {
+        if (statistic.registrableDomain == standaloneApplicationDomain())
+            continue;
         oldestUserInteraction = std::min(oldestUserInteraction, statistic.mostRecentUserInteractionTime);
         if (shouldRemoveAllWebsiteDataFor(statistic, shouldCheckForGrandfathering)) {
             toDeleteOrRestrictFor.domainsToDeleteAllCookiesFor.append(statistic.registrableDomain);
diff --git a/Source/WebKit/NetworkProcess/Classifier/ResourceLoadStatisticsMemoryStore.cpp b/Source/WebKit/NetworkProcess/Classifier/ResourceLoadStatisticsMemoryStore.cpp
index 05280f1..a438c02 100644
--- a/Source/WebKit/NetworkProcess/Classifier/ResourceLoadStatisticsMemoryStore.cpp
+++ b/Source/WebKit/NetworkProcess/Classifier/ResourceLoadStatisticsMemoryStore.cpp
@@ -974,6 +974,8 @@
     auto oldestUserInteraction = now;
     RegistrableDomainsToDeleteOrRestrictWebsiteDataFor toDeleteOrRestrictFor;
     for (auto& statistic : m_resourceStatisticsMap.values()) {
+        if (statistic.registrableDomain == standaloneApplicationDomain())
+            continue;
         oldestUserInteraction = std::min(oldestUserInteraction, statistic.mostRecentUserInteractionTime);
         if (shouldRemoveAllWebsiteDataFor(statistic, shouldCheckForGrandfathering)) {
             toDeleteOrRestrictFor.domainsToDeleteAllCookiesFor.append(statistic.registrableDomain);
diff --git a/Source/WebKit/NetworkProcess/Classifier/ResourceLoadStatisticsStore.h b/Source/WebKit/NetworkProcess/Classifier/ResourceLoadStatisticsStore.h
index 47814f3..a238af6 100644
--- a/Source/WebKit/NetworkProcess/Classifier/ResourceLoadStatisticsStore.h
+++ b/Source/WebKit/NetworkProcess/Classifier/ResourceLoadStatisticsStore.h
@@ -168,6 +168,7 @@
     void setSameSiteStrictEnforcementEnabled(WebCore::SameSiteStrictEnforcementEnabled enabled) { m_sameSiteStrictEnforcementEnabled = enabled; };
     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); }
 
     virtual bool areAllThirdPartyCookiesBlockedUnder(const TopFrameDomain&) = 0;
     virtual void hasStorageAccess(const SubFrameDomain&, const TopFrameDomain&, Optional<WebCore::FrameIdentifier>, WebCore::PageIdentifier, CompletionHandler<void(bool)>&&) = 0;
@@ -245,6 +246,7 @@
     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; }
 
     static constexpr unsigned maxNumberOfRecursiveCallsInRedirectTraceBack { 50 };
     
@@ -285,6 +287,7 @@
     bool m_dataRecordsBeingRemoved { false };
     ShouldIncludeLocalhost m_shouldIncludeLocalhost { ShouldIncludeLocalhost::Yes };
     WebCore::FirstPartyWebsiteDataRemovalMode m_firstPartyWebsiteDataRemovalMode { WebCore::FirstPartyWebsiteDataRemovalMode::AllButCookies };
+    RegistrableDomain m_standaloneApplicationDomain;
 };
 
 } // namespace WebKit
diff --git a/Source/WebKit/NetworkProcess/Classifier/WebResourceLoadStatisticsStore.cpp b/Source/WebKit/NetworkProcess/Classifier/WebResourceLoadStatisticsStore.cpp
index 08ede6c..6d9cd9f 100644
--- a/Source/WebKit/NetworkProcess/Classifier/WebResourceLoadStatisticsStore.cpp
+++ b/Source/WebKit/NetworkProcess/Classifier/WebResourceLoadStatisticsStore.cpp
@@ -635,6 +635,24 @@
     });
 }
 
+void WebResourceLoadStatisticsStore::setStandaloneApplicationDomain(const RegistrableDomain& domain, CompletionHandler<void()>&& completionHandler)
+{
+    ASSERT(RunLoop::isMain());
+
+    if (isEphemeral()) {
+        completionHandler();
+        return;
+    }
+
+    postTask([this, domain = domain.isolatedCopy(), completionHandler = WTFMove(completionHandler)]() mutable {
+        if (m_statisticsStore)
+            m_statisticsStore->setStandaloneApplicationDomain(WTFMove(domain));
+        postTaskReply([completionHandler = WTFMove(completionHandler)]() mutable {
+            completionHandler();
+        });
+    });
+}
+
 void WebResourceLoadStatisticsStore::didCreateNetworkProcess()
 {
     ASSERT(RunLoop::isMain());
diff --git a/Source/WebKit/NetworkProcess/Classifier/WebResourceLoadStatisticsStore.h b/Source/WebKit/NetworkProcess/Classifier/WebResourceLoadStatisticsStore.h
index 35b8303..3030a4d 100644
--- a/Source/WebKit/NetworkProcess/Classifier/WebResourceLoadStatisticsStore.h
+++ b/Source/WebKit/NetworkProcess/Classifier/WebResourceLoadStatisticsStore.h
@@ -277,6 +277,7 @@
     void setThirdPartyCookieBlockingMode(WebCore::ThirdPartyCookieBlockingMode);
     void setSameSiteStrictEnforcementEnabled(WebCore::SameSiteStrictEnforcementEnabled);
     void setFirstPartyWebsiteDataRemovalMode(WebCore::FirstPartyWebsiteDataRemovalMode, CompletionHandler<void()>&&);
+    void setStandaloneApplicationDomain(const RegistrableDomain&, CompletionHandler<void()>&&);
     void didCreateNetworkProcess();
 
     void notifyResourceLoadStatisticsProcessed();
diff --git a/Source/WebKit/NetworkProcess/NetworkSession.cpp b/Source/WebKit/NetworkProcess/NetworkSession.cpp
index 330427b..7524e48 100644
--- a/Source/WebKit/NetworkProcess/NetworkSession.cpp
+++ b/Source/WebKit/NetworkProcess/NetworkSession.cpp
@@ -89,6 +89,7 @@
     , m_thirdPartyCookieBlockingMode(parameters.resourceLoadStatisticsParameters.thirdPartyCookieBlockingMode)
     , m_sameSiteStrictEnforcementEnabled(parameters.resourceLoadStatisticsParameters.sameSiteStrictEnforcementEnabled)
     , m_firstPartyWebsiteDataRemovalMode(parameters.resourceLoadStatisticsParameters.firstPartyWebsiteDataRemovalMode)
+    , m_standaloneApplicationDomain(parameters.resourceLoadStatisticsParameters.standaloneApplicationDomain)
 #endif
     , m_adClickAttribution(makeUniqueRef<AdClickAttributionManager>(networkProcess, parameters.sessionID))
     , m_testSpeedMultiplier(parameters.testSpeedMultiplier)
@@ -204,6 +205,7 @@
     m_resourceLoadStatistics->setThirdPartyCookieBlockingMode(m_thirdPartyCookieBlockingMode);
     m_resourceLoadStatistics->setSameSiteStrictEnforcementEnabled(m_sameSiteStrictEnforcementEnabled);
     m_resourceLoadStatistics->setFirstPartyWebsiteDataRemovalMode(m_firstPartyWebsiteDataRemovalMode, [] { });
+    m_resourceLoadStatistics->setStandaloneApplicationDomain(m_standaloneApplicationDomain, [] { });
 }
 
 bool NetworkSession::isResourceLoadStatisticsEnabled() const
diff --git a/Source/WebKit/NetworkProcess/NetworkSession.h b/Source/WebKit/NetworkProcess/NetworkSession.h
index 4634293..099ce74 100644
--- a/Source/WebKit/NetworkProcess/NetworkSession.h
+++ b/Source/WebKit/NetworkProcess/NetworkSession.h
@@ -164,6 +164,7 @@
     WebCore::ThirdPartyCookieBlockingMode m_thirdPartyCookieBlockingMode { WebCore::ThirdPartyCookieBlockingMode::All };
     WebCore::SameSiteStrictEnforcementEnabled m_sameSiteStrictEnforcementEnabled { WebCore::SameSiteStrictEnforcementEnabled::No };
     WebCore::FirstPartyWebsiteDataRemovalMode m_firstPartyWebsiteDataRemovalMode { WebCore::FirstPartyWebsiteDataRemovalMode::AllButCookies };
+    WebCore::RegistrableDomain m_standaloneApplicationDomain;
 #endif
     bool m_isStaleWhileRevalidateEnabled { false };
     UniqueRef<AdClickAttributionManager> m_adClickAttribution;
diff --git a/Source/WebKit/Shared/ResourceLoadStatisticsParameters.h b/Source/WebKit/Shared/ResourceLoadStatisticsParameters.h
index f497609..927aec3 100644
--- a/Source/WebKit/Shared/ResourceLoadStatisticsParameters.h
+++ b/Source/WebKit/Shared/ResourceLoadStatisticsParameters.h
@@ -47,6 +47,7 @@
     WebCore::SameSiteStrictEnforcementEnabled sameSiteStrictEnforcementEnabled { WebCore::SameSiteStrictEnforcementEnabled::No };
 #endif
     WebCore::FirstPartyWebsiteDataRemovalMode firstPartyWebsiteDataRemovalMode { WebCore::FirstPartyWebsiteDataRemovalMode::AllButCookies };
+    WebCore::RegistrableDomain standaloneApplicationDomain { };
     WebCore::RegistrableDomain manualPrevalentResource { };
     
     void encode(IPC::Encoder& encoder) const
@@ -63,6 +64,7 @@
         encoder << sameSiteStrictEnforcementEnabled;
 #endif
         encoder << firstPartyWebsiteDataRemovalMode;
+        encoder << standaloneApplicationDomain;
         encoder << manualPrevalentResource;
     }
 
@@ -120,6 +122,11 @@
         if (!firstPartyWebsiteDataRemovalMode)
             return WTF::nullopt;
 
+        Optional<WebCore::RegistrableDomain> standaloneApplicationDomain;
+        decoder >> standaloneApplicationDomain;
+        if (!standaloneApplicationDomain)
+            return WTF::nullopt;
+
         Optional<WebCore::RegistrableDomain> manualPrevalentResource;
         decoder >> manualPrevalentResource;
         if (!manualPrevalentResource)
@@ -138,6 +145,7 @@
             WTFMove(*sameSiteStrictEnforcementEnabled),
 #endif
             WTFMove(*firstPartyWebsiteDataRemovalMode),
+            WTFMove(*standaloneApplicationDomain),
             WTFMove(*manualPrevalentResource),
         }};
     }
diff --git a/Source/WebKit/UIProcess/API/Cocoa/_WKWebsiteDataStoreConfiguration.h b/Source/WebKit/UIProcess/API/Cocoa/_WKWebsiteDataStoreConfiguration.h
index eeb481c..bae7bee 100644
--- a/Source/WebKit/UIProcess/API/Cocoa/_WKWebsiteDataStoreConfiguration.h
+++ b/Source/WebKit/UIProcess/API/Cocoa/_WKWebsiteDataStoreConfiguration.h
@@ -74,6 +74,8 @@
 
 @property (nonatomic, nullable, copy) NSURL *alternativeServicesStorageDirectory WK_API_AVAILABLE(macos(WK_MAC_TBA), ios(WK_IOS_TBA));
 
+@property (nonatomic, nullable, copy) NSURL *standaloneApplicationURL WK_API_AVAILABLE(macos(WK_MAC_TBA), ios(WK_IOS_TBA));
+
 // Testing only.
 @property (nonatomic) BOOL allLoadsBlockedByDeviceManagementRestrictionsForTesting WK_API_AVAILABLE(macos(10.15), ios(13.0));
 
diff --git a/Source/WebKit/UIProcess/API/Cocoa/_WKWebsiteDataStoreConfiguration.mm b/Source/WebKit/UIProcess/API/Cocoa/_WKWebsiteDataStoreConfiguration.mm
index b75a026..ebc0ee1 100644
--- a/Source/WebKit/UIProcess/API/Cocoa/_WKWebsiteDataStoreConfiguration.mm
+++ b/Source/WebKit/UIProcess/API/Cocoa/_WKWebsiteDataStoreConfiguration.mm
@@ -423,6 +423,16 @@
     _configuration->setProxyConfiguration((__bridge CFDictionaryRef)[configuration copy]);
 }
 
+- (NSURL *)standaloneApplicationURL
+{
+    return _configuration->standaloneApplicationURL();
+}
+
+- (void)setStandaloneApplicationURL:(NSURL *)url
+{
+    _configuration->setStandaloneApplicationURL(url);
+}
+
 - (BOOL)allLoadsBlockedByDeviceManagementRestrictionsForTesting
 {
     return _configuration->allLoadsBlockedByDeviceManagementRestrictionsForTesting();
diff --git a/Source/WebKit/UIProcess/WebProcessPool.cpp b/Source/WebKit/UIProcess/WebProcessPool.cpp
index 295a49e..b8fe9b7 100644
--- a/Source/WebKit/UIProcess/WebProcessPool.cpp
+++ b/Source/WebKit/UIProcess/WebProcessPool.cpp
@@ -599,6 +599,7 @@
 #if ENABLE(RESOURCE_LOAD_STATISTICS)
     WebCore::ThirdPartyCookieBlockingMode thirdPartyCookieBlockingMode = WebCore::ThirdPartyCookieBlockingMode::All;
 #endif
+    WebCore::RegistrableDomain standaloneApplicationDomain { };
     WebCore::FirstPartyWebsiteDataRemovalMode firstPartyWebsiteDataRemovalMode = WebCore::FirstPartyWebsiteDataRemovalMode::AllButCookies;
     WebCore::SameSiteStrictEnforcementEnabled sameSiteStrictEnforcementEnabled = WebCore::SameSiteStrictEnforcementEnabled::No;
     WebCore::RegistrableDomain manualPrevalentResource { };
@@ -617,6 +618,7 @@
             sameSiteStrictEnforcementEnabled = networkSessionParameters.resourceLoadStatisticsParameters.sameSiteStrictEnforcementEnabled;
 #endif
             firstPartyWebsiteDataRemovalMode = networkSessionParameters.resourceLoadStatisticsParameters.firstPartyWebsiteDataRemovalMode;
+            standaloneApplicationDomain = networkSessionParameters.resourceLoadStatisticsParameters.standaloneApplicationDomain;
             manualPrevalentResource = networkSessionParameters.resourceLoadStatisticsParameters.manualPrevalentResource;
         }
 
@@ -642,6 +644,7 @@
             sameSiteStrictEnforcementEnabled = networkSessionParameters.resourceLoadStatisticsParameters.sameSiteStrictEnforcementEnabled;
 #endif
             firstPartyWebsiteDataRemovalMode = networkSessionParameters.resourceLoadStatisticsParameters.firstPartyWebsiteDataRemovalMode;
+            standaloneApplicationDomain = networkSessionParameters.resourceLoadStatisticsParameters.standaloneApplicationDomain;
             manualPrevalentResource = networkSessionParameters.resourceLoadStatisticsParameters.manualPrevalentResource;
         }
 
diff --git a/Source/WebKit/UIProcess/WebsiteData/Cocoa/WebsiteDataStoreCocoa.mm b/Source/WebKit/UIProcess/WebsiteData/Cocoa/WebsiteDataStoreCocoa.mm
index a70b7d5..ecdf8df 100644
--- a/Source/WebKit/UIProcess/WebsiteData/Cocoa/WebsiteDataStoreCocoa.mm
+++ b/Source/WebKit/UIProcess/WebsiteData/Cocoa/WebsiteDataStoreCocoa.mm
@@ -180,6 +180,7 @@
     parameters.networkSessionParameters.resourceLoadStatisticsParameters.enableDebugMode = enableResourceLoadStatisticsDebugMode;
     parameters.networkSessionParameters.resourceLoadStatisticsParameters.sameSiteStrictEnforcementEnabled = sameSiteStrictEnforcementEnabled;
     parameters.networkSessionParameters.resourceLoadStatisticsParameters.firstPartyWebsiteDataRemovalMode = firstPartyWebsiteDataRemovalMode;
+    parameters.networkSessionParameters.resourceLoadStatisticsParameters.standaloneApplicationDomain = WebCore::RegistrableDomain { m_configuration->standaloneApplicationURL() };
     parameters.networkSessionParameters.resourceLoadStatisticsParameters.manualPrevalentResource = WTFMove(resourceLoadStatisticsManualPrevalentResource);
 
     auto cookieFile = resolvedCookieStorageFile();
diff --git a/Source/WebKit/UIProcess/WebsiteData/WebsiteDataStoreConfiguration.cpp b/Source/WebKit/UIProcess/WebsiteData/WebsiteDataStoreConfiguration.cpp
index 71e7627..0fb3a10 100644
--- a/Source/WebKit/UIProcess/WebsiteData/WebsiteDataStoreConfiguration.cpp
+++ b/Source/WebKit/UIProcess/WebsiteData/WebsiteDataStoreConfiguration.cpp
@@ -91,6 +91,7 @@
     copy->m_testSpeedMultiplier = this->m_testSpeedMultiplier;
     copy->m_suppressesConnectionTerminationOnSystemChange = this->m_suppressesConnectionTerminationOnSystemChange;
     copy->m_allowsServerPreconnect = this->m_allowsServerPreconnect;
+    copy->m_standaloneApplicationURL = this->m_standaloneApplicationURL;
 #if PLATFORM(COCOA)
     if (m_proxyConfiguration)
         copy->m_proxyConfiguration = adoptCF(CFDictionaryCreateCopy(nullptr, this->m_proxyConfiguration.get()));
diff --git a/Source/WebKit/UIProcess/WebsiteData/WebsiteDataStoreConfiguration.h b/Source/WebKit/UIProcess/WebsiteData/WebsiteDataStoreConfiguration.h
index 3e345b3..15fbe7e 100644
--- a/Source/WebKit/UIProcess/WebsiteData/WebsiteDataStoreConfiguration.h
+++ b/Source/WebKit/UIProcess/WebsiteData/WebsiteDataStoreConfiguration.h
@@ -153,6 +153,10 @@
     bool allowsServerPreconnect() const { return m_allowsServerPreconnect; }
     void setAllowsServerPreconnect(bool allows) { m_allowsServerPreconnect = allows; }
 
+    
+    const URL& standaloneApplicationURL() const { return m_standaloneApplicationURL; }
+    void setStandaloneApplicationURL(URL&& url) { m_standaloneApplicationURL = WTFMove(url); }
+
 private:
     IsPersistent m_isPersistent { IsPersistent::No };
 
@@ -195,6 +199,7 @@
     bool m_suppressesConnectionTerminationOnSystemChange { false };
     bool m_allowsServerPreconnect { true };
     unsigned m_testSpeedMultiplier { 1 };
+    URL m_standaloneApplicationURL;
 #if PLATFORM(COCOA)
     RetainPtr<CFDictionaryRef> m_proxyConfiguration;
 #endif
diff --git a/Tools/ChangeLog b/Tools/ChangeLog
index 3ae8d4a..328a4f5 100644
--- a/Tools/ChangeLog
+++ b/Tools/ChangeLog
@@ -1,3 +1,23 @@
+2020-04-02  John Wilander  <wilander@apple.com>
+
+        Add SPI to configure WebsiteDataStores with a URL for standalone web applications and use it to disable first-party website data removal in ITP
+        https://bugs.webkit.org/show_bug.cgi?id=209634
+        <rdar://problem/60943970>
+
+        Reviewed by Alex Christensen.
+
+        Added a new test option called standaloneWebApplicationURL so that layout tests can
+        configure the website data store accordingly. Picking it up and using it requires
+        creating a new website data store with a configuration that has the standalone web
+        application URL.
+
+        * WebKitTestRunner/TestController.cpp:
+        (WTR::parseStringTestHeaderValueAsURL):
+        (WTR::updateTestOptionsFromTestHeader):
+        * WebKitTestRunner/TestOptions.h:
+        * WebKitTestRunner/cocoa/TestControllerCocoa.mm:
+        (WTR::TestController::platformCreateWebView):
+
 2020-04-02  David Kilzer  <ddkilzer@apple.com>
 
         REGRESSION (r234685): Leak of CALayer in createCoreAnimationLayer() in PluginObjectMac.mm
diff --git a/Tools/WebKitTestRunner/TestController.cpp b/Tools/WebKitTestRunner/TestController.cpp
index 77a6443..ed7002a 100644
--- a/Tools/WebKitTestRunner/TestController.cpp
+++ b/Tools/WebKitTestRunner/TestController.cpp
@@ -1359,6 +1359,11 @@
     return toSTD(adoptWK(WKURLCopyPath(relativeURL.get())));
 }
 
+static std::string parseStringTestHeaderValueAsURL(const std::string& value)
+{
+    return toSTD(adoptWK(WKURLCopyString(createTestURL(value.c_str()))));
+}
+
 static void updateTestOptionsFromTestHeader(TestOptions& testOptions, const std::string& pathOrURL, const std::string& absolutePath)
 {
     std::string filename = absolutePath;
@@ -1512,7 +1517,9 @@
             testOptions.enableCaptureAudioInGPUProcess = parseBooleanTestHeaderValue(value);
         else if (key == "allowTopNavigationToDataURLs")
             testOptions.allowTopNavigationToDataURLs = parseBooleanTestHeaderValue(value);
-        
+        else if (key == "standaloneWebApplicationURL")
+            testOptions.standaloneWebApplicationURL = parseStringTestHeaderValueAsURL(value);
+
         pairStart = pairEnd + 1;
     }
 }
diff --git a/Tools/WebKitTestRunner/TestOptions.h b/Tools/WebKitTestRunner/TestOptions.h
index b94f8f7..7bd076c 100644
--- a/Tools/WebKitTestRunner/TestOptions.h
+++ b/Tools/WebKitTestRunner/TestOptions.h
@@ -110,6 +110,7 @@
     std::string applicationManifest;
     std::string jscOptions;
     std::string additionalSupportedImageTypes;
+    std::string standaloneWebApplicationURL;
     HashMap<String, bool> experimentalFeatures;
     HashMap<String, bool> internalDebugFeatures;
     String contentMode;
@@ -168,7 +169,8 @@
             || enableCaptureVideoInUIProcess != options.enableCaptureVideoInUIProcess
             || enableCaptureVideoInGPUProcess != options.enableCaptureVideoInGPUProcess
             || enableCaptureAudioInGPUProcess != options.enableCaptureAudioInGPUProcess
-            || allowTopNavigationToDataURLs != options.allowTopNavigationToDataURLs)
+            || allowTopNavigationToDataURLs != options.allowTopNavigationToDataURLs
+            || standaloneWebApplicationURL != options.standaloneWebApplicationURL)
             return false;
 
         if (!contextOptions.hasSameInitializationOptions(options.contextOptions))
diff --git a/Tools/WebKitTestRunner/cocoa/TestControllerCocoa.mm b/Tools/WebKitTestRunner/cocoa/TestControllerCocoa.mm
index 548fe73..033498c 100644
--- a/Tools/WebKitTestRunner/cocoa/TestControllerCocoa.mm
+++ b/Tools/WebKitTestRunner/cocoa/TestControllerCocoa.mm
@@ -51,6 +51,7 @@
 #import <WebKit/_WKApplicationManifest.h>
 #import <WebKit/_WKUserContentExtensionStore.h>
 #import <WebKit/_WKUserContentExtensionStorePrivate.h>
+#import <WebKit/_WKWebsiteDataStoreConfiguration.h>
 #import <wtf/MainThread.h>
 #import <wtf/spi/cocoa/SecuritySPI.h>
 
@@ -149,10 +150,13 @@
     if (options.enableEditableImages)
         [copiedConfiguration _setEditableImagesEnabled:YES];
 
-    if (options.useEphemeralSession) {
-        auto ephemeralWebsiteDataStore = [WKWebsiteDataStore nonPersistentDataStore];
-        [ephemeralWebsiteDataStore _setResourceLoadStatisticsEnabled:YES];
-        [copiedConfiguration setWebsiteDataStore:ephemeralWebsiteDataStore];
+    if (options.useEphemeralSession || options.standaloneWebApplicationURL.length()) {
+        auto websiteDataStoreConfig = options.useEphemeralSession ? [[[_WKWebsiteDataStoreConfiguration alloc] initNonPersistentConfiguration] autorelease] : [[[_WKWebsiteDataStoreConfiguration alloc] init] autorelease];
+        if (options.standaloneWebApplicationURL.length())
+            [websiteDataStoreConfig setStandaloneApplicationURL:[NSURL URLWithString:[NSString stringWithUTF8String:options.standaloneWebApplicationURL.c_str()]]];
+        auto websiteDataStore = [[[WKWebsiteDataStore alloc] _initWithConfiguration:websiteDataStoreConfig] autorelease];
+        [websiteDataStore _setResourceLoadStatisticsEnabled:YES];
+        [copiedConfiguration setWebsiteDataStore:websiteDataStore];
     }
 
     [copiedConfiguration _setAllowTopNavigationToDataURLs:options.allowTopNavigationToDataURLs];