Pick up Ad Click Attribution conversions in NetworkResourceLoader::willSendRedirected...
authorwilander@apple.com <wilander@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Tue, 9 Apr 2019 18:19:59 +0000 (18:19 +0000)
committerwilander@apple.com <wilander@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Tue, 9 Apr 2019 18:19:59 +0000 (18:19 +0000)
https://bugs.webkit.org/show_bug.cgi?id=196558
<rdar://problem/47650245>

Reviewed by Youenn Fablet.

Source/WebCore:

Tests: http/tests/adClickAttribution/attribution-conversion-through-cross-site-image-redirect.html
       http/tests/adClickAttribution/attribution-conversion-through-image-redirect-with-priority.html
       http/tests/adClickAttribution/attribution-conversion-through-image-redirect-without-priority.html

The existing API tests were expanded too.

* html/HTMLAnchorElement.cpp:
(WebCore::HTMLAnchorElement::parseAdClickAttribution const):
   Enhanced the use of AdClickAttribution::MaxEntropy.
* loader/AdClickAttribution.cpp:
(WebCore::AdClickAttribution::parseConversionRequest):
    New function to parse and validate URLs with a path starting with
    /.well-known/ad-click-attribution/.
(WebCore::AdClickAttribution::toString const):
    Added output for the conversion priority for testing purposes.
* loader/AdClickAttribution.h:
(WebCore::AdClickAttribution::Campaign::isValid const):
(WebCore::AdClickAttribution::Conversion::isValid const):
   Enhanced the use of AdClickAttribution::MaxEntropy.

Source/WebKit:

So called pixel requests have traditionally been used to send ad click
attribution data to click sources. The privacy implications of such
pixel requests are severe which is in part why browsers have started to
block cookies from being sent in such third-party requests.

To allow for a smooth transition to more privacy-friendly ad click
attribution, we should allow servers to make a redirect to
https://click-source.example/.well-known/ad-click-attribution/ to
trigger a so called conversion.

This patch checks for the well-known location in the path component of
the redirect URL. If the request indeed goes to the well-known location,
we parse the conversion data and send it to the storage in the network
session.

* NetworkProcess/NetworkAdClickAttribution.cpp:
(WebKit::NetworkAdClickAttribution::convert):
    Reporting function.
* NetworkProcess/NetworkAdClickAttribution.h:
* NetworkProcess/NetworkResourceLoader.cpp:
(WebKit::NetworkResourceLoader::willSendRedirectedRequest):
    Now checks for the well-known location through a call to
    WebCore::AdClickAttribution::parseConversionRequest().
* NetworkProcess/NetworkSession.cpp:
(WebKit::NetworkSession::convertAdClickAttribution):
    Piping to WebKit::NetworkAdClickAttribution::convert().
* NetworkProcess/NetworkSession.h:

Tools:

* TestWebKitAPI/Tests/WebCore/AdClickAttribution.cpp:
(TestWebKitAPI::TEST):
    Added tests of WebCore::AdClickAttribution::parseConversionRequest().

LayoutTests:

* http/tests/adClickAttribution/anchor-tag-attributes-validation-expected.txt:
   Enhanced the use of AdClickAttribution::MaxEntropy.
* http/tests/adClickAttribution/attribution-conversion-through-cross-site-image-redirect-expected.txt: Added.
* http/tests/adClickAttribution/attribution-conversion-through-cross-site-image-redirect.html: Added.
* http/tests/adClickAttribution/attribution-conversion-through-image-redirect-with-priority-expected.txt: Added.
* http/tests/adClickAttribution/attribution-conversion-through-image-redirect-with-priority.html: Added.
* http/tests/adClickAttribution/attribution-conversion-through-image-redirect-without-priority-expected.txt: Added.
* http/tests/adClickAttribution/attribution-conversion-through-image-redirect-without-priority.html: Added.
* http/tests/adClickAttribution/resources/redirectToConversion.php: Added.
* http/tests/adClickAttribution/resources/redirectToConversionOnIPAddress.php: Added.
* platform/ios-wk2/http/tests/adClickAttribution/anchor-tag-attributes-validation-expected.txt:
   Enhanced the use of AdClickAttribution::MaxEntropy.

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

23 files changed:
LayoutTests/ChangeLog
LayoutTests/http/tests/adClickAttribution/anchor-tag-attributes-validation-expected.txt
LayoutTests/http/tests/adClickAttribution/attribution-conversion-through-cross-site-image-redirect-expected.txt [new file with mode: 0644]
LayoutTests/http/tests/adClickAttribution/attribution-conversion-through-cross-site-image-redirect.html [new file with mode: 0644]
LayoutTests/http/tests/adClickAttribution/attribution-conversion-through-image-redirect-with-priority-expected.txt [new file with mode: 0644]
LayoutTests/http/tests/adClickAttribution/attribution-conversion-through-image-redirect-with-priority.html [new file with mode: 0644]
LayoutTests/http/tests/adClickAttribution/attribution-conversion-through-image-redirect-without-priority-expected.txt [new file with mode: 0644]
LayoutTests/http/tests/adClickAttribution/attribution-conversion-through-image-redirect-without-priority.html [new file with mode: 0644]
LayoutTests/http/tests/adClickAttribution/resources/redirectToConversion.php [new file with mode: 0644]
LayoutTests/http/tests/adClickAttribution/resources/redirectToConversionOnIPAddress.php [new file with mode: 0644]
LayoutTests/platform/ios-wk2/http/tests/adClickAttribution/anchor-tag-attributes-validation-expected.txt
Source/WebCore/ChangeLog
Source/WebCore/html/HTMLAnchorElement.cpp
Source/WebCore/loader/AdClickAttribution.cpp
Source/WebCore/loader/AdClickAttribution.h
Source/WebKit/ChangeLog
Source/WebKit/NetworkProcess/NetworkAdClickAttribution.cpp
Source/WebKit/NetworkProcess/NetworkAdClickAttribution.h
Source/WebKit/NetworkProcess/NetworkResourceLoader.cpp
Source/WebKit/NetworkProcess/NetworkSession.cpp
Source/WebKit/NetworkProcess/NetworkSession.h
Tools/ChangeLog
Tools/TestWebKitAPI/Tests/WebCore/AdClickAttribution.cpp

