Title: [275137] trunk
Revision
275137
Author
[email protected]
Date
2021-03-27 11:52:51 -0700 (Sat, 27 Mar 2021)

Log Message

PCM: Send report to both click source and attribution destination website
https://bugs.webkit.org/show_bug.cgi?id=223615
<rdar://problem/75849443>

Reviewed by Brent Fulgham.

Source/WebCore:

Introduce 2 new structs for storing the earliest time to send and
seconds until send for the source and destination sites.

* loader/PrivateClickMeasurement.cpp:
(WebCore::PrivateClickMeasurement::isValid const):
(WebCore::PrivateClickMeasurement::hasPreviouslyBeenReported):
(WebCore::randomlyBetweenTwentyFourAndFortyEightHours):
(WebCore::PrivateClickMeasurement::attributeAndGetEarliestTimeToSend):
* loader/PrivateClickMeasurement.h:
(WebCore::PrivateClickMeasurement::AttributionSecondsUntilSendData::hasValidSecondsUntilSendValues):
(WebCore::PrivateClickMeasurement::AttributionSecondsUntilSendData::minSecondsUntilSend):
(WebCore::PrivateClickMeasurement::AttributionSecondsUntilSendData::encode const):
(WebCore::PrivateClickMeasurement::AttributionSecondsUntilSendData::decode):
(WebCore::PrivateClickMeasurement::AttributionTimeToSendData::earliestTimeToSend):
(WebCore::PrivateClickMeasurement::AttributionTimeToSendData::latestTimeToSend):
(WebCore::PrivateClickMeasurement::AttributionTimeToSendData::attributionReportEndpoint):
(WebCore::PrivateClickMeasurement::AttributionTimeToSendData::encode const):
(WebCore::PrivateClickMeasurement::AttributionTimeToSendData::decode):
(WebCore::PrivateClickMeasurement::timesToSend const):
(WebCore::PrivateClickMeasurement::setTimesToSend):
(WebCore::PrivateClickMeasurement::encode const):
(WebCore::PrivateClickMeasurement::decode):
(WebCore::PrivateClickMeasurement::earliestTimeToSend const): Deleted.
(WebCore::PrivateClickMeasurement::setEarliestTimeToSend): Deleted.

Source/WebKit:

* NetworkProcess/Classifier/ResourceLoadStatisticsDatabaseStore.cpp:
Move these queries to the correct INSERT OR REPLACE category. Stop
inserting null for earliestTimeToSendToDestination and starting
binding a parameter to it now that we are supporting reports to both
sites.

Now that earliestTimeToSend* can be null if a report has been sent
to a site, we need queries to set the value to null, and also need
to sort attributions by the minimum of either the two reporting times,
or the non-null time if one is null.

(WebKit::ResourceLoadStatisticsDatabaseStore::destroyStatements):
(WebKit::ResourceLoadStatisticsDatabaseStore::buildPrivateClickMeasurementFromDatabase):
(WebKit::ResourceLoadStatisticsDatabaseStore::insertPrivateClickMeasurement):
(WebKit::ResourceLoadStatisticsDatabaseStore::attributePrivateClickMeasurement):
We should not attribute a PCM value if it has already been reported to
either the source or destination. This is covered by checking
secondsUntilSend.hasValidSecondsUntilSendValues() and
previouslyAttributed.value().hasPreviouslyBeenReported() before
inserting anything into the attributed PCM table.

(WebKit::ResourceLoadStatisticsDatabaseStore::earliestTimesToSend):
(WebKit::ResourceLoadStatisticsDatabaseStore::markReportAsSentToSource):
(WebKit::ResourceLoadStatisticsDatabaseStore::markReportAsSentToDestination):
(WebKit::ResourceLoadStatisticsDatabaseStore::clearSentAttribution):
Clear a value from the attributed table only if it has been sent to
both source and destination site. Otherwise, set the corresponding
attribution endpoint to null so we don't send it here again.

(WebKit::ResourceLoadStatisticsDatabaseStore::markAttributedPrivateClickMeasurementsAsExpiredForTesting):
For the sake of testing we can set the destination earliest time to
send to null. We are only confirming here that the expired attribution
gets sent.

* NetworkProcess/Classifier/ResourceLoadStatisticsDatabaseStore.h:
* NetworkProcess/Classifier/ResourceLoadStatisticsMemoryStore.h:
* NetworkProcess/Classifier/ResourceLoadStatisticsStore.h:
* NetworkProcess/Classifier/WebResourceLoadStatisticsStore.cpp:
(WebKit::WebResourceLoadStatisticsStore::attributePrivateClickMeasurement):
(WebKit::WebResourceLoadStatisticsStore::clearSentAttribution):
* NetworkProcess/Classifier/WebResourceLoadStatisticsStore.h:
* NetworkProcess/PrivateClickMeasurementManager.cpp:
(WebKit::PrivateClickMeasurementManager::storeUnattributed):
(WebKit::PrivateClickMeasurementManager::getTokenPublicKey):
We currently have no way of setting the destination token URL site for
testing. To avoid flakiness, we should not make a ping load for the
token URL if we are reporting to the destination in test mode.

(WebKit::PrivateClickMeasurementManager::attribute):
(WebKit::PrivateClickMeasurementManager::fireConversionRequest):
(WebKit::PrivateClickMeasurementManager::fireConversionRequestImpl):
(WebKit::PrivateClickMeasurementManager::clearSentAttribution):
(WebKit::PrivateClickMeasurementManager::firePendingAttributionRequests):
* NetworkProcess/PrivateClickMeasurementManager.h:

Tools:

Update API tests to check for a valid time to send for both the source
and destination site.

* TestWebKitAPI/Tests/WebCore/PrivateClickMeasurement.cpp:
(TestWebKitAPI::TEST):

LayoutTests:

Layout test coverage.

* http/tests/privateClickMeasurement/resources/conversionFilePath.py:
* http/tests/privateClickMeasurement/resources/conversionReport.py:
* http/tests/privateClickMeasurement/resources/fraudPreventionTestURL.py:
* http/tests/privateClickMeasurement/resources/getConversionData.py:
* http/tests/privateClickMeasurement/send-attribution-conversion-request-expected.txt:
* http/tests/privateClickMeasurement/send-attribution-conversion-request.html:

Modified Paths

Diff

Modified: trunk/LayoutTests/ChangeLog (275136 => 275137)


--- trunk/LayoutTests/ChangeLog	2021-03-27 18:31:30 UTC (rev 275136)
+++ trunk/LayoutTests/ChangeLog	2021-03-27 18:52:51 UTC (rev 275137)
@@ -1,3 +1,20 @@
+2021-03-27  Kate Cheney  <[email protected]>
+
+        PCM: Send report to both click source and attribution destination website
+        https://bugs.webkit.org/show_bug.cgi?id=223615
+        <rdar://problem/75849443>
+
+        Reviewed by Brent Fulgham.
+
+        Layout test coverage.
+
+        * http/tests/privateClickMeasurement/resources/conversionFilePath.py:
+        * http/tests/privateClickMeasurement/resources/conversionReport.py:
+        * http/tests/privateClickMeasurement/resources/fraudPreventionTestURL.py:
+        * http/tests/privateClickMeasurement/resources/getConversionData.py:
+        * http/tests/privateClickMeasurement/send-attribution-conversion-request-expected.txt:
+        * http/tests/privateClickMeasurement/send-attribution-conversion-request.html:
+
 2021-03-27  Zalan Bujtas  <[email protected]>
 
         [Multicolumn] Do not try to re-validate a multicol spanner when the renderer is moved internally

Modified: trunk/LayoutTests/http/tests/privateClickMeasurement/resources/conversionFilePath.py (275136 => 275137)


--- trunk/LayoutTests/http/tests/privateClickMeasurement/resources/conversionFilePath.py	2021-03-27 18:31:30 UTC (rev 275136)
+++ trunk/LayoutTests/http/tests/privateClickMeasurement/resources/conversionFilePath.py	2021-03-27 18:52:51 UTC (rev 275137)
@@ -9,11 +9,17 @@
 http_root = os.path.dirname(os.path.dirname(os.path.abspath(os.path.dirname(file))))
 sys.path.insert(0, http_root)
 
+recipient = parse_qs(os.environ.get('QUERY_STRING', ''), keep_blank_values=True).get('recipient', [None])[0]
 nonce = parse_qs(os.environ.get('QUERY_STRING', ''), keep_blank_values=True).get('nonce', [None])[0]
 
+if recipient is not None:
+    conversion_file_name = 'privateClickMeasurementConversion{}'.format(recipient)
+else:
+    conversion_file_name = 'privateClickMeasurementConversion'
+
 if nonce is not None:
-    conversion_file_name = 'privateClickMeasurementConversion{}.txt'.format(nonce)
+    conversion_file_name = conversion_file_name + '{}.txt'.format(nonce)
 else:
-    conversion_file_name = 'privateClickMeasurementConversion.txt'
+    conversion_file_name = conversion_file_name + '.txt'
 
-conversion_file_path = os.path.join(tempfile.gettempdir(), conversion_file_name)
\ No newline at end of file
+conversion_file_path = os.path.join(tempfile.gettempdir(), conversion_file_name)

Modified: trunk/LayoutTests/http/tests/privateClickMeasurement/resources/conversionReport.py (275136 => 275137)


--- trunk/LayoutTests/http/tests/privateClickMeasurement/resources/conversionReport.py	2021-03-27 18:31:30 UTC (rev 275136)
+++ trunk/LayoutTests/http/tests/privateClickMeasurement/resources/conversionReport.py	2021-03-27 18:52:51 UTC (rev 275137)
@@ -29,11 +29,16 @@
 
 if uri is not None:
     position_of_nonce = uri.find('?nonce=')
+    position_of_nonce_alternate = uri.find('&nonce=')
+
     if position_of_nonce == -1:
         output_url = uri
     else:
         output_url = uri[0:position_of_nonce]
 
+    if position_of_nonce_alternate != -1:
+        output_url = uri[0:position_of_nonce_alternate]
+
     conversion_file.write('REQUEST_URI: {}\n'.format(output_url))
 
 if not cookies_found:
@@ -49,4 +54,4 @@
     'status: 200\r\n'
     'Set-Cookie: cookieSetInConversionReport=1; path=/\r\n'
     'Content-Type: text/html\r\n\r\n'
-)
\ No newline at end of file
+)

Modified: trunk/LayoutTests/http/tests/privateClickMeasurement/resources/fraudPreventionTestURL.py (275136 => 275137)


--- trunk/LayoutTests/http/tests/privateClickMeasurement/resources/fraudPreventionTestURL.py	2021-03-27 18:31:30 UTC (rev 275136)
+++ trunk/LayoutTests/http/tests/privateClickMeasurement/resources/fraudPreventionTestURL.py	2021-03-27 18:52:51 UTC (rev 275137)
@@ -60,4 +60,4 @@
     'secret_token_signature: ABCD\r\n'
     'Set-Cookie: cookieSetInTokenSigningResponse=1; path=/\r\n'
     'Content-Type: text/html\r\n\r\n'
-)
\ No newline at end of file
+)

Modified: trunk/LayoutTests/http/tests/privateClickMeasurement/resources/getConversionData.py (275136 => 275137)