index 8b9e98e..3b09e10 100644 (file)
@@ -1,3 +1,24 @@
+2019-04-09  John Wilander  <wilander@apple.com>
+
+        Pick up Ad Click Attribution conversions in NetworkResourceLoader::willSendRedirectedRequest()
+        https://bugs.webkit.org/show_bug.cgi?id=196558
+        <rdar://problem/47650245>
+
+        Reviewed by Youenn Fablet.
+
+        * http/tests/adClickAttribution/anchor-tag-attributes-validation-expected.txt:
+           Enhanced the use of AdClickAttribution::MaxEntropy.
+        * http/tests/adClickAttribution/attribution-conversion-through-cross-site-image-redirect-expected.txt: Added.
+        * http/tests/adClickAttribution/attribution-conversion-through-cross-site-image-redirect.html: Added.
+        * http/tests/adClickAttribution/attribution-conversion-through-image-redirect-with-priority-expected.txt: Added.
+        * http/tests/adClickAttribution/attribution-conversion-through-image-redirect-with-priority.html: Added.
+        * http/tests/adClickAttribution/attribution-conversion-through-image-redirect-without-priority-expected.txt: Added.
+        * http/tests/adClickAttribution/attribution-conversion-through-image-redirect-without-priority.html: Added.
+        * http/tests/adClickAttribution/resources/redirectToConversion.php: Added.
+        * http/tests/adClickAttribution/resources/redirectToConversionOnIPAddress.php: Added.
+        * platform/ios-wk2/http/tests/adClickAttribution/anchor-tag-attributes-validation-expected.txt:
+           Enhanced the use of AdClickAttribution::MaxEntropy.
+
 2019-04-09  Shawn Roberts  <sroberts@apple.com>
 
         inspector/canvas/css-canvas-clients.html is a flaky failure
index 69af4ad..32368b0 100644 (file)
@@ -1,5 +1,5 @@
-CONSOLE MESSAGE: line 165: adcampaignid must have a non-negative value less than 64 for Ad Click Attribution.
-CONSOLE MESSAGE: line 165: adcampaignid must have a non-negative value less than 64 for Ad Click Attribution.
+CONSOLE MESSAGE: line 165: adcampaignid must have a non-negative value less than or equal to 63 for Ad Click Attribution.
+CONSOLE MESSAGE: line 165: adcampaignid must have a non-negative value less than or equal to 63 for Ad Click Attribution.
 CONSOLE MESSAGE: line 165: adcampaignid can not be converted to a non-negative integer which is required for Ad Click Attribution.
 CONSOLE MESSAGE: line 165: adcampaignid can not be converted to a non-negative integer which is required for Ad Click Attribution.
 CONSOLE MESSAGE: line 165: adcampaignid can not be converted to a non-negative integer which is required for Ad Click Attribution.
diff --git a/LayoutTests/http/tests/adClickAttribution/attribution-conversion-through-cross-site-image-redirect-expected.txt b/LayoutTests/http/tests/adClickAttribution/attribution-conversion-through-cross-site-image-redirect-expected.txt
new file mode 100644 (file)
index 0000000..5d6fc6c
--- /dev/null
@@ -0,0 +1,8 @@
+Tests that triggering of ad click attribution conversions through cross-site redirects do not work.
+
+
+WebCore::AdClickAttribution 1
+Source: 127.0.0.1
+Destination: localhost
+Campaign ID: 3
+No conversion data.
diff --git a/LayoutTests/http/tests/adClickAttribution/attribution-conversion-through-cross-site-image-redirect.html b/LayoutTests/http/tests/adClickAttribution/attribution-conversion-through-cross-site-image-redirect.html
new file mode 100644 (file)
index 0000000..3124067
--- /dev/null
@@ -0,0 +1,55 @@
+<!DOCTYPE html> <!-- webkit-test-runner [ useFlexibleViewport=true internal:AdClickAttributionEnabled=true ] -->
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
+    <script src="/js-test-resources/ui-helper.js"></script>
+</head>
+<body onload="setTimeout(runTest, 0)">
+<div id="description">Tests that triggering of ad click attribution conversions through cross-site redirects do not work.</div>
+<a id="targetLink" href="http://localhost:8000/adClickAttribution/attribution-conversion-through-cross-site-image-redirect.html?stepTwo" adcampaignid="3" addestination="http://localhost:8000">Link</a><br>
+<div id="output"></div>
+<script>
+    if (window.testRunner) {
+        testRunner.waitUntilDone();
+        testRunner.dumpAsText();
+        testRunner.setAllowsAnySSLCertificate(true);
+    }
+
+    function activateElement(elementID) {
+        var element = document.getElementById(elementID);
+        var centerX = element.offsetLeft + element.offsetWidth / 2;
+        var centerY = element.offsetTop + element.offsetHeight / 2;
+        UIHelper.activateAt(centerX, centerY).then(
+            function () {
+            },
+            function () {
+                document.getElementById("output").innerText = "FAIL Promise rejected.";
+                testRunner.notifyDone();
+            }
+        );
+    }
+
+    function runTest() {
+        if (window.testRunner) {
+            if (window.location.search === "?stepTwo") {
+                let imageElement = document.createElement("img");
+                imageElement.src = "https://localhost:8443/adClickAttribution/resources/redirectToConversionOnIPAddress.php?conversionData=12";
+                imageElement.id = "pixel";
+                imageElement.onerror = function(e) {
+                    testRunner.dumpAdClickAttribution();
+                    document.body.removeChild(document.getElementById("targetLink"));
+                    document.body.removeChild(document.getElementById("pixel"));
+                    testRunner.notifyDone();
+                };
+                document.body.appendChild(imageElement);
+            } else {
+                activateElement("targetLink");
+            }
+        } else {
+            document.getElementById("output").innerText = "FAIL No testRunner.";
+        }
+    }
+</script>
+</body>
+</html>
diff --git a/LayoutTests/http/tests/adClickAttribution/attribution-conversion-through-image-redirect-with-priority-expected.txt b/LayoutTests/http/tests/adClickAttribution/attribution-conversion-through-image-redirect-with-priority-expected.txt
new file mode 100644 (file)
index 0000000..bc271d4
--- /dev/null
@@ -0,0 +1,9 @@
+Tests triggering of ad click attribution conversions with priority.
+
+
+WebCore::AdClickAttribution 1
+Source: 127.0.0.1
+Destination: localhost
+Campaign ID: 3
+Conversion data: 12
+Conversion priority: 3
diff --git a/LayoutTests/http/tests/adClickAttribution/attribution-conversion-through-image-redirect-with-priority.html b/LayoutTests/http/tests/adClickAttribution/attribution-conversion-through-image-redirect-with-priority.html
new file mode 100644 (file)
index 0000000..c5fb5f5
--- /dev/null
@@ -0,0 +1,55 @@
+<!DOCTYPE html> <!-- webkit-test-runner [ useFlexibleViewport=true internal:AdClickAttributionEnabled=true ] -->
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
+    <script src="/js-test-resources/ui-helper.js"></script>
+</head>
+<body onload="setTimeout(runTest, 0)">
+<div id="description">Tests triggering of ad click attribution conversions with priority.</div>
+<a id="targetLink" href="http://localhost:8000/adClickAttribution/attribution-conversion-through-image-redirect-with-priority.html?stepTwo" adcampaignid="3" addestination="http://localhost:8000">Link</a><br>
+<div id="output"></div>
+<script>
+    if (window.testRunner) {
+        testRunner.waitUntilDone();
+        testRunner.dumpAsText();
+        testRunner.setAllowsAnySSLCertificate(true);
+    }
+
+    function activateElement(elementID) {
+        var element = document.getElementById(elementID);
+        var centerX = element.offsetLeft + element.offsetWidth / 2;
+        var centerY = element.offsetTop + element.offsetHeight / 2;
+        UIHelper.activateAt(centerX, centerY).then(
+            function () {
+            },
+            function () {
+                document.getElementById("output").innerText = "FAIL Promise rejected.";
+                testRunner.notifyDone();
+            }
+        );
+    }
+
+    function runTest() {
+        if (window.testRunner) {
+            if (window.location.search === "?stepTwo") {
+                let imageElement = document.createElement("img");
+                imageElement.src = "https://127.0.0.1:8443/adClickAttribution/resources/redirectToConversion.php?conversionData=12&priority=03";
+                imageElement.id = "pixel";
+                imageElement.onerror = function(e) {
+                    testRunner.dumpAdClickAttribution();
+                    document.body.removeChild(document.getElementById("targetLink"));
+                    document.body.removeChild(document.getElementById("pixel"));
+                    testRunner.notifyDone();
+                };
+                document.body.appendChild(imageElement);
+            } else {
+                activateElement("targetLink");
+            }
+        } else {
+            document.getElementById("output").innerText = "FAIL No testRunner.";
+        }
+    }
+</script>
+</body>
+</html>
diff --git a/LayoutTests/http/tests/adClickAttribution/attribution-conversion-through-image-redirect-without-priority-expected.txt b/LayoutTests/http/tests/adClickAttribution/attribution-conversion-through-image-redirect-without-priority-expected.txt
new file mode 100644 (file)
index 0000000..4c858f0
--- /dev/null
@@ -0,0 +1,9 @@
+Tests triggering of ad click attribution conversions without priority.
+
+
+WebCore::AdClickAttribution 1
+Source: 127.0.0.1
+Destination: localhost
+Campaign ID: 3
+Conversion data: 12
+Conversion priority: 0
diff --git a/LayoutTests/http/tests/adClickAttribution/attribution-conversion-through-image-redirect-without-priority.html b/LayoutTests/http/tests/adClickAttribution/attribution-conversion-through-image-redirect-without-priority.html
new file mode 100644 (file)
index 0000000..97ba52e
--- /dev/null
@@ -0,0 +1,55 @@
+<!DOCTYPE html> <!-- webkit-test-runner [ useFlexibleViewport=true internal:AdClickAttributionEnabled=true ] -->
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
+    <script src="/js-test-resources/ui-helper.js"></script>
+</head>
+<body onload="setTimeout(runTest, 0)">
+<div id="description">Tests triggering of ad click attribution conversions without priority.</div>
+<a id="targetLink" href="http://localhost:8000/adClickAttribution/attribution-conversion-through-image-redirect-without-priority.html?stepTwo" adcampaignid="3" addestination="http://localhost:8000">Link</a><br>
+<div id="output"></div>
+<script>
+    if (window.testRunner) {
+        testRunner.waitUntilDone();
+        testRunner.dumpAsText();
+        testRunner.setAllowsAnySSLCertificate(true);
+    }
+
+    function activateElement(elementID) {
+        var element = document.getElementById(elementID);
+        var centerX = element.offsetLeft + element.offsetWidth / 2;
+        var centerY = element.offsetTop + element.offsetHeight / 2;
+        UIHelper.activateAt(centerX, centerY).then(
+            function () {
+            },
+            function () {
+                document.getElementById("output").innerText = "FAIL Promise rejected.";
+                testRunner.notifyDone();
+            }
+        );
+    }
+
+    function runTest() {
+        if (window.testRunner) {
+            if (window.location.search === "?stepTwo") {
+                let imageElement = document.createElement("img");
+                imageElement.src = "https://127.0.0.1:8443/adClickAttribution/resources/redirectToConversion.php?conversionData=12";
+                imageElement.id = "pixel";
+                imageElement.onerror = function() {
+                    testRunner.dumpAdClickAttribution();
+                    document.body.removeChild(document.getElementById("targetLink"));
+                    document.body.removeChild(document.getElementById("pixel"));
+                    testRunner.notifyDone();
+                };
+                document.body.appendChild(imageElement);
+            } else {
+                activateElement("targetLink");
+            }
+        } else {
+            document.getElementById("output").innerText = "FAIL No testRunner.";
+        }
+    }
+</script>
+</body>
+</html>
diff --git a/LayoutTests/http/tests/adClickAttribution/resources/redirectToConversion.php b/LayoutTests/http/tests/adClickAttribution/resources/redirectToConversion.php
new file mode 100644 (file)
index 0000000..1ad333c
--- /dev/null
@@ -0,0 +1,8 @@
+<?php
+header("HTTP/1.0 302 Found");
+if (isset($_GET["conversionData"]) && isset($_GET["priority"])) {
+  header("Location: /.well-known/ad-click-attribution/" . $_GET["conversionData"] . "/" . $_GET["priority"]);
+} else if (isset($_GET["conversionData"])) {
+  header("Location: /.well-known/ad-click-attribution/" . $_GET["conversionData"]);
+}
+?>
diff --git a/LayoutTests/http/tests/adClickAttribution/resources/redirectToConversionOnIPAddress.php b/LayoutTests/http/tests/adClickAttribution/resources/redirectToConversionOnIPAddress.php
new file mode 100644 (file)
index 0000000..0fe1aa9
--- /dev/null
@@ -0,0 +1,8 @@
+<?php
+header("HTTP/1.0 302 Found");
+if (isset($_GET["conversionData"]) && isset($_GET["priority"])) {
+  header("Location: https://127.0.0.1:8000/.well-known/ad-click-attribution/" . $_GET["conversionData"] . "/" . $_GET["priority"]);
+} else if (isset($_GET["conversionData"])) {
+  header("Location: https://127.0.0.1:8000/.well-known/ad-click-attribution/" . $_GET["conversionData"]);
+}
+?>
index 2e89842..fb5499f 100644 (file)
@@ -1,5 +1,5 @@
-CONSOLE MESSAGE: adcampaignid must have a non-negative value less than 64 for Ad Click Attribution.
-CONSOLE MESSAGE: adcampaignid must have a non-negative value less than 64 for Ad Click Attribution.
+CONSOLE MESSAGE: adcampaignid must have a non-negative value less than or equal to 63 for Ad Click Attribution.
+CONSOLE MESSAGE: adcampaignid must have a non-negative value less than or equal to 63 for Ad Click Attribution.
 CONSOLE MESSAGE: adcampaignid can not be converted to a non-negative integer which is required for Ad Click Attribution.
 CONSOLE MESSAGE: adcampaignid can not be converted to a non-negative integer which is required for Ad Click Attribution.
 CONSOLE MESSAGE: adcampaignid can not be converted to a non-negative integer which is required for Ad Click Attribution.