--- trunk/LayoutTests/http/tests/privateClickMeasurement/resources/getConversionData.py	2021-03-27 18:31:30 UTC (rev 275136)
+++ trunk/LayoutTests/http/tests/privateClickMeasurement/resources/getConversionData.py	2021-03-27 18:52:51 UTC (rev 275137)
@@ -62,4 +62,4 @@
         '</script>'
     )
 
-sys.stdout.write('</body></html>')
\ No newline at end of file
+sys.stdout.write('</body></html>')

Modified: trunk/LayoutTests/http/tests/privateClickMeasurement/send-attribution-conversion-request-expected.txt (275136 => 275137)


--- trunk/LayoutTests/http/tests/privateClickMeasurement/send-attribution-conversion-request-expected.txt	2021-03-27 18:31:30 UTC (rev 275136)
+++ trunk/LayoutTests/http/tests/privateClickMeasurement/send-attribution-conversion-request-expected.txt	2021-03-27 18:52:51 UTC (rev 275137)
@@ -13,7 +13,7 @@
 Attribution received.
 HTTP_HOST: 127.0.0.1:8000
 Content type: application/json
-REQUEST_URI: /privateClickMeasurement/resources/conversionReport.py
+REQUEST_URI: /privateClickMeasurement/resources/conversionReport.py?recipient=ClickSource
 No cookies in attribution request.
 Request body:
 {"source_engagement_type":"click","source_site":"127.0.0.1","source_id":3,"attributed_on_site":"localhost","trigger_data":12,"version":2}
@@ -22,6 +22,21 @@
 --------
 Frame: '<!--frame3-->'
 --------
+Attribution received.
+HTTP_HOST: localhost:8000
+Content type: application/json
+REQUEST_URI: /privateClickMeasurement/resources/conversionReport.py?recipient=ClickDestination
+No cookies in attribution request.
+Request body:
+{"source_engagement_type":"click","source_site":"127.0.0.1","source_id":3,"attributed_on_site":"localhost","trigger_data":12,"version":2}
+
+
+--------
+Frame: '<!--frame4-->'
+--------
 Cookies are: cookieSetAsFirstParty = 1
 
-No stored Private Click Measurement data.
+--------
+Frame: '<!--frame5-->'
+--------
+Cookies are: cookieSetAsFirstParty = 1

Modified: trunk/LayoutTests/http/tests/privateClickMeasurement/send-attribution-conversion-request.html (275136 => 275137)


--- trunk/LayoutTests/http/tests/privateClickMeasurement/send-attribution-conversion-request.html	2021-03-27 18:31:30 UTC (rev 275136)
+++ trunk/LayoutTests/http/tests/privateClickMeasurement/send-attribution-conversion-request.html	2021-03-27 18:52:51 UTC (rev 275137)
@@ -43,23 +43,35 @@
         document.body.appendChild(iframeElement);
     }
 