index 5773274..c8e5fbd 100644 (file)
@@ -1,3 +1,31 @@
+2019-04-09  John Wilander  <wilander@apple.com>
+
+        Pick up Ad Click Attribution conversions in NetworkResourceLoader::willSendRedirectedRequest()
+        https://bugs.webkit.org/show_bug.cgi?id=196558
+        <rdar://problem/47650245>
+
+        Reviewed by Youenn Fablet.
+
+        Tests: http/tests/adClickAttribution/attribution-conversion-through-cross-site-image-redirect.html
+               http/tests/adClickAttribution/attribution-conversion-through-image-redirect-with-priority.html
+               http/tests/adClickAttribution/attribution-conversion-through-image-redirect-without-priority.html
+
+        The existing API tests were expanded too.
+
+        * html/HTMLAnchorElement.cpp:
+        (WebCore::HTMLAnchorElement::parseAdClickAttribution const):
+           Enhanced the use of AdClickAttribution::MaxEntropy.
+        * loader/AdClickAttribution.cpp:
+        (WebCore::AdClickAttribution::parseConversionRequest):
+            New function to parse and validate URLs with a path starting with
+            /.well-known/ad-click-attribution/.
+        (WebCore::AdClickAttribution::toString const):
+            Added output for the conversion priority for testing purposes.
+        * loader/AdClickAttribution.h:
+        (WebCore::AdClickAttribution::Campaign::isValid const):
+        (WebCore::AdClickAttribution::Conversion::isValid const):
+           Enhanced the use of AdClickAttribution::MaxEntropy.
+
 2019-04-09  Don Olmstead  <don.olmstead@sony.com>
 
         [CMake] Apple builds should use ICU_INCLUDE_DIRS
index f45e5be..5a1dce8 100644 (file)
@@ -431,8 +431,8 @@ Optional<AdClickAttribution> HTMLAnchorElement::parseAdClickAttribution() const
         return WTF::nullopt;
     }
     
-    if (adCampaignID.value() >= AdClickAttribution::MaxEntropy) {
-        document().addConsoleMessage(MessageSource::Other, MessageLevel::Warning, makeString("adcampaignid must have a non-negative value less than ", AdClickAttribution::MaxEntropy, " for Ad Click Attribution."));
+    if (adCampaignID.value() > AdClickAttribution::MaxEntropy) {
+        document().addConsoleMessage(MessageSource::Other, MessageLevel::Warning, makeString("adcampaignid must have a non-negative value less than or equal to ", AdClickAttribution::MaxEntropy, " for Ad Click Attribution."));
         return WTF::nullopt;
     }
 
index 4e6a893..5d97faa 100644 (file)
 #include <wtf/RandomNumber.h>
 #include <wtf/URL.h>
 #include <wtf/text/StringBuilder.h>
+#include <wtf/text/StringView.h>
 
 namespace WebCore {
 
+static const char adClickAttributionPathPrefix[] = "/.well-known/ad-click-attribution/";
+const size_t adClickConversionDataPathSegmentSize = 2;
+const size_t adClickPriorityPathSegmentSize = 2;
+
 bool AdClickAttribution::isValid() const
 {
     return m_conversion
@@ -42,6 +47,39 @@ bool AdClickAttribution::isValid() const
         && m_earliestTimeToSend;
 }
 
+Optional<AdClickAttribution::Conversion> AdClickAttribution::parseConversionRequest(const URL& redirectURL)
+{
+    if (!redirectURL.protocolIs("https"_s) || redirectURL.hasUsername() || redirectURL.hasPassword() || redirectURL.hasQuery() || redirectURL.hasFragment())
+        return { };
+
+    auto path = StringView(redirectURL.string()).substring(redirectURL.pathStart(), redirectURL.pathEnd() - redirectURL.pathStart());
+    if (path.isEmpty() || !path.startsWith(adClickAttributionPathPrefix))
+        return { };
+
+    auto prefixLength = sizeof(adClickAttributionPathPrefix) - 1;
+    if (path.length() == prefixLength + adClickConversionDataPathSegmentSize) {
+        auto conversionDataUInt64 = path.substring(prefixLength, adClickConversionDataPathSegmentSize).toUInt64Strict();
+        if (!conversionDataUInt64 || *conversionDataUInt64 > MaxEntropy)
+            return { };
+
+        return Conversion { static_cast<uint32_t>(*conversionDataUInt64), Priority { 0 } };
+    }
+    
+    if (path.length() == prefixLength + adClickConversionDataPathSegmentSize + 1 + adClickPriorityPathSegmentSize) {
+        auto conversionDataUInt64 = path.substring(prefixLength, adClickConversionDataPathSegmentSize).toUInt64Strict();
+        if (!conversionDataUInt64 || *conversionDataUInt64 > MaxEntropy)
+            return { };
+
+        auto conversionPriorityUInt64 = path.substring(prefixLength + adClickConversionDataPathSegmentSize + 1, adClickPriorityPathSegmentSize).toUInt64Strict();
+        if (!conversionPriorityUInt64 || *conversionPriorityUInt64 > MaxEntropy)
+            return { };
+
+        return Conversion { static_cast<uint32_t>(*conversionDataUInt64), Priority { static_cast<uint32_t>(*conversionPriorityUInt64) } };
+    }
+
+    return { };
+}
+
 void AdClickAttribution::setConversion(Conversion&& conversion)
 {
     if (!conversion.isValid() || (m_conversion && m_conversion->priority > conversion.priority))
@@ -102,6 +140,8 @@ String AdClickAttribution::toString() const
     if (m_conversion) {
         builder.appendLiteral("\nConversion data: ");
         builder.appendNumber(m_conversion.value().data);
+        builder.appendLiteral("\nConversion priority: ");
+        builder.appendNumber(m_conversion.value().priority);
     } else
         builder.appendLiteral("\nNo conversion data.");
     builder.append('\n');
index 3ecbbbe..88db8c2 100644 (file)
@@ -26,6 +26,7 @@
 #pragma once
 
 #include "RegistrableDomain.h"
+#include <wtf/CompletionHandler.h>
 #include <wtf/Optional.h>
 #include <wtf/URL.h>
 #include <wtf/WallTime.h>
@@ -40,7 +41,7 @@ public:
     using ConversionData = uint32_t;
     using PriorityValue = uint32_t;
 
-    static constexpr uint32_t MaxEntropy = 64;
+    static constexpr uint32_t MaxEntropy = 63;
 
     struct Campaign {
         Campaign() = default;
@@ -51,7 +52,7 @@ public:
         
         bool isValid() const
         {
-            return id < MaxEntropy;
+            return id <= MaxEntropy;
         }
         
         CampaignId id { 0 };
@@ -210,7 +211,7 @@ public:
 
         bool isValid() const
         {
-            return data < MaxEntropy && priority < MaxEntropy;
+            return data <= MaxEntropy && priority <= MaxEntropy;
         }
         
         ConversionData data;
@@ -229,6 +230,7 @@ public:
     {
     }
 
+    WEBCORE_EXPORT static Optional<Conversion> parseConversionRequest(const URL& redirectURL);
     WEBCORE_EXPORT void setConversion(Conversion&&);
     WEBCORE_EXPORT URL url() const;
     WEBCORE_EXPORT URL referrer() const;
index 9faffab..1318da8 100644 (file)
@@ -1,3 +1,39 @@
+2019-04-09  John Wilander  <wilander@apple.com>
+
+        Pick up Ad Click Attribution conversions in NetworkResourceLoader::willSendRedirectedRequest()
+        https://bugs.webkit.org/show_bug.cgi?id=196558
+        <rdar://problem/47650245>
+
+        Reviewed by Youenn Fablet.
+
+        So called pixel requests have traditionally been used to send ad click
+        attribution data to click sources. The privacy implications of such
+        pixel requests are severe which is in part why browsers have started to
+        block cookies from being sent in such third-party requests.
+
+        To allow for a smooth transition to more privacy-friendly ad click
+        attribution, we should allow servers to make a redirect to
+        https://click-source.example/.well-known/ad-click-attribution/ to
+        trigger a so called conversion.
+
+        This patch checks for the well-known location in the path component of
+        the redirect URL. If the request indeed goes to the well-known location,
+        we parse the conversion data and send it to the storage in the network
+        session.
+
+        * NetworkProcess/NetworkAdClickAttribution.cpp:
+        (WebKit::NetworkAdClickAttribution::convert):
+            Reporting function.
+        * NetworkProcess/NetworkAdClickAttribution.h:
+        * NetworkProcess/NetworkResourceLoader.cpp:
+        (WebKit::NetworkResourceLoader::willSendRedirectedRequest):
+            Now checks for the well-known location through a call to
+            WebCore::AdClickAttribution::parseConversionRequest().
+        * NetworkProcess/NetworkSession.cpp:
+        (WebKit::NetworkSession::convertAdClickAttribution):
+            Piping to WebKit::NetworkAdClickAttribution::convert().
+        * NetworkProcess/NetworkSession.h:
+
 2019-04-09  Chris Dumez  <cdumez@apple.com>
 
         [iOS] WebContent processes should be marked as "Foreground Running" when their view is visible
index 8f77e84..391c098 100644 (file)
 
 namespace WebKit {
 
+using RegistrableDomain = WebCore::RegistrableDomain;
 using AdClickAttribution = WebCore::AdClickAttribution;
 using Source = WebCore::AdClickAttribution::Source;
 using Destination = WebCore::AdClickAttribution::Destination;
 using DestinationMap = HashMap<Destination, AdClickAttribution>;
+using Conversion = WebCore::AdClickAttribution::Conversion;
 
 DestinationMap& NetworkAdClickAttribution::ensureDestinationMapForSource(const Source& source)
 {
@@ -49,6 +51,19 @@ void NetworkAdClickAttribution::store(AdClickAttribution&& adClickAttribution)
     destinationMapForSource.add(adClickAttribution.destination(), WTFMove(adClickAttribution));
 }
 
+void NetworkAdClickAttribution::convert(const Source& source, const Destination& destination, Conversion&& conversion)
+{
+    auto sourceIter = m_adClickAttributionMap.find(source);
+    if (sourceIter == m_adClickAttributionMap.end())
+        return;
+
+    auto destinationIter = sourceIter->value.find(destination);
+    if (destinationIter == sourceIter->value.end())
+        return;
+
+    destinationIter->value.setConversion(WTFMove(conversion));
+}
+
 void NetworkAdClickAttribution::clear(CompletionHandler<void()>&& completionHandler)
 {
     m_adClickAttributionMap.clear();
index 93dd333..09a0ed3 100644 (file)
@@ -26,6 +26,7 @@
 #pragma once
 
 #include <WebCore/AdClickAttribution.h>
+#include <WebCore/RegistrableDomain.h>
 #include <wtf/CompletionHandler.h>
 #include <wtf/HashMap.h>
 #include <wtf/text/WTFString.h>
@@ -35,17 +36,20 @@ namespace WebKit {
 class NetworkAdClickAttribution {
 public:
 
+    using RegistrableDomain = WebCore::RegistrableDomain;
     using AdClickAttribution = WebCore::AdClickAttribution;
     using Source = WebCore::AdClickAttribution::Source;
     using Destination = WebCore::AdClickAttribution::Destination;
     using DestinationMap = HashMap<Destination, AdClickAttribution>;
+    using Conversion = WebCore::AdClickAttribution::Conversion;
 
     void store(AdClickAttribution&&);
+    void convert(const Source&, const Destination&, Conversion&&);
     void clear(CompletionHandler<void()>&&);
     void toString(CompletionHandler<void(String)>&&) const;
 
 private:
-    DestinationMap& ensureDestinationMapForSource(const AdClickAttribution::Source&);
+    DestinationMap& ensureDestinationMapForSource(const Source&);
 
     HashMap<Source, DestinationMap> m_adClickAttributionMap;
 };
index 6539536..4ac4ffa 100644 (file)
@@ -41,6 +41,7 @@
 #include "WebPageMessages.h"
 #include "WebResourceLoaderMessages.h"
 #include "WebsiteDataStoreParameters.h"
+#include <WebCore/AdClickAttribution.h>
 #include <WebCore/BlobDataFileReference.h>
 #include <WebCore/CertificateInfo.h>
 #include <WebCore/ContentSecurityPolicy.h>
@@ -583,6 +584,16 @@ void NetworkResourceLoader::willSendRedirectedRequest(ResourceRequest&& request,
 {
     ++m_redirectCount;
 
+    auto& redirectURL = redirectRequest.url();
+    if (auto adClickConversion = AdClickAttribution::parseConversionRequest(redirectURL)) {
+        RegistrableDomain redirectDomain { redirectURL };
+        auto& firstPartyURL = redirectRequest.firstPartyForCookies();
+        NetworkSession* networkSession;
+        // The redirect has to be done by the same registrable domain and it has to be a third-party request.
+        if (redirectDomain.matches(request.url()) && !redirectDomain.matches(firstPartyURL) && (networkSession = m_connection->networkProcess().networkSession(sessionID())))
+            networkSession->convertAdClickAttribution(AdClickAttribution::Source { WTFMove(redirectDomain) }, AdClickAttribution::Destination { firstPartyURL }, WTFMove(*adClickConversion));
+    }
+
     auto maxAgeCap = validateCacheEntryForMaxAgeCapValidation(request, redirectRequest, redirectResponse);
     if (redirectResponse.source() == ResourceResponse::Source::Network && canUseCachedRedirect(request))
         m_cache->storeRedirect(request, redirectResponse, redirectRequest, maxAgeCap);
index 1e30cea..98cd05b 100644 (file)
@@ -32,7 +32,6 @@
 #include "WebPageProxy.h"
 #include "WebPageProxyMessages.h"
 #include "WebProcessProxy.h"
-#include <WebCore/AdClickAttribution.h>
 #include <WebCore/CookieJar.h>
 #include <WebCore/NetworkStorageSession.h>
 
@@ -141,6 +140,11 @@ void NetworkSession::storeAdClickAttribution(WebCore::AdClickAttribution&& adCli
     m_adClickAttribution->store(WTFMove(adClickAttribution));
 }
 
+void NetworkSession::convertAdClickAttribution(const WebCore::AdClickAttribution::Source& source, const WebCore::AdClickAttribution::Destination& destination, WebCore::AdClickAttribution::Conversion&& conversion)
+{
+    m_adClickAttribution->convert(source, destination, WTFMove(conversion));
+}
+
 void NetworkSession::dumpAdClickAttribution(CompletionHandler<void(String)>&& completionHandler)
 {
     m_adClickAttribution->toString(WTFMove(completionHandler));
index 818b15e..a9ea287 100644 (file)
@@ -26,6 +26,7 @@
 #pragma once
 
 #include "WebResourceLoadStatisticsStore.h"
+#include <WebCore/AdClickAttribution.h>
 #include <WebCore/RegistrableDomain.h>
 #include <pal/SessionID.h>
 #include <wtf/HashSet.h>
@@ -37,7 +38,6 @@
 #include <wtf/text/WTFString.h>
 
 namespace WebCore {
-class AdClickAttribution;
 class NetworkStorageSession;
 enum class IncludeHttpOnlyCookies : bool;
 enum class ShouldSample : bool;
@@ -80,6 +80,7 @@ public:
     void notifyPageStatisticsTelemetryFinished(unsigned totalPrevalentResources, unsigned totalPrevalentResourcesWithUserInteraction, unsigned top3SubframeUnderTopFrameOrigins);
 #endif
     void storeAdClickAttribution(WebCore::AdClickAttribution&&);
+    void convertAdClickAttribution(const WebCore::AdClickAttribution::Source&, const WebCore::AdClickAttribution::Destination&, WebCore::AdClickAttribution::Conversion&&);
     void dumpAdClickAttribution(CompletionHandler<void(String)>&&);
     void clearAdClickAttribution(CompletionHandler<void()>&&);
 
index 7409f5a..7aad637 100644 (file)
@@ -1,3 +1,15 @@
+2019-04-09  John Wilander  <wilander@apple.com>
+
+        Pick up Ad Click Attribution conversions in NetworkResourceLoader::willSendRedirectedRequest()
+        https://bugs.webkit.org/show_bug.cgi?id=196558
+        <rdar://problem/47650245>
+
+        Reviewed by Youenn Fablet.
+
+        * TestWebKitAPI/Tests/WebCore/AdClickAttribution.cpp:
+        (TestWebKitAPI::TEST):
+            Added tests of WebCore::AdClickAttribution::parseConversionRequest().
+
 2019-04-09  Don Olmstead  <don.olmstead@sony.com>
 
         [CMake] Apple builds should use ICU_INCLUDE_DIRS
index 21a38de..3a9813e 100644 (file)
@@ -34,7 +34,6 @@ using namespace WebCore;
 namespace TestWebKitAPI {
 
 constexpr uint32_t min6BitValue { 0 };
-constexpr uint32_t max6BitValue { 63 };
 
 const URL webKitURL { { }, "https://webkit.org"_s };
 const URL exampleURL { { }, "https://example.com"_s };
@@ -68,8 +67,8 @@ TEST(AdClickAttribution, ValidMidValues)
 
 TEST(AdClickAttribution, ValidMaxValues)
 {
-    AdClickAttribution attribution { AdClickAttribution::Campaign(max6BitValue), AdClickAttribution::Source { webKitURL }, AdClickAttribution::Destination { exampleURL } };
-    attribution.setConversion(AdClickAttribution::Conversion(max6BitValue, AdClickAttribution::Priority(max6BitValue)));
+    AdClickAttribution attribution { AdClickAttribution::Campaign(AdClickAttribution::MaxEntropy), AdClickAttribution::Source { webKitURL }, AdClickAttribution::Destination { exampleURL } };
+    attribution.setConversion(AdClickAttribution::Conversion(AdClickAttribution::MaxEntropy, AdClickAttribution::Priority(AdClickAttribution::MaxEntropy)));
 
     auto attributionURL = attribution.url();
     auto referrerURL = attribution.referrer();
@@ -80,20 +79,56 @@ TEST(AdClickAttribution, ValidMaxValues)
 
 TEST(AdClickAttribution, EarliestTimeToSendAttributionMinimumDelay)
 {
-    AdClickAttribution attribution { AdClickAttribution::Campaign(max6BitValue), AdClickAttribution::Source { webKitURL }, AdClickAttribution::Destination { exampleURL } };
+    AdClickAttribution attribution { AdClickAttribution::Campaign(AdClickAttribution::MaxEntropy), AdClickAttribution::Source { webKitURL }, AdClickAttribution::Destination { exampleURL } };
     auto now = WallTime::now();
-    attribution.setConversion(AdClickAttribution::Conversion(max6BitValue, AdClickAttribution::Priority(max6BitValue)));
+    attribution.setConversion(AdClickAttribution::Conversion(AdClickAttribution::MaxEntropy, AdClickAttribution::Priority(AdClickAttribution::MaxEntropy)));
     auto earliestTimeToSend = attribution.earliestTimeToSend();
     ASSERT_TRUE(earliestTimeToSend);
     ASSERT_TRUE(earliestTimeToSend.value().secondsSinceEpoch() - 24_h >= now.secondsSinceEpoch());
 }
 
+TEST(AdClickAttribution, ValidConversionURLs)
+{
+    const URL conversionURLWithoutPriority { { }, "https://webkit.org/.well-known/ad-click-attribution/22"_s };
+    auto optionalConversion = AdClickAttribution::parseConversionRequest(conversionURLWithoutPriority);
+    ASSERT_TRUE(optionalConversion);
+    ASSERT_EQ(optionalConversion->data, (uint32_t)22);
+
+    const URL conversionURLWithoutPriorityMaxEntropy { { }, "https://webkit.org/.well-known/ad-click-attribution/63"_s };
+    optionalConversion = AdClickAttribution::parseConversionRequest(conversionURLWithoutPriorityMaxEntropy);
+    ASSERT_TRUE(optionalConversion);
+    ASSERT_EQ(optionalConversion->data, (uint32_t)63);
+    
+    const URL conversionURLWithoutPriorityAndLeadingZero { { }, "https://webkit.org/.well-known/ad-click-attribution/02"_s };
+    optionalConversion = AdClickAttribution::parseConversionRequest(conversionURLWithoutPriorityAndLeadingZero);
+    ASSERT_TRUE(optionalConversion);
+    ASSERT_EQ(optionalConversion->data, (uint32_t)2);
+
+    const URL conversionURLWithPriority { { }, "https://webkit.org/.well-known/ad-click-attribution/22/12"_s };
+    optionalConversion = AdClickAttribution::parseConversionRequest(conversionURLWithPriority);
+    ASSERT_TRUE(optionalConversion);
+    ASSERT_EQ(optionalConversion->data, (uint32_t)22);
+    ASSERT_EQ(optionalConversion->priority, (uint32_t)12);
+
+    const URL conversionURLWithPriorityMaxEntropy { { }, "https://webkit.org/.well-known/ad-click-attribution/63/63"_s };
+    optionalConversion = AdClickAttribution::parseConversionRequest(conversionURLWithPriorityMaxEntropy);
+    ASSERT_TRUE(optionalConversion);
+    ASSERT_EQ(optionalConversion->data, (uint32_t)63);
+    ASSERT_EQ(optionalConversion->priority, (uint32_t)63);
+    
+    const URL conversionURLWithPriorityAndLeadingZero { { }, "https://webkit.org/.well-known/ad-click-attribution/22/02"_s };
+    optionalConversion = AdClickAttribution::parseConversionRequest(conversionURLWithPriorityAndLeadingZero);
+    ASSERT_TRUE(optionalConversion);
+    ASSERT_EQ(optionalConversion->data, (uint32_t)22);
+    ASSERT_EQ(optionalConversion->priority, (uint32_t)2);
+}
+
 // Negative test cases.
 
 TEST(AdClickAttribution, InvalidCampaignId)
 {
-    AdClickAttribution attribution { AdClickAttribution::Campaign(max6BitValue + 1), AdClickAttribution::Source { webKitURL }, AdClickAttribution::Destination { exampleURL } };
-    attribution.setConversion(AdClickAttribution::Conversion(max6BitValue, AdClickAttribution::Priority(max6BitValue)));
+    AdClickAttribution attribution { AdClickAttribution::Campaign(AdClickAttribution::MaxEntropy + 1), AdClickAttribution::Source { webKitURL }, AdClickAttribution::Destination { exampleURL } };
+    attribution.setConversion(AdClickAttribution::Conversion(AdClickAttribution::MaxEntropy, AdClickAttribution::Priority(AdClickAttribution::MaxEntropy)));
 
     auto attributionURL = attribution.url();
     auto referrerURL = attribution.referrer();
@@ -104,8 +139,8 @@ TEST(AdClickAttribution, InvalidCampaignId)
 
 TEST(AdClickAttribution, InvalidSourceHost)
 {
-    AdClickAttribution attribution { AdClickAttribution::Campaign(max6BitValue), AdClickAttribution::Source { emptyURL }, AdClickAttribution::Destination { exampleURL } };
-    attribution.setConversion(AdClickAttribution::Conversion(max6BitValue, AdClickAttribution::Priority(max6BitValue)));
+    AdClickAttribution attribution { AdClickAttribution::Campaign(AdClickAttribution::MaxEntropy), AdClickAttribution::Source { emptyURL }, AdClickAttribution::Destination { exampleURL } };
+    attribution.setConversion(AdClickAttribution::Conversion(AdClickAttribution::MaxEntropy, AdClickAttribution::Priority(AdClickAttribution::MaxEntropy)));
 
     auto attributionURL = attribution.url();
     auto referrerURL = attribution.referrer();
@@ -116,8 +151,8 @@ TEST(AdClickAttribution, InvalidSourceHost)
 
 TEST(AdClickAttribution, InvalidDestinationHost)
 {
-    AdClickAttribution attribution { AdClickAttribution::Campaign(max6BitValue + 1), AdClickAttribution::Source { webKitURL }, AdClickAttribution::Destination { emptyURL } };
-    attribution.setConversion(AdClickAttribution::Conversion(max6BitValue, AdClickAttribution::Priority(max6BitValue)));
+    AdClickAttribution attribution { AdClickAttribution::Campaign(AdClickAttribution::MaxEntropy + 1), AdClickAttribution::Source { webKitURL }, AdClickAttribution::Destination { emptyURL } };
+    attribution.setConversion(AdClickAttribution::Conversion(AdClickAttribution::MaxEntropy, AdClickAttribution::Priority(AdClickAttribution::MaxEntropy)));
 
     auto attributionURL = attribution.url();
     auto referrerURL = attribution.referrer();
@@ -128,8 +163,8 @@ TEST(AdClickAttribution, InvalidDestinationHost)
 
 TEST(AdClickAttribution, InvalidConversionData)
 {
-    AdClickAttribution attribution { AdClickAttribution::Campaign(max6BitValue), AdClickAttribution::Source { webKitURL }, AdClickAttribution::Destination { exampleURL } };
-    attribution.setConversion(AdClickAttribution::Conversion((max6BitValue + 1), AdClickAttribution::Priority(max6BitValue)));
+    AdClickAttribution attribution { AdClickAttribution::Campaign(AdClickAttribution::MaxEntropy), AdClickAttribution::Source { webKitURL }, AdClickAttribution::Destination { exampleURL } };
+    attribution.setConversion(AdClickAttribution::Conversion((AdClickAttribution::MaxEntropy + 1), AdClickAttribution::Priority(AdClickAttribution::MaxEntropy)));
 
     auto attributionURL = attribution.url();
     auto referrerURL = attribution.referrer();
@@ -140,8 +175,8 @@ TEST(AdClickAttribution, InvalidConversionData)
 
 TEST(AdClickAttribution, InvalidPriority)
 {
-    AdClickAttribution attribution { AdClickAttribution::Campaign(max6BitValue), AdClickAttribution::Source { webKitURL }, AdClickAttribution::Destination { exampleURL } };
-    attribution.setConversion(AdClickAttribution::Conversion(max6BitValue, AdClickAttribution::Priority(max6BitValue + 1)));
+    AdClickAttribution attribution { AdClickAttribution::Campaign(AdClickAttribution::MaxEntropy), AdClickAttribution::Source { webKitURL }, AdClickAttribution::Destination { exampleURL } };
+    attribution.setConversion(AdClickAttribution::Conversion(AdClickAttribution::MaxEntropy, AdClickAttribution::Priority(AdClickAttribution::MaxEntropy + 1)));
 
     auto attributionURL = attribution.url();
     auto referrerURL = attribution.referrer();
@@ -152,7 +187,7 @@ TEST(AdClickAttribution, InvalidPriority)
 
 TEST(AdClickAttribution, InvalidMissingConversion)
 {
-    AdClickAttribution attribution { AdClickAttribution::Campaign(max6BitValue), AdClickAttribution::Source { webKitURL }, AdClickAttribution::Destination { exampleURL } };
+    AdClickAttribution attribution { AdClickAttribution::Campaign(AdClickAttribution::MaxEntropy), AdClickAttribution::Source { webKitURL }, AdClickAttribution::Destination { exampleURL } };
 
     auto attributionURL = attribution.url();
     auto referrerURL = attribution.referrer();
@@ -162,4 +197,106 @@ TEST(AdClickAttribution, InvalidMissingConversion)
     ASSERT_FALSE(attribution.earliestTimeToSend());
 }
 
+TEST(AdClickAttribution, InvalidConversionURLs)
+{
+    const URL conversionURLWithSingleDigitConversionData { { }, "https://webkit.org/.well-known/ad-click-attribution/2"_s };
+    auto optionalConversion = AdClickAttribution::parseConversionRequest(conversionURLWithSingleDigitConversionData);
+    ASSERT_FALSE(optionalConversion);
+    
+    const URL conversionURLWithNonNumeralConversionData { { }, "https://webkit.org/.well-known/ad-click-attribution/2s"_s };
+    optionalConversion = AdClickAttribution::parseConversionRequest(conversionURLWithNonNumeralConversionData);
+    ASSERT_FALSE(optionalConversion);
+
+    const URL conversionURLWithNegativeConversionData { { }, "https://webkit.org/.well-known/ad-click-attribution/-2"_s };
+    optionalConversion = AdClickAttribution::parseConversionRequest(conversionURLWithNegativeConversionData);
+    ASSERT_FALSE(optionalConversion);
+
+    const URL conversionURLWithTooLargeConversionData { { }, "https://webkit.org/.well-known/ad-click-attribution/64"_s };
+    optionalConversion = AdClickAttribution::parseConversionRequest(conversionURLWithTooLargeConversionData);
+    ASSERT_FALSE(optionalConversion);
+
+    const URL conversionURLWithSingleDigitPriority { { }, "https://webkit.org/.well-known/ad-click-attribution/22/2"_s };
+    optionalConversion = AdClickAttribution::parseConversionRequest(conversionURLWithSingleDigitPriority);
+    ASSERT_FALSE(optionalConversion);
+    
+    const URL conversionURLWithNonNumeralPriority { { }, "https://webkit.org/.well-known/ad-click-attribution/22/2s"_s };
+    optionalConversion = AdClickAttribution::parseConversionRequest(conversionURLWithNonNumeralPriority);
+    ASSERT_FALSE(optionalConversion);
+    
+    const URL conversionURLWithNegativePriority { { }, "https://webkit.org/.well-known/ad-click-attribution/22/-2"_s };
+    optionalConversion = AdClickAttribution::parseConversionRequest(conversionURLWithNegativePriority);
+    ASSERT_FALSE(optionalConversion);
+    
+    const URL conversionURLWithTooLargePriority { { }, "https://webkit.org/.well-known/ad-click-attribution/22/64"_s };
+    optionalConversion = AdClickAttribution::parseConversionRequest(conversionURLWithTooLargePriority);
+    ASSERT_FALSE(optionalConversion);
+
+    const URL conversionURLWithTooLargeConversionDataAndPriority { { }, "https://webkit.org/.well-known/ad-click-attribution/64/22"_s };
+    optionalConversion = AdClickAttribution::parseConversionRequest(conversionURLWithTooLargeConversionDataAndPriority);
+    ASSERT_FALSE(optionalConversion);
+
+    const URL conversionURLWithTooLargeConversionDataAndTooLargePriority { { }, "https://webkit.org/.well-known/ad-click-attribution/64/64"_s };
+    optionalConversion = AdClickAttribution::parseConversionRequest(conversionURLWithTooLargeConversionDataAndTooLargePriority);
+    ASSERT_FALSE(optionalConversion);
+
+    const URL conversionURLWithExtraLeadingSlash = { { }, "https://webkit.org/.well-known/ad-click-attribution//22/12"_s };
+    optionalConversion = AdClickAttribution::parseConversionRequest(conversionURLWithExtraLeadingSlash);
+    ASSERT_FALSE(optionalConversion);
+
+    const URL conversionURLWithExtraTrailingSlash = { { }, "https://webkit.org/.well-known/ad-click-attribution/22/12/"_s };
+    optionalConversion = AdClickAttribution::parseConversionRequest(conversionURLWithExtraTrailingSlash);
+    ASSERT_FALSE(optionalConversion);
+
+    const URL conversionURLWithTrailingQuestionMark = { { }, "https://webkit.org/.well-known/ad-click-attribution/22/12?"_s };
+    optionalConversion = AdClickAttribution::parseConversionRequest(conversionURLWithTrailingQuestionMark);
+    ASSERT_FALSE(optionalConversion);
+}
+
+TEST(AdClickAttribution, InvalidConversionWithDisallowedURLComponents)
+{
+    // Protocol.
+    const URL conversionURLWithHttpProtocol { { }, "http://webkit.org/.well-known/ad-click-attribution/2"_s };
+    auto optionalConversion = AdClickAttribution::parseConversionRequest(conversionURLWithHttpProtocol);
+    ASSERT_FALSE(optionalConversion);
+
+    const URL conversionURLWithWssProtocol { { }, "wss://webkit.org/.well-known/ad-click-attribution/2"_s };
+    optionalConversion = AdClickAttribution::parseConversionRequest(conversionURLWithWssProtocol);
+    ASSERT_FALSE(optionalConversion);
+
+    const URL conversionURLWithFileProtocol { { }, "file:///.well-known/ad-click-attribution/2"_s };
+    optionalConversion = AdClickAttribution::parseConversionRequest(conversionURLWithFileProtocol);
+    ASSERT_FALSE(optionalConversion);
+
+    // Username and password.
+    const URL conversionURLWithUserName { { }, "https://user@webkit.org/.well-known/ad-click-attribution/2"_s };
+    optionalConversion = AdClickAttribution::parseConversionRequest(conversionURLWithUserName);
+    ASSERT_FALSE(optionalConversion);
+
+    const URL conversionURLWithPassword = { { }, "https://:pwd@webkit.org/.well-known/ad-click-attribution/22/12?"_s };
+    optionalConversion = AdClickAttribution::parseConversionRequest(conversionURLWithPassword);
+    ASSERT_FALSE(optionalConversion);
+
+    const URL conversionURLWithUsernameAndPassword = { { }, "https://user:pwd@webkit.org/.well-known/ad-click-attribution/22/12?"_s };
+    optionalConversion = AdClickAttribution::parseConversionRequest(conversionURLWithUsernameAndPassword);
+    ASSERT_FALSE(optionalConversion);
+
+    // Query string.
+    const URL conversionURLWithTrailingQuestionMark = { { }, "https://webkit.org/.well-known/ad-click-attribution/22/12?"_s };
+    optionalConversion = AdClickAttribution::parseConversionRequest(conversionURLWithTrailingQuestionMark);
+    ASSERT_FALSE(optionalConversion);
+
+    const URL conversionURLWithQueryString = { { }, "https://webkit.org/.well-known/ad-click-attribution/22/12?extra=data"_s };
+    optionalConversion = AdClickAttribution::parseConversionRequest(conversionURLWithQueryString);
+    ASSERT_FALSE(optionalConversion);
+    
+    // Fragment.
+    const URL conversionURLWithTrailingHash = { { }, "https://webkit.org/.well-known/ad-click-attribution/22/12#"_s };
+    optionalConversion = AdClickAttribution::parseConversionRequest(conversionURLWithTrailingHash);
+    ASSERT_FALSE(optionalConversion);
+
+    const URL conversionURLWithFragment = { { }, "https://webkit.org/.well-known/ad-click-attribution/22/12#fragment"_s };
+    optionalConversion = AdClickAttribution::parseConversionRequest(conversionURLWithFragment);
+    ASSERT_FALSE(optionalConversion);
+}
+
 } // namespace TestWebKitAPI