+    let reportsReceived = 0;
     function appendConversionDataIframeAndFinish() {
-        testRunner.dumpPrivateClickMeasurement();
         document.body.removeChild(document.getElementById("targetLink"));
         document.body.removeChild(document.getElementById("pixel"));
 
         appendIframe("http://127.0.0.1:8000/cookies/resources/echo-cookies.php");
-        appendIframe("http://127.0.0.1:8000/privateClickMeasurement/resources/getConversionData.py?timeout_ms=2000&nonce=" + nonce, function() {
+        // Click source.
+        appendIframe("http://127.0.0.1:8000/privateClickMeasurement/resources/getConversionData.py?timeout_ms=2000&recipient=ClickSource&nonce=" + nonce, function() {
             appendIframe("http://127.0.0.1:8000/cookies/resources/echo-cookies.php", function() {
-                tearDownAndFinish();
+                reportsReceived++;
+                if (reportsReceived >= 2)
+                    tearDownAndFinish();
             });
         });
+
+        // Click destination.
+        appendIframe("http://127.0.0.1:8000/privateClickMeasurement/resources/getConversionData.py?timeout_ms=2000&recipient=ClickDestination&nonce=" + nonce, function() {
+            appendIframe("http://127.0.0.1:8000/cookies/resources/echo-cookies.php", function() {
+                reportsReceived++;
+                if (reportsReceived >= 2)
+                    tearDownAndFinish();
+            });
+        });
     }
 
     function runTest() {
         if (window.testRunner) {
             if (window.location.search === "?stepTwo") {
-                testRunner.setPrivateClickMeasurementAttributionReportURLsForTesting("http://127.0.0.1:8000/privateClickMeasurement/resources/conversionReport.py?nonce=" + nonce, "http://localhost:8000/privateClickMeasurement/resources/conversionReport.py?nonce=" + nonce);
+                testRunner.setPrivateClickMeasurementAttributionReportURLsForTesting("http://127.0.0.1:8000/privateClickMeasurement/resources/conversionReport.py?recipient=ClickSource&nonce=" + nonce, "http://localhost:8000/privateClickMeasurement/resources/conversionReport.py?recipient=ClickDestination&nonce=" + nonce);
                 let imageElement = document.createElement("img");
                 imageElement.src = "" + nonce;
                 imageElement.id = "pixel";

Modified: trunk/Source/WebCore/ChangeLog (275136 => 275137)


--- trunk/Source/WebCore/ChangeLog	2021-03-27 18:31:30 UTC (rev 275136)
+++ trunk/Source/WebCore/ChangeLog	2021-03-27 18:52:51 UTC (rev 275137)
@@ -1,3 +1,36 @@
+2021-03-27  Kate Cheney  <[email protected]>
+
+        PCM: Send report to both click source and attribution destination website
+        https://bugs.webkit.org/show_bug.cgi?id=223615
+        <rdar://problem/75849443>
+
+        Reviewed by Brent Fulgham.
+
+        Introduce 2 new structs for storing the earliest time to send and 
+        seconds until send for the source and destination sites.
+
+        * loader/PrivateClickMeasurement.cpp:
+        (WebCore::PrivateClickMeasurement::isValid const):
+        (WebCore::PrivateClickMeasurement::hasPreviouslyBeenReported):
+        (WebCore::randomlyBetweenTwentyFourAndFortyEightHours):
+        (WebCore::PrivateClickMeasurement::attributeAndGetEarliestTimeToSend):
+        * loader/PrivateClickMeasurement.h:
+        (WebCore::PrivateClickMeasurement::AttributionSecondsUntilSendData::hasValidSecondsUntilSendValues):
+        (WebCore::PrivateClickMeasurement::AttributionSecondsUntilSendData::minSecondsUntilSend):
+        (WebCore::PrivateClickMeasurement::AttributionSecondsUntilSendData::encode const):
+        (WebCore::PrivateClickMeasurement::AttributionSecondsUntilSendData::decode):
+        (WebCore::PrivateClickMeasurement::AttributionTimeToSendData::earliestTimeToSend):
+        (WebCore::PrivateClickMeasurement::AttributionTimeToSendData::latestTimeToSend):
+        (WebCore::PrivateClickMeasurement::AttributionTimeToSendData::attributionReportEndpoint):
+        (WebCore::PrivateClickMeasurement::AttributionTimeToSendData::encode const):
+        (WebCore::PrivateClickMeasurement::AttributionTimeToSendData::decode):
+        (WebCore::PrivateClickMeasurement::timesToSend const):
+        (WebCore::PrivateClickMeasurement::setTimesToSend):
+        (WebCore::PrivateClickMeasurement::encode const):
+        (WebCore::PrivateClickMeasurement::decode):
+        (WebCore::PrivateClickMeasurement::earliestTimeToSend const): Deleted.
+        (WebCore::PrivateClickMeasurement::setEarliestTimeToSend): Deleted.
+
 2021-03-27  Simon Fraser  <[email protected]>
 
         Remove DisplayRefreshMonitor::handleDisplayRefreshedNotificationOnMainThread()

Modified: trunk/Source/WebCore/loader/PrivateClickMeasurement.cpp (275136 => 275137)


--- trunk/Source/WebCore/loader/PrivateClickMeasurement.cpp	2021-03-27 18:31:30 UTC (rev 275136)
+++ trunk/Source/WebCore/loader/PrivateClickMeasurement.cpp	2021-03-27 18:52:51 UTC (rev 275137)
@@ -55,7 +55,7 @@
         && m_sourceID.isValid()
         && !m_sourceSite.registrableDomain.isEmpty()
         && !m_destinationSite.registrableDomain.isEmpty()
-        && m_earliestTimeToSend;
+        && (m_timesToSend.sourceEarliestTimeToSend || m_timesToSend.destinationEarliestTimeToSend);
 }
 
 Expected<PrivateClickMeasurement::AttributionTriggerData, String> PrivateClickMeasurement::parseAttributionRequest(const URL& redirectURL)
@@ -92,8 +92,18 @@
     return makeUnexpected("[Private Click Measurement] Conversion was not accepted because the URL path contained unrecognized parts."_s);
 }
 
-Optional<Seconds> PrivateClickMeasurement::attributeAndGetEarliestTimeToSend(AttributionTriggerData&& attributionTriggerData)
+bool PrivateClickMeasurement::hasPreviouslyBeenReported()
 {
+    return !m_timesToSend.sourceEarliestTimeToSend || !m_timesToSend.destinationEarliestTimeToSend;
+}
+
+static Seconds randomlyBetweenTwentyFourAndFortyEightHours()
+{
+    return 24_h + Seconds(randomNumber() * (24_h).value());
+}
+
+PrivateClickMeasurement::AttributionSecondsUntilSendData PrivateClickMeasurement::attributeAndGetEarliestTimeToSend(AttributionTriggerData&& attributionTriggerData)
+{
     if (!attributionTriggerData.isValid() || (m_attributionTriggerData && m_attributionTriggerData->priority >= attributionTriggerData.priority))
         return { };
 
@@ -100,9 +110,11 @@
     m_attributionTriggerData = WTFMove(attributionTriggerData);
     // 24-48 hour delay before sending. This helps privacy since the conversion and the attribution
     // requests are detached and the time of the attribution does not reveal the time of the conversion.
-    auto seconds = 24_h + Seconds(randomNumber() * (24_h).value());
-    m_earliestTimeToSend = WallTime::now() + seconds;
-    return seconds;
+    auto sourceSecondsUntilSend = randomlyBetweenTwentyFourAndFortyEightHours();
+    auto destinationSecondsUntilSend = randomlyBetweenTwentyFourAndFortyEightHours();
+    m_timesToSend = { WallTime::now() + sourceSecondsUntilSend, WallTime::now() + destinationSecondsUntilSend };
+
+    return AttributionSecondsUntilSendData { sourceSecondsUntilSend, destinationSecondsUntilSend };
 }
 
 bool PrivateClickMeasurement::hasHigherPriorityThan(const PrivateClickMeasurement& other) const

Modified: trunk/Source/WebCore/loader/PrivateClickMeasurement.h (275136 => 275137)


--- trunk/Source/WebCore/loader/PrivateClickMeasurement.h	2021-03-27 18:31:30 UTC (rev 275136)
+++ trunk/Source/WebCore/loader/PrivateClickMeasurement.h	2021-03-27 18:52:51 UTC (rev 275137)
@@ -53,6 +53,7 @@
     using PriorityValue = uint32_t;
 
     enum class PcmDataCarried : bool { NonPersonallyIdentifiable, PersonallyIdentifiable };
+    enum class AttributionReportEndpoint : bool { Source, Destination };
 
     struct SourceID {
         static constexpr uint32_t MaxEntropy = 255;
@@ -247,6 +248,116 @@
         template<class Decoder> static Optional<AttributionTriggerData> decode(Decoder&);
     };
 
+    struct AttributionSecondsUntilSendData {
+        Optional<Seconds> sourceSeconds;
+        Optional<Seconds> destinationSeconds;
+
+        bool hasValidSecondsUntilSendValues()
+        {
+            return sourceSeconds && destinationSeconds;
+        }
+
+        Optional<Seconds> minSecondsUntilSend()
+        {
+            if (!sourceSeconds && !destinationSeconds)
+                return WTF::nullopt;
+
+            if (sourceSeconds && destinationSeconds)
+                return std::min(sourceSeconds, destinationSeconds);
+
+            return sourceSeconds ? sourceSeconds : destinationSeconds;
+        }
+
+        template<class Encoder>
+        void encode(Encoder& encoder) const
+        {
+            encoder << sourceSeconds << destinationSeconds;
+        }
+
+        template<class Decoder>
+        static Optional<AttributionSecondsUntilSendData> decode(Decoder& decoder)
+        {
+            Optional<Optional<Seconds>> sourceSeconds;
+            decoder >> sourceSeconds;
+            if (!sourceSeconds)
+                return WTF::nullopt;
+
+            Optional<Optional<Seconds>> destinationSeconds;
+            decoder >> destinationSeconds;
+            if (!destinationSeconds)
+                return WTF::nullopt;
+
+            return AttributionSecondsUntilSendData { WTFMove(*sourceSeconds), WTFMove(*destinationSeconds) };
+        }
+    };
+
+    struct AttributionTimeToSendData {
+        Optional<WallTime> sourceEarliestTimeToSend;
+        Optional<WallTime> destinationEarliestTimeToSend;
+
+        Optional<WallTime> earliestTimeToSend()
+        {
+            if (!sourceEarliestTimeToSend && !destinationEarliestTimeToSend)
+                return WTF::nullopt;
+
+            if (sourceEarliestTimeToSend && destinationEarliestTimeToSend)
+                return std::min(sourceEarliestTimeToSend, destinationEarliestTimeToSend);
+
+            return sourceEarliestTimeToSend ? sourceEarliestTimeToSend : destinationEarliestTimeToSend;
+        }
+
+        Optional<WallTime> latestTimeToSend()
+        {
+            if (!sourceEarliestTimeToSend && !destinationEarliestTimeToSend)
+                return WTF::nullopt;
+
+            if (sourceEarliestTimeToSend && destinationEarliestTimeToSend)
+                return std::max(sourceEarliestTimeToSend, destinationEarliestTimeToSend);
+
+            return sourceEarliestTimeToSend ? sourceEarliestTimeToSend : destinationEarliestTimeToSend;
+        }
+
+        Optional<AttributionReportEndpoint> attributionReportEndpoint()
+        {
+            if (sourceEarliestTimeToSend && destinationEarliestTimeToSend) {
+                if (*sourceEarliestTimeToSend < *destinationEarliestTimeToSend)
+                    return AttributionReportEndpoint::Source;
+
+                return AttributionReportEndpoint::Destination;
+            }
+
+            if (sourceEarliestTimeToSend)
+                return AttributionReportEndpoint::Source;
+
+            if (destinationEarliestTimeToSend)
+                return AttributionReportEndpoint::Destination;
+
+            return WTF::nullopt;
+        }
+
+        template<class Encoder>
+        void encode(Encoder& encoder) const
+        {
+            encoder << sourceEarliestTimeToSend << destinationEarliestTimeToSend;
+        }
+
+        template<class Decoder>
+        static Optional<AttributionTimeToSendData> decode(Decoder& decoder)
+        {
+            Optional<Optional<WallTime>> sourceEarliestTimeToSend;
+            decoder >> sourceEarliestTimeToSend;
+            if (!sourceEarliestTimeToSend)
+                return WTF::nullopt;
+
+            Optional<Optional<WallTime>> destinationEarliestTimeToSend;
+            decoder >> destinationEarliestTimeToSend;
+            if (!destinationEarliestTimeToSend)
+                return WTF::nullopt;
+
+            return AttributionTimeToSendData { WTFMove(*sourceEarliestTimeToSend), WTFMove(*destinationEarliestTimeToSend) };
+        }
+    };
+
     PrivateClickMeasurement() = default;
     PrivateClickMeasurement(SourceID sourceID, const SourceSite& sourceSite, const AttributionDestinationSite& destinationSite, String&& sourceDescription = { }, String&& purchaser = { }, WallTime timeOfAdClick = WallTime::now())
         : m_sourceID { sourceID }
@@ -260,7 +371,7 @@
 
     WEBCORE_EXPORT static const Seconds maxAge();
     WEBCORE_EXPORT static Expected<AttributionTriggerData, String> parseAttributionRequest(const URL& redirectURL);
-    WEBCORE_EXPORT Optional<Seconds> attributeAndGetEarliestTimeToSend(AttributionTriggerData&&);
+    WEBCORE_EXPORT AttributionSecondsUntilSendData attributeAndGetEarliestTimeToSend(AttributionTriggerData&&);
     WEBCORE_EXPORT bool hasHigherPriorityThan(const PrivateClickMeasurement&) const;
     WEBCORE_EXPORT URL attributionReportSourceURL() const;
     WEBCORE_EXPORT URL attributionReportAttributeOnURL() const;
@@ -268,8 +379,9 @@
     const SourceSite& sourceSite() const { return m_sourceSite; };
     const AttributionDestinationSite& destinationSite() const { return m_destinationSite; };
     WallTime timeOfAdClick() const { return m_timeOfAdClick; }
-    Optional<WallTime> earliestTimeToSend() const { return m_earliestTimeToSend; };
-    void setEarliestTimeToSend(WallTime time) { m_earliestTimeToSend = time; }
+    WEBCORE_EXPORT bool hasPreviouslyBeenReported();
+    AttributionTimeToSendData timesToSend() const { return m_timesToSend; };
+    void setTimesToSend(AttributionTimeToSendData data) { m_timesToSend = data; }
     const SourceID& sourceID() const { return m_sourceID; }
     Optional<AttributionTriggerData> attributionTriggerData() { return m_attributionTriggerData; }
     void setAttribution(AttributionTriggerData&& attributionTriggerData) { m_attributionTriggerData = WTFMove(attributionTriggerData); }
@@ -327,7 +439,7 @@
     WallTime m_timeOfAdClick;
 
     Optional<AttributionTriggerData> m_attributionTriggerData;
-    Optional<WallTime> m_earliestTimeToSend;
+    AttributionTimeToSendData m_timesToSend;
 
     struct SourceUnlinkableToken {
 #if PLATFORM(COCOA)
@@ -354,7 +466,7 @@
         << m_timeOfAdClick
         << m_ephemeralSourceNonce
         << m_attributionTriggerData
-        << m_earliestTimeToSend;
+        << m_timesToSend;
 }
 
 template<class Decoder>
@@ -400,9 +512,9 @@
     if (!attributionTriggerData)
         return WTF::nullopt;
     
-    Optional<Optional<WallTime>> earliestTimeToSend;
-    decoder >> earliestTimeToSend;
-    if (!earliestTimeToSend)
+    Optional<AttributionTimeToSendData> timesToSend;
+    decoder >> timesToSend;
+    if (!timesToSend)
         return WTF::nullopt;
     
     PrivateClickMeasurement attribution {
@@ -415,7 +527,7 @@
     };
     attribution.m_ephemeralSourceNonce = WTFMove(*ephemeralSourceNonce);
     attribution.m_attributionTriggerData = WTFMove(*attributionTriggerData);
-    attribution.m_earliestTimeToSend = WTFMove(*earliestTimeToSend);
+    attribution.m_timesToSend = WTFMove(*timesToSend);
     
     return attribution;
 }

Modified: trunk/Source/WebKit/ChangeLog (275136 => 275137)


--- trunk/Source/WebKit/ChangeLog	2021-03-27 18:31:30 UTC (rev 275136)
+++ trunk/Source/WebKit/ChangeLog	2021-03-27 18:52:51 UTC (rev 275137)
@@ -1,3 +1,66 @@
+2021-03-27  Kate Cheney  <[email protected]>
+
+        PCM: Send report to both click source and attribution destination website
+        https://bugs.webkit.org/show_bug.cgi?id=223615
+        <rdar://problem/75849443>
+
+        Reviewed by Brent Fulgham.
+
+        * NetworkProcess/Classifier/ResourceLoadStatisticsDatabaseStore.cpp:
+        Move these queries to the correct INSERT OR REPLACE category. Stop
+        inserting null for earliestTimeToSendToDestination and starting
+        binding a parameter to it now that we are supporting reports to both
+        sites.
+
+        Now that earliestTimeToSend* can be null if a report has been sent
+        to a site, we need queries to set the value to null, and also need
+        to sort attributions by the minimum of either the two reporting times,
+        or the non-null time if one is null.
+
+        (WebKit::ResourceLoadStatisticsDatabaseStore::destroyStatements):
+        (WebKit::ResourceLoadStatisticsDatabaseStore::buildPrivateClickMeasurementFromDatabase):
+        (WebKit::ResourceLoadStatisticsDatabaseStore::insertPrivateClickMeasurement):
+        (WebKit::ResourceLoadStatisticsDatabaseStore::attributePrivateClickMeasurement):
+        We should not attribute a PCM value if it has already been reported to
+        either the source or destination. This is covered by checking
+        secondsUntilSend.hasValidSecondsUntilSendValues() and 
+        previouslyAttributed.value().hasPreviouslyBeenReported() before
+        inserting anything into the attributed PCM table.
+
+        (WebKit::ResourceLoadStatisticsDatabaseStore::earliestTimesToSend):
+        (WebKit::ResourceLoadStatisticsDatabaseStore::markReportAsSentToSource):
+        (WebKit::ResourceLoadStatisticsDatabaseStore::markReportAsSentToDestination):
+        (WebKit::ResourceLoadStatisticsDatabaseStore::clearSentAttribution):
+        Clear a value from the attributed table only if it has been sent to
+        both source and destination site. Otherwise, set the corresponding
+        attribution endpoint to null so we don't send it here again.
+
+        (WebKit::ResourceLoadStatisticsDatabaseStore::markAttributedPrivateClickMeasurementsAsExpiredForTesting):
+        For the sake of testing we can set the destination earliest time to
+        send to null. We are only confirming here that the expired attribution
+        gets sent.
+
+        * NetworkProcess/Classifier/ResourceLoadStatisticsDatabaseStore.h:
+        * NetworkProcess/Classifier/ResourceLoadStatisticsMemoryStore.h:
+        * NetworkProcess/Classifier/ResourceLoadStatisticsStore.h:
+        * NetworkProcess/Classifier/WebResourceLoadStatisticsStore.cpp:
+        (WebKit::WebResourceLoadStatisticsStore::attributePrivateClickMeasurement):
+        (WebKit::WebResourceLoadStatisticsStore::clearSentAttribution):
+        * NetworkProcess/Classifier/WebResourceLoadStatisticsStore.h:
+        * NetworkProcess/PrivateClickMeasurementManager.cpp:
+        (WebKit::PrivateClickMeasurementManager::storeUnattributed):
+        (WebKit::PrivateClickMeasurementManager::getTokenPublicKey):
+        We currently have no way of setting the destination token URL site for
+        testing. To avoid flakiness, we should not make a ping load for the
+        token URL if we are reporting to the destination in test mode.
+
+        (WebKit::PrivateClickMeasurementManager::attribute):
+        (WebKit::PrivateClickMeasurementManager::fireConversionRequest):
+        (WebKit::PrivateClickMeasurementManager::fireConversionRequestImpl):
+        (WebKit::PrivateClickMeasurementManager::clearSentAttribution):
+        (WebKit::PrivateClickMeasurementManager::firePendingAttributionRequests):
+        * NetworkProcess/PrivateClickMeasurementManager.h:
+
 2021-03-27  Tyler Wilcock  <[email protected]>
 
         Remove DisplayRefreshMonitor::handleDisplayRefreshedNotificationOnMainThread()

Modified: trunk/Source/WebKit/NetworkProcess/Classifier/ResourceLoadStatisticsDatabaseStore.cpp (275136 => 275137)


--- trunk/Source/WebKit/NetworkProcess/Classifier/ResourceLoadStatisticsDatabaseStore.cpp	2021-03-27 18:31:30 UTC (rev 275136)
+++ trunk/Source/WebKit/NetworkProcess/Classifier/ResourceLoadStatisticsDatabaseStore.cpp	2021-03-27 18:52:51 UTC (rev 275137)
@@ -81,10 +81,6 @@
 constexpr auto topFrameUniqueRedirectsFromQuery = "INSERT OR IGNORE INTO TopFrameUniqueRedirectsFrom (targetDomainID, fromDomainID) SELECT ?, domainID FROM ObservedDomains WHERE registrableDomain in ( "_s;
 constexpr auto topFrameLoadedThirdPartyScriptsQuery = "INSERT OR IGNORE into TopFrameLoadedThirdPartyScripts (topFrameDomainID, subresourceDomainID) SELECT ?, domainID FROM ObservedDomains where registrableDomain in ( "_s;
 constexpr auto subresourceUniqueRedirectsFromQuery = "INSERT OR IGNORE INTO SubresourceUniqueRedirectsFrom (subresourceDomainID, fromDomainID) SELECT ?, domainID FROM ObservedDomains WHERE registrableDomain in ( "_s;
-constexpr auto insertUnattributedPrivateClickMeasurementQuery = "INSERT OR REPLACE INTO UnattributedPrivateClickMeasurement (sourceSiteDomainID, destinationSiteDomainID, "
-    "sourceID, timeOfAdClick, token, signature, keyID) VALUES (?, ?, ?, ?, ?, ?, ?)"_s;
-constexpr auto insertAttributedPrivateClickMeasurementQuery = "INSERT OR REPLACE INTO AttributedPrivateClickMeasurement (sourceSiteDomainID, destinationSiteDomainID, "
-    "sourceID, attributionTriggerData, priority, timeOfAdClick, earliestTimeToSendToSource, token, signature, keyID, earliestTimeToSendToDestination) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, null)"_s;
 
 // INSERT OR REPLACE Queries
 constexpr auto subframeUnderTopFrameDomainsQuery = "INSERT OR REPLACE into SubframeUnderTopFrameDomains (subFrameDomainID, lastUpdated, topFrameDomainID) SELECT ?, ?, domainID FROM ObservedDomains where registrableDomain in ( "_s;
@@ -91,6 +87,10 @@
 constexpr auto topFrameLinkDecorationsFromQuery = "INSERT OR REPLACE INTO TopFrameLinkDecorationsFrom (toDomainID, lastUpdated, fromDomainID) SELECT ?, ?, domainID FROM ObservedDomains WHERE registrableDomain in ( "_s;
 constexpr auto subresourceUnderTopFrameDomainsQuery = "INSERT OR REPLACE INTO SubresourceUnderTopFrameDomains (subresourceDomainID, lastUpdated, topFrameDomainID) SELECT ?, ?, domainID FROM ObservedDomains WHERE registrableDomain in ( "_s;
 constexpr auto subresourceUniqueRedirectsToQuery = "INSERT OR REPLACE INTO SubresourceUniqueRedirectsTo (subresourceDomainID, lastUpdated, toDomainID) SELECT ?, ?, domainID FROM ObservedDomains WHERE registrableDomain in ( "_s;
+constexpr auto insertUnattributedPrivateClickMeasurementQuery = "INSERT OR REPLACE INTO UnattributedPrivateClickMeasurement (sourceSiteDomainID, destinationSiteDomainID, "
+    "sourceID, timeOfAdClick, token, signature, keyID) VALUES (?, ?, ?, ?, ?, ?, ?)"_s;
+constexpr auto insertAttributedPrivateClickMeasurementQuery = "INSERT OR REPLACE INTO AttributedPrivateClickMeasurement (sourceSiteDomainID, destinationSiteDomainID, "
+    "sourceID, attributionTriggerData, priority, timeOfAdClick, earliestTimeToSendToSource, token, signature, keyID, earliestTimeToSendToDestination) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"_s;
 
 // EXISTS Queries
 constexpr auto subframeUnderTopFrameDomainExistsQuery = "SELECT EXISTS (SELECT 1 FROM SubframeUnderTopFrameDomains WHERE subFrameDomainID = ? "
@@ -116,6 +116,8 @@
 constexpr auto updateGrandfatheredQuery = "UPDATE ObservedDomains SET grandfathered = ? WHERE registrableDomain = ?"_s;
 constexpr auto updateIsScheduledForAllButCookieDataRemovalQuery = "UPDATE ObservedDomains SET isScheduledForAllButCookieDataRemoval = ? WHERE registrableDomain = ?"_s;
 constexpr auto setUnattributedPrivateClickMeasurementAsExpiredQuery = "UPDATE UnattributedPrivateClickMeasurement SET timeOfAdClick = -1.0"_s;
+constexpr auto markReportAsSentToSourceQuery = "UPDATE AttributedPrivateClickMeasurement SET earliestTimeToSendToSource = null WHERE sourceSiteDomainID = ? AND destinationSiteDomainID = ?"_s;
+constexpr auto markReportAsSentToDestinationQuery = "UPDATE AttributedPrivateClickMeasurement SET earliestTimeToSendToDestination = null WHERE sourceSiteDomainID = ? AND destinationSiteDomainID = ?"_s;
 
 // SELECT Queries
 constexpr auto domainIDFromStringQuery = "SELECT domainID FROM ObservedDomains WHERE registrableDomain = ?"_s;
@@ -135,9 +137,13 @@
     "UNION ALL SELECT topFrameDomainID FROM SubresourceUnderTopFrameDomains WHERE subresourceDomainID = ?"
     "UNION ALL SELECT toDomainID FROM SubresourceUniqueRedirectsTo WHERE subresourceDomainID = ?"_s;
 constexpr auto allUnattributedPrivateClickMeasurementAttributionsQuery = "SELECT * FROM UnattributedPrivateClickMeasurement"_s;
-constexpr auto allAttributedPrivateClickMeasurementQuery = "SELECT * FROM AttributedPrivateClickMeasurement ORDER BY earliestTimeToSendToSource"_s;
+constexpr auto allAttributedPrivateClickMeasurementQuery = "SELECT *, MIN(earliestTimeToSendToSource, earliestTimeToSendToDestination) as minVal "
+    "FROM AttributedPrivateClickMeasurement WHERE earliestTimeToSendToSource IS NOT NULL AND earliestTimeToSendToDestination IS NOT NULL "
+    "UNION ALL SELECT *, earliestTimeToSendToSource as minVal FROM AttributedPrivateClickMeasurement WHERE earliestTimeToSendToDestination IS NULL "
+    "UNION ALL SELECT *, earliestTimeToSendToDestination as minVal FROM AttributedPrivateClickMeasurement WHERE earliestTimeToSendToSource IS NULL ORDER BY minVal"_s;
 constexpr auto findUnattributedQuery = "SELECT * FROM UnattributedPrivateClickMeasurement WHERE sourceSiteDomainID = ? AND destinationSiteDomainID = ?"_s;
 constexpr auto findAttributedQuery = "SELECT * FROM AttributedPrivateClickMeasurement WHERE sourceSiteDomainID = ? AND destinationSiteDomainID = ?"_s;
+constexpr auto earliestTimesToSendQuery = "SELECT earliestTimeToSendToSource, earliestTimeToSendToDestination FROM AttributedPrivateClickMeasurement WHERE sourceSiteDomainID = ? AND destinationSiteDomainID = ?"_s;
 
 // EXISTS for testing queries
 constexpr auto linkDecorationExistsQuery = "SELECT EXISTS (SELECT * FROM TopFrameLinkDecorationsFrom WHERE toDomainID = ? OR fromDomainID = ?)"_s;
@@ -866,6 +872,9 @@
     m_findAttributedStatement = nullptr;
     m_updateAttributionsEarliestTimeToSendStatement = nullptr;
     m_removeUnattributedStatement = nullptr;
+    m_markReportAsSentToSourceStatement = nullptr;
+    m_markReportAsSentToDestinationStatement = nullptr;
+    m_earliestTimesToSendStatement = nullptr;
 }
 
 bool ResourceLoadStatisticsDatabaseStore::insertObservedDomain(const ResourceLoadStatistics& loadStatistics)
@@ -3047,12 +3056,23 @@
     if (attributionType == PrivateClickMeasurementAttributionType::Attributed) {
         auto attributionTriggerData = statement->getColumnInt(3);
         auto priority = statement->getColumnInt(4);
-        auto earliestTimeToSend = statement->getColumnDouble(6);
-        
+        auto sourceEarliestTimeToSendValue = statement->getColumnDouble(6);
+        auto destinationEarliestTimeToSendValue = statement->getColumnDouble(10);
+
         if (attributionTriggerData != -1)
             attribution.setAttribution(WebCore::PrivateClickMeasurement::AttributionTriggerData { static_cast<uint32_t>(attributionTriggerData), WebCore::PrivateClickMeasurement::Priority(priority) });
 
-        attribution.setEarliestTimeToSend(WallTime::fromRawSeconds(earliestTimeToSend));
+        Optional<WallTime> sourceEarliestTimeToSend;
+        Optional<WallTime> destinationEarliestTimeToSend;
+        
+        // A value of 0.0 indicates that the report has been sent to the respective site.
+        if (sourceEarliestTimeToSendValue > 0.0)
+            sourceEarliestTimeToSend = WallTime::fromRawSeconds(sourceEarliestTimeToSendValue);
+
+        if (destinationEarliestTimeToSendValue > 0.0)
+            destinationEarliestTimeToSend = WallTime::fromRawSeconds(destinationEarliestTimeToSendValue);
+
+        attribution.setTimesToSend({ sourceEarliestTimeToSend, destinationEarliestTimeToSend });
     }
 
     attribution.setSourceSecretToken({ token, signature, keyID });
@@ -3107,8 +3127,12 @@
     if (attributionType == PrivateClickMeasurementAttributionType::Attributed) {
         auto attributionTriggerData = attribution.attributionTriggerData() ? attribution.attributionTriggerData().value().data : -1;
         auto priority = attribution.attributionTriggerData() ? attribution.attributionTriggerData().value().priority : -1;
-        auto earliestTimeToSend = attribution.earliestTimeToSend() ? attribution.earliestTimeToSend().value().secondsSinceEpoch().value() : -1;
+        auto sourceEarliestTimeToSend = attribution.timesToSend().sourceEarliestTimeToSend ? attribution.timesToSend().sourceEarliestTimeToSend.value().secondsSinceEpoch().value() : -1;
+        auto destinationEarliestTimeToSend = attribution.timesToSend().destinationEarliestTimeToSend ? attribution.timesToSend().destinationEarliestTimeToSend.value().secondsSinceEpoch().value() : -1;
 
+        // We should never be inserting an attributed private click measurement value into the database without valid report times.
+        ASSERT(sourceEarliestTimeToSend != -1 && destinationEarliestTimeToSend != -1);
+
         auto statement = SQLiteStatement(m_database, insertAttributedPrivateClickMeasurementQuery);
         if (statement.prepare() != SQLITE_OK
             || statement.bindInt(1, *sourceData.second) != SQLITE_OK
@@ -3117,10 +3141,11 @@
             || statement.bindInt(4, attributionTriggerData) != SQLITE_OK
             || statement.bindInt(5, priority) != SQLITE_OK
             || statement.bindDouble(6, attribution.timeOfAdClick().secondsSinceEpoch().value()) != SQLITE_OK
-            || statement.bindDouble(7, earliestTimeToSend) != SQLITE_OK
+            || statement.bindDouble(7, sourceEarliestTimeToSend) != SQLITE_OK
             || statement.bindText(8, sourceUnlinkableToken ? sourceUnlinkableToken->tokenBase64URL : emptyString()) != SQLITE_OK
             || statement.bindText(9, sourceUnlinkableToken ? sourceUnlinkableToken->signatureBase64URL : emptyString()) != SQLITE_OK
             || statement.bindText(10, sourceUnlinkableToken ? sourceUnlinkableToken->keyIDBase64URL : emptyString()) != SQLITE_OK
+            || statement.bindDouble(11, destinationEarliestTimeToSend) != SQLITE_OK
             || statement.step() != SQLITE_DONE) {
             RELEASE_LOG_ERROR_IF_ALLOWED(m_sessionID, "%p - ResourceLoadStatisticsDatabaseStore::insertPrivateClickMeasurement insertAttributedPrivateClickMeasurementQuery, error message: %{private}s", this, m_database.lastErrorMsg());
             ASSERT_NOT_REACHED();
@@ -3172,7 +3197,7 @@
     }
 }
 
-Optional<Seconds> ResourceLoadStatisticsDatabaseStore::attributePrivateClickMeasurement(const SourceSite& sourceSite, const AttributionDestinationSite& destinationSite, AttributionTriggerData&& attributionTriggerData)
+Optional<PrivateClickMeasurement::AttributionSecondsUntilSendData> ResourceLoadStatisticsDatabaseStore::attributePrivateClickMeasurement(const SourceSite& sourceSite, const AttributionDestinationSite& destinationSite, AttributionTriggerData&& attributionTriggerData)
 {
     // We should always clear expired clicks from the database before scheduling an attribution.
     clearExpiredPrivateClickMeasurement();
@@ -3193,7 +3218,7 @@
         debugBroadcastConsoleMessage(MessageSource::PrivateClickMeasurement, MessageLevel::Info, makeString("[Private Click Measurement] Got an attribution with attribution trigger data: '"_s, data, "' and priority: '"_s, priority, "'."_s));
     }
 
-    auto secondsUntilSend = Seconds::infinity();
+    PrivateClickMeasurement::AttributionSecondsUntilSendData secondsUntilSend { WTF::nullopt, WTF::nullopt };
 
     auto attribution = findPrivateClickMeasurement(sourceSite, destinationSite);
     auto& previouslyUnattributed = attribution.first;
@@ -3202,15 +3227,19 @@
     if (previouslyUnattributed) {
         // Always convert the pending attribution and remove it from the unattributed map.
         removeUnattributed(*previouslyUnattributed);
-        if (auto optionalSecondsUntilSend = previouslyUnattributed.value().attributeAndGetEarliestTimeToSend(WTFMove(attributionTriggerData))) {
-            secondsUntilSend = *optionalSecondsUntilSend;
-            ASSERT(secondsUntilSend != Seconds::infinity());
-            if (UNLIKELY(debugModeEnabled())) {
-                RELEASE_LOG_INFO(PrivateClickMeasurement, "Converted a stored ad click with attribution trigger data: %{public}u and priority: %{public}u.", data, priority);
-                debugBroadcastConsoleMessage(MessageSource::PrivateClickMeasurement, MessageLevel::Info, makeString("[Private Click Measurement] Converted a stored ad click with attribution trigger data: '"_s, data, "' and priority: '"_s, priority, "'."_s));
-            }
+        secondsUntilSend = previouslyUnattributed.value().attributeAndGetEarliestTimeToSend(WTFMove(attributionTriggerData));
+
+        // We should always have a valid secondsUntilSend value for a previouslyUnattributed value because there can be no previous attribution with a higher priority.
+        if (!secondsUntilSend.hasValidSecondsUntilSendValues()) {
+            ASSERT_NOT_REACHED();
+            return WTF::nullopt;
         }
 
+        if (UNLIKELY(debugModeEnabled())) {
+            RELEASE_LOG_INFO(PrivateClickMeasurement, "Converted a stored ad click with attribution trigger data: %{public}u and priority: %{public}u.", data, priority);
+            debugBroadcastConsoleMessage(MessageSource::PrivateClickMeasurement, MessageLevel::Info, makeString("[Private Click Measurement] Converted a stored ad click with attribution trigger data: '"_s, data, "' and priority: '"_s, priority, "'."_s));
+        }
+
         // If there is no previous attribution, or the new attribution has higher priority, insert/update the database.
         if (!previouslyAttributed || previouslyUnattributed.value().hasHigherPriorityThan(*previouslyAttributed)) {
             insertPrivateClickMeasurement(WTFMove(*previouslyUnattributed), PrivateClickMeasurementAttributionType::Attributed);
@@ -3221,13 +3250,15 @@
             }
         }
     } else if (previouslyAttributed) {
-        // If we have no new attribution, re-attribute the old one to respect the new priority.
-        if (auto optionalSecondsUntilSend = previouslyAttributed.value().attributeAndGetEarliestTimeToSend(WTFMove(attributionTriggerData))) {
+        // If we have no new attribution, re-attribute the old one to respect the new priority, but only if this report has
+        // not been sent to the source or destination site yet.
+        if (!previouslyAttributed.value().hasPreviouslyBeenReported()) {
+            auto secondsUntilSend = previouslyAttributed.value().attributeAndGetEarliestTimeToSend(WTFMove(attributionTriggerData));
+            if (!secondsUntilSend.hasValidSecondsUntilSendValues())
+                return WTF::nullopt;
+
             insertPrivateClickMeasurement(WTFMove(*previouslyAttributed), PrivateClickMeasurementAttributionType::Attributed);
 
-            secondsUntilSend = *optionalSecondsUntilSend;
-            ASSERT(secondsUntilSend != Seconds::infinity());
-
             if (UNLIKELY(debugModeEnabled())) {
                 RELEASE_LOG_INFO(PrivateClickMeasurement, "Re-converted an ad click with a new one with attribution trigger data: %{public}u and priority: %{public}u because it had higher priority.", data, priority);
                 debugBroadcastConsoleMessage(MessageSource::PrivateClickMeasurement, MessageLevel::Info, makeString("[Private Click Measurement] Re-converted an ad click with a new one with attribution trigger data: '"_s, data, "' and priority: '"_s, priority, "'' because it had higher priority."_s));
@@ -3235,7 +3266,7 @@
         }
     }
 
-    if (secondsUntilSend == Seconds::infinity())
+    if (!secondsUntilSend.hasValidSecondsUntilSendValues())
         return WTF::nullopt;
 
     return secondsUntilSend;
@@ -3398,14 +3429,97 @@
     return builder.toString();
 }
 
-void ResourceLoadStatisticsDatabaseStore::clearSentAttribution(WebCore::PrivateClickMeasurement&& attribution)
+std::pair<Optional<SourceEarliestTimeToSend>, Optional<DestinationEarliestTimeToSend>> ResourceLoadStatisticsDatabaseStore::earliestTimesToSend(const WebCore::PrivateClickMeasurement& attribution)
 {
     auto sourceSiteDomainID = domainID(attribution.sourceSite().registrableDomain);
     auto destinationSiteDomainID = domainID(attribution.destinationSite().registrableDomain);
 
     if (!sourceSiteDomainID || !destinationSiteDomainID)
+        return std::make_pair(WTF::nullopt, WTF::nullopt);
+
+    auto scopedStatement = this->scopedStatement(m_earliestTimesToSendStatement, earliestTimesToSendQuery, "earliestTimesToSend"_s);
+
+    if (!scopedStatement
+        || scopedStatement->bindInt(1, *sourceSiteDomainID) != SQLITE_OK
+        || scopedStatement->bindInt(2, *destinationSiteDomainID) != SQLITE_OK
+        || scopedStatement->step() != SQLITE_ROW) {
+        RELEASE_LOG_ERROR(Network, "ResourceLoadStatisticsDatabaseStore::earliestTimesToSend, error message: %" PUBLIC_LOG_STRING, m_database.lastErrorMsg());
+        ASSERT_NOT_REACHED();
+    }
+
+    Optional<SourceEarliestTimeToSend> earliestTimeToSendToSource;
+    Optional<DestinationEarliestTimeToSend> earliestTimeToSendToDestination;
+    
+    // A value of 0.0 indicates that the report has been sent to the respective site.
+    if (scopedStatement->getColumnDouble(0) > 0.0)
+        earliestTimeToSendToSource = scopedStatement->getColumnDouble(0);
+    
+    if (scopedStatement->getColumnDouble(1) > 0.0)
+        earliestTimeToSendToDestination = scopedStatement->getColumnDouble(1);
+    
+    return std::make_pair(earliestTimeToSendToSource, earliestTimeToSendToDestination);
+}
+
+void ResourceLoadStatisticsDatabaseStore::markReportAsSentToSource(SourceDomainID sourceSiteDomainID, DestinationDomainID destinationSiteDomainID)
+{
+    auto scopedStatement = this->scopedStatement(m_markReportAsSentToSourceStatement, markReportAsSentToSourceQuery, "markReportAsSentToSource"_s);
+
+    if (!scopedStatement
+        || scopedStatement->bindInt(1, sourceSiteDomainID) != SQLITE_OK
+        || scopedStatement->bindInt(2, destinationSiteDomainID) != SQLITE_OK
+        || scopedStatement->step() != SQLITE_DONE) {
+        RELEASE_LOG_ERROR(Network, "ResourceLoadStatisticsDatabaseStore::markReportAsSentToSource, error message: %" PUBLIC_LOG_STRING, m_database.lastErrorMsg());
+        ASSERT_NOT_REACHED();
+    }
+}
+
+void ResourceLoadStatisticsDatabaseStore::markReportAsSentToDestination(SourceDomainID sourceSiteDomainID, DestinationDomainID destinationSiteDomainID)
+{
+    auto scopedStatement = this->scopedStatement(m_markReportAsSentToDestinationStatement, markReportAsSentToDestinationQuery, "markReportAsSentToDestination"_s);
+
+    if (!scopedStatement
+        || scopedStatement->bindInt(1, sourceSiteDomainID) != SQLITE_OK
+        || scopedStatement->bindInt(2, destinationSiteDomainID) != SQLITE_OK
+        || scopedStatement->step() != SQLITE_DONE) {
+        RELEASE_LOG_ERROR(Network, "ResourceLoadStatisticsDatabaseStore::markReportAsSentToDestination, error message: %" PUBLIC_LOG_STRING, m_database.lastErrorMsg());
+        ASSERT_NOT_REACHED();
+    }
+}
+
+void ResourceLoadStatisticsDatabaseStore::clearSentAttribution(WebCore::PrivateClickMeasurement&& attribution, PrivateClickMeasurement::AttributionReportEndpoint attributionReportEndpoint)
+{    
+    auto timesToSend = earliestTimesToSend(attribution);
+    auto sourceEarliestTimeToSend = timesToSend.first;
+    auto destinationEarliestTimeToSend = timesToSend.second;
+
+    auto sourceSiteDomainID = domainID(attribution.sourceSite().registrableDomain);
+    auto destinationSiteDomainID = domainID(attribution.destinationSite().registrableDomain);
+
+    if (!sourceSiteDomainID || !destinationSiteDomainID)
         return;
 
+    switch (attributionReportEndpoint) {
+    case PrivateClickMeasurement::AttributionReportEndpoint::Source:
+        if (!sourceEarliestTimeToSend) {
+            ASSERT_NOT_REACHED();
+            return;
+        }
+        markReportAsSentToSource(*sourceSiteDomainID, *destinationSiteDomainID);
+        sourceEarliestTimeToSend = WTF::nullopt;
+        break;
+    case PrivateClickMeasurement::AttributionReportEndpoint::Destination:
+        if (!destinationEarliestTimeToSend) {
+            ASSERT_NOT_REACHED();
+            return;
+        }
+        markReportAsSentToDestination(*sourceSiteDomainID, *destinationSiteDomainID);
+        destinationEarliestTimeToSend = WTF::nullopt;
+    }
+
+    // Don't clear the attribute from the database unless it has been reported both to the source and destination site.
+    if (destinationEarliestTimeToSend || sourceEarliestTimeToSend)
+        return;
+
     SQLiteStatement clearAttributedStatement(m_database, "DELETE FROM AttributedPrivateClickMeasurement WHERE sourceSiteDomainID = ? AND destinationSiteDomainID = ?"_s);
     if (clearAttributedStatement.prepare() != SQLITE_OK
         || clearAttributedStatement.bindInt(1, *sourceSiteDomainID) != SQLITE_OK
@@ -3419,14 +3533,23 @@
 void ResourceLoadStatisticsDatabaseStore::markAttributedPrivateClickMeasurementsAsExpiredForTesting()
 {
     auto expiredTimeToSend = WallTime::now() - 1_h;
-    
-    auto statement = SQLiteStatement(m_database, "UPDATE AttributedPrivateClickMeasurement SET earliestTimeToSendToSource = ?");
-    if (statement.prepare() != SQLITE_OK
-        || statement.bindInt(1, expiredTimeToSend.secondsSinceEpoch().value()) != SQLITE_OK
-        || statement.step() != SQLITE_DONE) {
-        RELEASE_LOG_ERROR_IF_ALLOWED(m_sessionID, "%p - ResourceLoadStatisticsDatabaseStore::insertPrivateClickMeasurement, error message: %{private}s", this, m_database.lastErrorMsg());
+
+    auto earliestTimeToSendToSourceStatement = SQLiteStatement(m_database, "UPDATE AttributedPrivateClickMeasurement SET earliestTimeToSendToSource = ?");
+    auto earliestTimeToSendToDestinationStatement = SQLiteStatement(m_database, "UPDATE AttributedPrivateClickMeasurement SET earliestTimeToSendToDestination = null");
+
+    if (earliestTimeToSendToSourceStatement.prepare() != SQLITE_OK
+        || earliestTimeToSendToSourceStatement.bindInt(1, expiredTimeToSend.secondsSinceEpoch().value()) != SQLITE_OK
+        || earliestTimeToSendToSourceStatement.step() != SQLITE_DONE) {
+        RELEASE_LOG_ERROR_IF_ALLOWED(m_sessionID, "%p - ResourceLoadStatisticsDatabaseStore::markAttributedPrivateClickMeasurementsAsExpiredForTesting, error message: %{private}s", this, m_database.lastErrorMsg());
         ASSERT_NOT_REACHED();
     }
+
+    if (earliestTimeToSendToDestinationStatement.prepare() != SQLITE_OK
+        || earliestTimeToSendToDestinationStatement.step() != SQLITE_DONE) {
+        RELEASE_LOG_ERROR_IF_ALLOWED(m_sessionID, "%p - ResourceLoadStatisticsDatabaseStore::markAttributedPrivateClickMeasurementsAsExpiredForTesting, error message: %{private}s", this, m_database.lastErrorMsg());
+        ASSERT_NOT_REACHED();
+    }
+
     return;
 }
 

Modified: trunk/Source/WebKit/NetworkProcess/Classifier/ResourceLoadStatisticsDatabaseStore.h (275136 => 275137)


--- trunk/Source/WebKit/NetworkProcess/Classifier/ResourceLoadStatisticsDatabaseStore.h	2021-03-27 18:31:30 UTC (rev 275136)
+++ trunk/Source/WebKit/NetworkProcess/Classifier/ResourceLoadStatisticsDatabaseStore.h	2021-03-27 18:52:51 UTC (rev 275137)
@@ -64,6 +64,10 @@
 using ExpectedColumns = Vector<String>;
 using ExistingColumnName = String;
 using ExpectedColumnName = String;
+using SourceEarliestTimeToSend = double;
+using DestinationEarliestTimeToSend = double;
+using SourceDomainID = unsigned;
+using DestinationDomainID = unsigned;
 
 // This is always constructed / used / destroyed on the WebResourceLoadStatisticsStore's statistics queue.
 class ResourceLoadStatisticsDatabaseStore final : public ResourceLoadStatisticsStore {
@@ -138,12 +142,12 @@
     // Private Click Measurement.
     void insertPrivateClickMeasurement(WebCore::PrivateClickMeasurement&&, PrivateClickMeasurementAttributionType) override;
     void markAllUnattributedPrivateClickMeasurementAsExpiredForTesting() override;
-    Optional<Seconds> attributePrivateClickMeasurement(const WebCore::PrivateClickMeasurement::SourceSite&, const WebCore::PrivateClickMeasurement::AttributionDestinationSite&, WebCore::PrivateClickMeasurement::AttributionTriggerData&&) override;
+    Optional<WebCore::PrivateClickMeasurement::AttributionSecondsUntilSendData> attributePrivateClickMeasurement(const WebCore::PrivateClickMeasurement::SourceSite&, const WebCore::PrivateClickMeasurement::AttributionDestinationSite&, WebCore::PrivateClickMeasurement::AttributionTriggerData&&) override;
     Vector<WebCore::PrivateClickMeasurement> allAttributedPrivateClickMeasurement() override;
     void clearPrivateClickMeasurement(Optional<RegistrableDomain>) override;
     void clearExpiredPrivateClickMeasurement() override;
     String privateClickMeasurementToString() override;
-    void clearSentAttribution(WebCore::PrivateClickMeasurement&&) override;
+    void clearSentAttribution(WebCore::PrivateClickMeasurement&&, WebCore::PrivateClickMeasurement::AttributionReportEndpoint) override;
     void markAttributedPrivateClickMeasurementsAsExpiredForTesting() override;
     Vector<String> columnsForTable(const String&);
 
@@ -193,6 +197,10 @@
     Vector<RegistrableDomain> domainsWithUserInteractionAsFirstParty() const;
     HashMap<TopFrameDomain, SubResourceDomain> domainsWithStorageAccess() const;
 
+    void markReportAsSentToDestination(SourceDomainID, DestinationDomainID);
+    void markReportAsSentToSource(SourceDomainID, DestinationDomainID);
+    std::pair<Optional<SourceEarliestTimeToSend>, Optional<DestinationEarliestTimeToSend>> earliestTimesToSend(const WebCore::PrivateClickMeasurement&);
+
     struct DomainData {
         unsigned domainID;
         RegistrableDomain registrableDomain;
@@ -289,6 +297,7 @@
     mutable std::unique_ptr<WebCore::SQLiteStatement> m_uniqueRedirectExistsStatement;
     mutable std::unique_ptr<WebCore::SQLiteStatement> m_observedDomainsExistsStatement;
     mutable std::unique_ptr<WebCore::SQLiteStatement> m_removeAllDataStatement;
+    mutable std::unique_ptr<WebCore::SQLiteStatement> m_earliestTimesToSendStatement;
     std::unique_ptr<WebCore::SQLiteStatement> m_insertUnattributedPrivateClickMeasurementStatement;
     std::unique_ptr<WebCore::SQLiteStatement> m_insertAttributedPrivateClickMeasurementStatement;
     std::unique_ptr<WebCore::SQLiteStatement> m_setUnattributedPrivateClickMeasurementAsExpiredStatement;
@@ -301,7 +310,9 @@
     std::unique_ptr<WebCore::SQLiteStatement> m_findAttributedStatement;
     std::unique_ptr<WebCore::SQLiteStatement> m_updateAttributionsEarliestTimeToSendStatement;
     std::unique_ptr<WebCore::SQLiteStatement> m_removeUnattributedStatement;
-    
+    std::unique_ptr<WebCore::SQLiteStatement> m_markReportAsSentToSourceStatement;
+    std::unique_ptr<WebCore::SQLiteStatement> m_markReportAsSentToDestinationStatement;
+
     PAL::SessionID m_sessionID;
     bool m_isNewResourceLoadStatisticsDatabaseFile { false };
     unsigned m_operatingDatesSize { 0 };

Modified: trunk/Source/WebKit/NetworkProcess/Classifier/ResourceLoadStatisticsMemoryStore.h (275136 => 275137)


--- trunk/Source/WebKit/NetworkProcess/Classifier/ResourceLoadStatisticsMemoryStore.h	2021-03-27 18:31:30 UTC (rev 275136)
+++ trunk/Source/WebKit/NetworkProcess/Classifier/ResourceLoadStatisticsMemoryStore.h	2021-03-27 18:52:51 UTC (rev 275137)
@@ -111,12 +111,12 @@
     // Private Click Measurement is not implemented in the ITP memory store.
     void insertPrivateClickMeasurement(WebCore::PrivateClickMeasurement&&, PrivateClickMeasurementAttributionType) override { };
     void markAllUnattributedPrivateClickMeasurementAsExpiredForTesting() override { };
-    Optional<Seconds> attributePrivateClickMeasurement(const WebCore::PrivateClickMeasurement::SourceSite&, const WebCore::PrivateClickMeasurement::AttributionDestinationSite&, WebCore::PrivateClickMeasurement::AttributionTriggerData&&) override { return { }; };
+    Optional<WebCore::PrivateClickMeasurement::AttributionSecondsUntilSendData>attributePrivateClickMeasurement(const WebCore::PrivateClickMeasurement::SourceSite&, const WebCore::PrivateClickMeasurement::AttributionDestinationSite&, WebCore::PrivateClickMeasurement::AttributionTriggerData&&) override { return { }; };
     Vector<WebCore::PrivateClickMeasurement> allAttributedPrivateClickMeasurement() override { return { }; };
     void clearPrivateClickMeasurement(Optional<RegistrableDomain>) override { };
     void clearExpiredPrivateClickMeasurement() override { };
     String privateClickMeasurementToString() override { return String(); };
-    void clearSentAttribution(WebCore::PrivateClickMeasurement&&) override { };
+    void clearSentAttribution(WebCore::PrivateClickMeasurement&&, WebCore::PrivateClickMeasurement::AttributionReportEndpoint) override { };
     void markAttributedPrivateClickMeasurementsAsExpiredForTesting() override { };
 
 private:

Modified: trunk/Source/WebKit/NetworkProcess/Classifier/ResourceLoadStatisticsStore.h (275136 => 275137)


--- trunk/Source/WebKit/NetworkProcess/Classifier/ResourceLoadStatisticsStore.h	2021-03-27 18:31:30 UTC (rev 275136)
+++ trunk/Source/WebKit/NetworkProcess/Classifier/ResourceLoadStatisticsStore.h	2021-03-27 18:52:51 UTC (rev 275137)
@@ -206,12 +206,12 @@
     // Private Click Measurement.
     virtual void insertPrivateClickMeasurement(WebCore::PrivateClickMeasurement&&, PrivateClickMeasurementAttributionType) = 0;
     virtual void markAllUnattributedPrivateClickMeasurementAsExpiredForTesting() = 0;
-    virtual Optional<Seconds> attributePrivateClickMeasurement(const WebCore::PrivateClickMeasurement::SourceSite&, const WebCore::PrivateClickMeasurement::AttributionDestinationSite&, WebCore::PrivateClickMeasurement::AttributionTriggerData&&) = 0;
+    virtual Optional<WebCore::PrivateClickMeasurement::AttributionSecondsUntilSendData> attributePrivateClickMeasurement(const WebCore::PrivateClickMeasurement::SourceSite&, const WebCore::PrivateClickMeasurement::AttributionDestinationSite&, WebCore::PrivateClickMeasurement::AttributionTriggerData&&) = 0;
     virtual Vector<WebCore::PrivateClickMeasurement> allAttributedPrivateClickMeasurement() = 0;
     virtual void clearPrivateClickMeasurement(Optional<RegistrableDomain>) = 0;
     virtual void clearExpiredPrivateClickMeasurement() = 0;
     virtual String privateClickMeasurementToString() = 0;
-    virtual void clearSentAttribution(WebCore::PrivateClickMeasurement&&) = 0;
+    virtual void clearSentAttribution(WebCore::PrivateClickMeasurement&&, WebCore::PrivateClickMeasurement::AttributionReportEndpoint) = 0;
     virtual void markAttributedPrivateClickMeasurementsAsExpiredForTesting() = 0;
 
 protected:

Modified: trunk/Source/WebKit/NetworkProcess/Classifier/WebResourceLoadStatisticsStore.cpp (275136 => 275137)


--- trunk/Source/WebKit/NetworkProcess/Classifier/WebResourceLoadStatisticsStore.cpp	2021-03-27 18:31:30 UTC (rev 275136)
+++ trunk/Source/WebKit/NetworkProcess/Classifier/WebResourceLoadStatisticsStore.cpp	2021-03-27 18:52:51 UTC (rev 275137)
@@ -1522,7 +1522,7 @@
     });
 }
 
-void WebResourceLoadStatisticsStore::attributePrivateClickMeasurement(const PrivateClickMeasurement::SourceSite& sourceSite, const PrivateClickMeasurement::AttributionDestinationSite& destinationSite, PrivateClickMeasurement::AttributionTriggerData&& attributionTriggerData, CompletionHandler<void(Optional<Seconds>)>&& completionHandler)
+void WebResourceLoadStatisticsStore::attributePrivateClickMeasurement(const PrivateClickMeasurement::SourceSite& sourceSite, const PrivateClickMeasurement::AttributionDestinationSite& destinationSite, PrivateClickMeasurement::AttributionTriggerData&& attributionTriggerData, CompletionHandler<void(Optional<WebCore::PrivateClickMeasurement::AttributionSecondsUntilSendData>)>&& completionHandler)
 {
     ASSERT(RunLoop::isMain());
 
@@ -1639,7 +1639,7 @@
     });
 }
 
-void WebResourceLoadStatisticsStore::clearSentAttribution(WebCore::PrivateClickMeasurement&& attributionToClear)
+void WebResourceLoadStatisticsStore::clearSentAttribution(WebCore::PrivateClickMeasurement&& attributionToClear, PrivateClickMeasurement::AttributionReportEndpoint attributionReportEndpoint)
 {
     ASSERT(RunLoop::isMain());
 
@@ -1646,11 +1646,11 @@
     if (isEphemeral())
         return;
 
-    postTask([this, attributionToClear = WTFMove(attributionToClear)]() mutable {
+    postTask([this, attributionToClear = WTFMove(attributionToClear), attributionReportEndpoint]() mutable {
         if (!m_statisticsStore)
             return;
 
-        m_statisticsStore->clearSentAttribution(WTFMove(attributionToClear));
+        m_statisticsStore->clearSentAttribution(WTFMove(attributionToClear), attributionReportEndpoint);
     });
 }
 

Modified: trunk/Source/WebKit/NetworkProcess/Classifier/WebResourceLoadStatisticsStore.h (275136 => 275137)


--- trunk/Source/WebKit/NetworkProcess/Classifier/WebResourceLoadStatisticsStore.h	2021-03-27 18:31:30 UTC (rev 275136)
+++ trunk/Source/WebKit/NetworkProcess/Classifier/WebResourceLoadStatisticsStore.h	2021-03-27 18:52:51 UTC (rev 275137)
@@ -310,13 +310,13 @@
     // Private Click Measurement.
     void insertPrivateClickMeasurement(WebCore::PrivateClickMeasurement&&, PrivateClickMeasurementAttributionType);
     void markAllUnattributedPrivateClickMeasurementAsExpiredForTesting();
-    void attributePrivateClickMeasurement(const WebCore::PrivateClickMeasurement::SourceSite&, const WebCore::PrivateClickMeasurement::AttributionDestinationSite&, WebCore::PrivateClickMeasurement::AttributionTriggerData&&, CompletionHandler<void(Optional<Seconds>)>&&);
+    void attributePrivateClickMeasurement(const WebCore::PrivateClickMeasurement::SourceSite&, const WebCore::PrivateClickMeasurement::AttributionDestinationSite&, WebCore::PrivateClickMeasurement::AttributionTriggerData&&, CompletionHandler<void(Optional<WebCore::PrivateClickMeasurement::AttributionSecondsUntilSendData>)>&&);
     void allAttributedPrivateClickMeasurement(CompletionHandler<void(Vector<WebCore::PrivateClickMeasurement>&&)>&&);
     void clearPrivateClickMeasurement();
     void clearPrivateClickMeasurementForRegistrableDomain(const WebCore::RegistrableDomain&);
     void clearExpiredPrivateClickMeasurement();
     void privateClickMeasurementToString(CompletionHandler<void(String)>&&);
-    void clearSentAttribution(WebCore::PrivateClickMeasurement&&);
+    void clearSentAttribution(WebCore::PrivateClickMeasurement&&, WebCore::PrivateClickMeasurement::AttributionReportEndpoint);
     void markAttributedPrivateClickMeasurementsAsExpiredForTesting(CompletionHandler<void()>&&);
 
 private:

Modified: trunk/Source/WebKit/NetworkProcess/PrivateClickMeasurementManager.cpp (275136 => 275137)


--- trunk/Source/WebKit/NetworkProcess/PrivateClickMeasurementManager.cpp	2021-03-27 18:31:30 UTC (rev 275136)
+++ trunk/Source/WebKit/NetworkProcess/PrivateClickMeasurementManager.cpp	2021-03-27 18:52:51 UTC (rev 275137)
@@ -77,7 +77,7 @@
 
     if (attribution.ephemeralSourceNonce()) {
         auto attributionCopy = attribution;
-        getTokenPublicKey(WTFMove(attributionCopy), [weakThis = makeWeakPtr(*this), this] (PrivateClickMeasurement&& attribution, const String& publicKeyBase64URL) {
+        getTokenPublicKey(WTFMove(attributionCopy), PrivateClickMeasurement::AttributionReportEndpoint::Source, [weakThis = makeWeakPtr(*this), this] (PrivateClickMeasurement&& attribution, const String& publicKeyBase64URL) {
             if (!weakThis)
                 return;
 
@@ -144,7 +144,7 @@
     return generateNetworkResourceLoadParameters(WTFMove(url), "GET"_s, nullptr, dataTypeCarried);
 }
 
-void PrivateClickMeasurementManager::getTokenPublicKey(PrivateClickMeasurement&& attribution, Function<void(PrivateClickMeasurement&& attribution, const String& publicKeyBase64URL)>&& callback)
+void PrivateClickMeasurementManager::getTokenPublicKey(PrivateClickMeasurement&& attribution, PrivateClickMeasurement::AttributionReportEndpoint attributionReportEndpoint, Function<void(PrivateClickMeasurement&& attribution, const String& publicKeyBase64URL)>&& callback)
 {
     if (!featureEnabled())
         return;
@@ -153,6 +153,8 @@
     auto pcmDataCarried = PrivateClickMeasurement::PcmDataCarried::PersonallyIdentifiable;
     auto tokenPublicKeyURL = attribution.tokenPublicKeyURL();
     if (m_tokenPublicKeyURLForTesting) {
+        if (attributionReportEndpoint == PrivateClickMeasurement::AttributionReportEndpoint::Destination)
+            return;
         tokenPublicKeyURL = *m_tokenPublicKeyURLForTesting;
         pcmDataCarried = PrivateClickMeasurement::PcmDataCarried::NonPersonallyIdentifiable;
     }
@@ -270,21 +272,30 @@
         return;
 
     if (auto* resourceLoadStatistics = m_networkSession->resourceLoadStatistics()) {
-        resourceLoadStatistics->attributePrivateClickMeasurement(sourceSite, destinationSite, WTFMove(attributionTriggerData), [this, weakThis = makeWeakPtr(*this)] (auto optionalSecondsUntilSend) {
+        resourceLoadStatistics->attributePrivateClickMeasurement(sourceSite, destinationSite, WTFMove(attributionTriggerData), [this, weakThis = makeWeakPtr(*this)] (auto attributionSecondsUntilSendData) {
             if (!weakThis)
                 return;
-            if (optionalSecondsUntilSend) {
-                auto secondsUntilSend = *optionalSecondsUntilSend;
-                if (m_firePendingAttributionRequestsTimer.isActive() && m_firePendingAttributionRequestsTimer.nextFireInterval() < secondsUntilSend)
+            
+            if (!attributionSecondsUntilSendData)
+                return;
+
+            if (attributionSecondsUntilSendData.value().hasValidSecondsUntilSendValues()) {
+                auto minSecondsUntilSend = attributionSecondsUntilSendData.value().minSecondsUntilSend();
+
+                ASSERT(minSecondsUntilSend);
+                if (!minSecondsUntilSend)
                     return;
 
+                if (m_firePendingAttributionRequestsTimer.isActive() && m_firePendingAttributionRequestsTimer.nextFireInterval() < *minSecondsUntilSend)
+                    return;
+
                 if (UNLIKELY(debugModeEnabled())) {
-                    m_networkProcess->broadcastConsoleMessage(m_sessionID, MessageSource::PrivateClickMeasurement, MessageLevel::Log, makeString("[Private Click Measurement] Setting timer for firing attribution request to the debug mode timeout of "_s, debugModeSecondsUntilSend.seconds(), " seconds where the regular timeout would have been "_s, secondsUntilSend.seconds(), " seconds."_s));
-                    secondsUntilSend = debugModeSecondsUntilSend;
+                    m_networkProcess->broadcastConsoleMessage(m_sessionID, MessageSource::PrivateClickMeasurement, MessageLevel::Log, makeString("[Private Click Measurement] Setting timer for firing attribution request to the debug mode timeout of "_s, debugModeSecondsUntilSend.seconds(), " seconds where the regular timeout would have been "_s, minSecondsUntilSend.value().seconds(), " seconds."_s));
+                    minSecondsUntilSend = debugModeSecondsUntilSend;
                 } else
-                    m_networkProcess->broadcastConsoleMessage(m_sessionID, MessageSource::PrivateClickMeasurement, MessageLevel::Log, makeString("[Private Click Measurement] Setting timer for firing attribution request to the timeout of "_s, secondsUntilSend.seconds(), " seconds."_s));
+                    m_networkProcess->broadcastConsoleMessage(m_sessionID, MessageSource::PrivateClickMeasurement, MessageLevel::Log, makeString("[Private Click Measurement] Setting timer for firing attribution request to the timeout of "_s, minSecondsUntilSend.value().seconds(), " seconds."_s));
 
-                startTimer(secondsUntilSend);
+                startTimer(*minSecondsUntilSend);
             }
         });
     }
@@ -291,18 +302,18 @@
 #endif
 }
 
-void PrivateClickMeasurementManager::fireConversionRequest(const PrivateClickMeasurement& attribution)
+void PrivateClickMeasurementManager::fireConversionRequest(const PrivateClickMeasurement& attribution, PrivateClickMeasurement::AttributionReportEndpoint attributionReportEndpoint)
 {
     if (!featureEnabled())
         return;
 
     if (!attribution.sourceUnlinkableToken()) {
-        fireConversionRequestImpl(attribution);
+        fireConversionRequestImpl(attribution, attributionReportEndpoint);
         return;
     }
 
     auto attributionCopy = attribution;
-    getTokenPublicKey(WTFMove(attributionCopy), [weakThis = makeWeakPtr(*this), this] (PrivateClickMeasurement&& attribution, const String& publicKeyBase64URL) {
+    getTokenPublicKey(WTFMove(attributionCopy), attributionReportEndpoint, [weakThis = makeWeakPtr(*this), this, attributionReportEndpoint] (PrivateClickMeasurement&& attribution, const String& publicKeyBase64URL) {
         if (!weakThis)
             return;
 
@@ -318,13 +329,21 @@
         if (keyID != attribution.sourceUnlinkableToken()->keyIDBase64URL)
             return;
 
-        fireConversionRequestImpl(attribution);
+        fireConversionRequestImpl(attribution, attributionReportEndpoint);
     });
 }
 
-void PrivateClickMeasurementManager::fireConversionRequestImpl(const PrivateClickMeasurement& attribution)
+void PrivateClickMeasurementManager::fireConversionRequestImpl(const PrivateClickMeasurement& attribution, PrivateClickMeasurement::AttributionReportEndpoint attributionReportEndpoint)
 {
-    auto attributionURL = m_attributionReportTestConfig ? m_attributionReportTestConfig->attributionReportSourceURL : attribution.attributionReportSourceURL();
+    URL attributionURL;
+    switch (attributionReportEndpoint) {
+    case PrivateClickMeasurement::AttributionReportEndpoint::Source:
+        attributionURL = m_attributionReportTestConfig ? m_attributionReportTestConfig->attributionReportSourceURL : attribution.attributionReportSourceURL();
+        break;
+    case PrivateClickMeasurement::AttributionReportEndpoint::Destination:
+        attributionURL = m_attributionReportTestConfig ? m_attributionReportTestConfig->attributionReportAttributeOnURL : attribution.attributionReportAttributeOnURL();
+    }
+
     if (attributionURL.isEmpty() || !attributionURL.isValid())
         return;
 
@@ -343,7 +362,7 @@
     });
 }
 
-void PrivateClickMeasurementManager::clearSentAttribution(PrivateClickMeasurement&& sentConversion)
+void PrivateClickMeasurementManager::clearSentAttribution(PrivateClickMeasurement&& sentConversion, PrivateClickMeasurement::AttributionReportEndpoint attributionReportEndpoint)
 {
 #if ENABLE(RESOURCE_LOAD_STATISTICS)
     if (!featureEnabled())
@@ -350,7 +369,7 @@
         return;
 
     if (auto* resourceLoadStatistics = m_networkSession->resourceLoadStatistics())
-        resourceLoadStatistics->clearSentAttribution(WTFMove(sentConversion));
+        resourceLoadStatistics->clearSentAttribution(WTFMove(sentConversion), attributionReportEndpoint);
 #endif
 }
 
@@ -371,8 +390,10 @@
         bool hasSentAttribution = false;
 
         for (auto& attribution : attributions) {
-            auto earliestTimeToSend = attribution.earliestTimeToSend();
-            if (!earliestTimeToSend) {
+            Optional<WallTime> earliestTimeToSend = attribution.timesToSend().earliestTimeToSend();
+            Optional<WebCore::PrivateClickMeasurement::AttributionReportEndpoint> attributionReportEndpoint = attribution.timesToSend().attributionReportEndpoint();
+
+            if (!earliestTimeToSend || !attributionReportEndpoint) {
                 ASSERT_NOT_REACHED();
                 continue;
             }
@@ -388,9 +409,15 @@
                     return;
                 }
 
-                fireConversionRequest(attribution);
-                clearSentAttribution(WTFMove(attribution));
+                auto laterTimeToSend = attribution.timesToSend().latestTimeToSend();
+                fireConversionRequest(attribution, *attributionReportEndpoint);
+                clearSentAttribution(WTFMove(attribution), *attributionReportEndpoint);
                 hasSentAttribution = true;
+
+                // Update nextTimeToFire in case the later report time for this attribution is sooner than the scheduled next time to fire.
+                if (laterTimeToSend)
+                    nextTimeToFire = std::min(nextTimeToFire, laterTimeToSend.value().secondsSinceEpoch());
+
                 continue;
             }
 

Modified: trunk/Source/WebKit/NetworkProcess/PrivateClickMeasurementManager.h (275136 => 275137)


--- trunk/Source/WebKit/NetworkProcess/PrivateClickMeasurementManager.h	2021-03-27 18:31:30 UTC (rev 275136)
+++ trunk/Source/WebKit/NetworkProcess/PrivateClickMeasurementManager.h	2021-03-27 18:52:51 UTC (rev 275137)
@@ -70,12 +70,12 @@
     void startTimer(Seconds);
 
 private:
-    void getTokenPublicKey(PrivateClickMeasurement&&, Function<void(PrivateClickMeasurement&& attribution, const String& publicKeyBase64URL)>&&);
+    void getTokenPublicKey(PrivateClickMeasurement&&, PrivateClickMeasurement::AttributionReportEndpoint, Function<void(PrivateClickMeasurement&& attribution, const String& publicKeyBase64URL)>&&);
     void getSignedUnlinkableToken(PrivateClickMeasurement&&);
-    void clearSentAttribution(PrivateClickMeasurement&&);
+    void clearSentAttribution(PrivateClickMeasurement&&, PrivateClickMeasurement::AttributionReportEndpoint);
     void attribute(const SourceSite&, const AttributionDestinationSite&, AttributionTriggerData&&);
-    void fireConversionRequest(const PrivateClickMeasurement&);
-    void fireConversionRequestImpl(const PrivateClickMeasurement&);
+    void fireConversionRequest(const PrivateClickMeasurement&, PrivateClickMeasurement::AttributionReportEndpoint);
+    void fireConversionRequestImpl(const PrivateClickMeasurement&, PrivateClickMeasurement::AttributionReportEndpoint);
     void firePendingAttributionRequests();
     void clearExpired();
     bool featureEnabled() const;

Modified: trunk/Tools/ChangeLog (275136 => 275137)


--- trunk/Tools/ChangeLog	2021-03-27 18:31:30 UTC (rev 275136)
+++ trunk/Tools/ChangeLog	2021-03-27 18:52:51 UTC (rev 275137)
@@ -1,3 +1,17 @@
+2021-03-27  Kate Cheney  <[email protected]>
+
+        PCM: Send report to both click source and attribution destination website
+        https://bugs.webkit.org/show_bug.cgi?id=223615
+        <rdar://problem/75849443>
+
+        Reviewed by Brent Fulgham.
+
+        Update API tests to check for a valid time to send for both the source
+        and destination site.
+
+        * TestWebKitAPI/Tests/WebCore/PrivateClickMeasurement.cpp:
+        (TestWebKitAPI::TEST):
+
 2021-03-26  Lauro Moura  <[email protected]>
 
         REGRESSION(r275111) [GLIB] Fix build with new derived sources and forwarding headers scheme

Modified: trunk/Tools/TestWebKitAPI/Tests/WebCore/PrivateClickMeasurement.cpp (275136 => 275137)


--- trunk/Tools/TestWebKitAPI/Tests/WebCore/PrivateClickMeasurement.cpp	2021-03-27 18:31:30 UTC (rev 275136)
+++ trunk/Tools/TestWebKitAPI/Tests/WebCore/PrivateClickMeasurement.cpp	2021-03-27 18:52:51 UTC (rev 275137)
@@ -82,9 +82,10 @@
     PrivateClickMeasurement attribution { PrivateClickMeasurement::SourceID(PrivateClickMeasurement::SourceID::MaxEntropy), PrivateClickMeasurement::SourceSite { webKitURL }, PrivateClickMeasurement::AttributionDestinationSite { exampleURL } };
     auto now = WallTime::now();
     attribution.attributeAndGetEarliestTimeToSend(PrivateClickMeasurement::AttributionTriggerData(PrivateClickMeasurement::AttributionTriggerData::MaxEntropy, PrivateClickMeasurement::Priority(PrivateClickMeasurement::Priority::MaxEntropy)));
-    auto earliestTimeToSend = attribution.earliestTimeToSend();
-    ASSERT_TRUE(earliestTimeToSend);
-    ASSERT_TRUE(earliestTimeToSend.value().secondsSinceEpoch() - 24_h >= now.secondsSinceEpoch());
+    auto earliestTimeToSend = attribution.timesToSend();
+    ASSERT_TRUE(earliestTimeToSend.sourceEarliestTimeToSend && earliestTimeToSend.destinationEarliestTimeToSend);
+    ASSERT_TRUE(earliestTimeToSend.sourceEarliestTimeToSend.value().secondsSinceEpoch() - 24_h >= now.secondsSinceEpoch());
+    ASSERT_TRUE(earliestTimeToSend.destinationEarliestTimeToSend.value().secondsSinceEpoch() - 24_h >= now.secondsSinceEpoch());
 }
 
 TEST(PrivateClickMeasurement, ValidConversionURLs)
@@ -182,7 +183,7 @@
 
     ASSERT_TRUE(attribution.attributionReportSourceURL().isEmpty());
     ASSERT_TRUE(attribution.attributionReportAttributeOnURL().isEmpty());
-    ASSERT_FALSE(attribution.earliestTimeToSend());
+    ASSERT_FALSE(attribution.timesToSend().sourceEarliestTimeToSend && attribution.timesToSend().destinationEarliestTimeToSend);
 }
 
 TEST(PrivateClickMeasurement, InvalidConversionURLs)
_______________________________________________
webkit-changes mailing list
[email protected]
https://lists.webkit.org/mailman/listinfo/webkit-changes

Reply via email to