PrivateClickMeasurementManager.cpp [plain text]
#include "config.h"
#include "PrivateClickMeasurementManager.h"
#include "Logging.h"
#include "NetworkSession.h"
#include <JavaScriptCore/ConsoleTypes.h>
#include <WebCore/FetchOptions.h>
#include <WebCore/FormData.h>
#include <WebCore/HTTPHeaderValues.h>
#include <WebCore/ResourceError.h>
#include <WebCore/ResourceRequest.h>
#include <WebCore/ResourceResponse.h>
#include <WebCore/RuntimeApplicationChecks.h>
#include <WebCore/RuntimeEnabledFeatures.h>
#include <wtf/text/StringBuilder.h>
#include <wtf/text/StringHash.h>
namespace WebKit {
using namespace WebCore;
using SourceSite = PrivateClickMeasurement::SourceSite;
using AttributeOnSite = PrivateClickMeasurement::AttributeOnSite;
using AttributionTriggerData = PrivateClickMeasurement::AttributionTriggerData;
constexpr Seconds debugModeSecondsUntilSend { 10_s };
PrivateClickMeasurementManager::PrivateClickMeasurementManager(NetworkSession& networkSession, NetworkProcess& networkProcess, PAL::SessionID sessionID)
: m_firePendingAttributionRequestsTimer(*this, &PrivateClickMeasurementManager::firePendingAttributionRequests)
, m_networkSession(makeWeakPtr(networkSession))
, m_networkProcess(networkProcess)
, m_sessionID(sessionID)
, m_pingLoadFunction([](NetworkResourceLoadParameters&& params, CompletionHandler<void(const WebCore::ResourceError&, const WebCore::ResourceResponse&)>&& completionHandler) {
UNUSED_PARAM(params);
completionHandler(WebCore::ResourceError(), WebCore::ResourceResponse());
})
{
startTimer(5_s);
}
void PrivateClickMeasurementManager::storeUnattributed(PrivateClickMeasurement&& attribution)
{
if (!featureEnabled())
return;
clearExpired();
m_networkProcess->broadcastConsoleMessage(m_sessionID, MessageSource::PrivateClickMeasurement, MessageLevel::Log, "[Private Click Measurement] Storing an ad click."_s);
#if ENABLE(RESOURCE_LOAD_STATISTICS)
if (auto* resourceLoadStatistics = m_networkSession->resourceLoadStatistics())
resourceLoadStatistics->insertPrivateClickMeasurement(WTFMove(attribution), PrivateClickMeasurementAttributionType::Unattributed);
#endif
}
void PrivateClickMeasurementManager::handleAttribution(AttributionTriggerData&& attributionTriggerData, const URL& requestURL, const WebCore::ResourceRequest& redirectRequest)
{
if (!featureEnabled())
return;
RegistrableDomain redirectDomain { redirectRequest.url() };
auto& firstPartyURL = redirectRequest.firstPartyForCookies();
if (!redirectDomain.matches(requestURL)) {
m_networkProcess->broadcastConsoleMessage(m_sessionID, MessageSource::PrivateClickMeasurement, MessageLevel::Warning, "[Private Click Measurement] Attribution was not accepted because the HTTP redirect was not same-site."_s);
return;
}
if (redirectDomain.matches(firstPartyURL)) {
m_networkProcess->broadcastConsoleMessage(m_sessionID, MessageSource::PrivateClickMeasurement, MessageLevel::Warning, "[Private Click Measurement] Attribution was not accepted because it was requested in an HTTP redirect that is same-site as the first-party."_s);
return;
}
attribute(SourceSite { WTFMove(redirectDomain) }, AttributeOnSite { firstPartyURL }, WTFMove(attributionTriggerData));
}
void PrivateClickMeasurementManager::startTimer(Seconds seconds)
{
m_firePendingAttributionRequestsTimer.startOneShot(m_isRunningTest ? 0_s : seconds);
}
void PrivateClickMeasurementManager::attribute(const SourceSite& sourceSite, const AttributeOnSite& attributeOnSite, AttributionTriggerData&& attributionTriggerData)
{
#if ENABLE(RESOURCE_LOAD_STATISTICS)
if (!featureEnabled())
return;
if (auto* resourceLoadStatistics = m_networkSession->resourceLoadStatistics()) {
resourceLoadStatistics->attributePrivateClickMeasurement(sourceSite, attributeOnSite, WTFMove(attributionTriggerData), [this, weakThis = makeWeakPtr(*this)] (auto optionalSecondsUntilSend) {
if (!weakThis)
return;
if (optionalSecondsUntilSend) {
auto secondsUntilSend = *optionalSecondsUntilSend;
if (m_firePendingAttributionRequestsTimer.isActive() && m_firePendingAttributionRequestsTimer.nextFireInterval() < secondsUntilSend)
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;
} 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));
startTimer(secondsUntilSend);
}
});
}
#endif
}
void PrivateClickMeasurementManager::fireConversionRequest(const PrivateClickMeasurement& attribution)
{
if (!featureEnabled())
return;
auto attributionURL = m_attributionBaseURLForTesting ? *m_attributionBaseURLForTesting : attribution.reportURL();
if (attributionURL.isEmpty() || !attributionURL.isValid())
return;
ResourceRequest request { WTFMove(attributionURL) };
request.setHTTPMethod("POST"_s);
request.setHTTPHeaderField(HTTPHeaderName::CacheControl, WebCore::HTTPHeaderValues::maxAge0());
request.setHTTPContentType(WebCore::HTTPHeaderValues::applicationJSONContentType());
request.setHTTPBody(WebCore::FormData::create(attribution.json()->toJSONString().utf8().data()));
FetchOptions options;
options.credentials = FetchOptions::Credentials::Omit;
options.redirect = FetchOptions::Redirect::Error;
static uint64_t identifier = 0;
NetworkResourceLoadParameters loadParameters;
loadParameters.identifier = ++identifier;
loadParameters.request = request;
loadParameters.parentPID = presentingApplicationPID();
loadParameters.storedCredentialsPolicy = StoredCredentialsPolicy::EphemeralStateless;
loadParameters.options = options;
loadParameters.shouldClearReferrerOnHTTPSToHTTPRedirect = true;
loadParameters.shouldRestrictHTTPResponseAccess = false;
RELEASE_LOG_INFO(PrivateClickMeasurement, "About to fire an attribution request.");
m_networkProcess->broadcastConsoleMessage(m_sessionID, MessageSource::PrivateClickMeasurement, MessageLevel::Log, "[Private Click Measurement] About to fire an attribution request."_s);
m_pingLoadFunction(WTFMove(loadParameters), [weakThis = makeWeakPtr(*this)](const WebCore::ResourceError& error, const WebCore::ResourceResponse& response) {
if (!weakThis)
return;
if (!error.isNull()) {
weakThis->m_networkProcess->broadcastConsoleMessage(weakThis->m_sessionID, MessageSource::PrivateClickMeasurement, MessageLevel::Error, makeString("[Private Click Measurement] Received error: '"_s, error.localizedDescription(), "' for ad click attribution request."_s));
}
});
}
void PrivateClickMeasurementManager::clearSentAttributions(Vector<PrivateClickMeasurement>&& sentConversions)
{
#if ENABLE(RESOURCE_LOAD_STATISTICS)
if (!featureEnabled())
return;
if (auto* resourceLoadStatistics = m_networkSession->resourceLoadStatistics())
resourceLoadStatistics->clearSentAttributions(WTFMove(sentConversions));
#endif
}
void PrivateClickMeasurementManager::firePendingAttributionRequests()
{
#if ENABLE(RESOURCE_LOAD_STATISTICS)
if (!featureEnabled())
return;
auto* resourceLoadStatistics = m_networkSession->resourceLoadStatistics();
if (!resourceLoadStatistics)
return;
resourceLoadStatistics->allAttributedPrivateClickMeasurement([this, weakThis = makeWeakPtr(*this)] (auto&& attributions) {
if (!weakThis)
return;
auto nextTimeToFire = Seconds::infinity();
Vector<PrivateClickMeasurement> sentAttributions;
for (auto& attribution : attributions) {
auto earliestTimeToSend = attribution.earliestTimeToSend();
if (!earliestTimeToSend) {
ASSERT_NOT_REACHED();
continue;
}
auto now = WallTime::now();
if (*earliestTimeToSend <= now || m_isRunningTest || debugModeEnabled()) {
fireConversionRequest(attribution);
sentAttributions.append(WTFMove(attribution));
continue;
}
auto seconds = *earliestTimeToSend - now;
nextTimeToFire = std::min(nextTimeToFire, seconds);
}
clearSentAttributions(WTFMove(sentAttributions));
if (nextTimeToFire < Seconds::infinity())
startTimer(nextTimeToFire);
});
#endif
}
void PrivateClickMeasurementManager::clear()
{
m_firePendingAttributionRequestsTimer.stop();
#if ENABLE(RESOURCE_LOAD_STATISTICS)
if (!featureEnabled())
return;
if (auto* resourceLoadStatistics = m_networkSession->resourceLoadStatistics())
resourceLoadStatistics->clearPrivateClickMeasurement();
#endif
}
void PrivateClickMeasurementManager::clearForRegistrableDomain(const RegistrableDomain& domain)
{
#if ENABLE(RESOURCE_LOAD_STATISTICS)
if (!featureEnabled())
return;
if (auto* resourceLoadStatistics = m_networkSession->resourceLoadStatistics())
resourceLoadStatistics->clearPrivateClickMeasurementForRegistrableDomain(domain);
#endif
}
void PrivateClickMeasurementManager::clearExpired()
{
#if ENABLE(RESOURCE_LOAD_STATISTICS)
if (!featureEnabled())
return;
if (auto* resourceLoadStatistics = m_networkSession->resourceLoadStatistics())
resourceLoadStatistics->clearExpiredPrivateClickMeasurement();
#endif
}
void PrivateClickMeasurementManager::toString(CompletionHandler<void(String)>&& completionHandler) const
{
#if ENABLE(RESOURCE_LOAD_STATISTICS)
if (!featureEnabled()) {
completionHandler("\nNo stored Private Click Measurement data.\n"_s);
return;
}
if (auto* resourceLoadStatistics = m_networkSession->resourceLoadStatistics()) {
resourceLoadStatistics->privateClickMeasurementToString(WTFMove(completionHandler));
return;
}
#endif
completionHandler("\nNo stored Private Click Measurement data.\n"_s);
}
void PrivateClickMeasurementManager::setConversionURLForTesting(URL&& testURL)
{
if (testURL.isEmpty())
m_attributionBaseURLForTesting = { };
else
m_attributionBaseURLForTesting = WTFMove(testURL);
}
void PrivateClickMeasurementManager::markAllUnattributedAsExpiredForTesting()
{
#if ENABLE(RESOURCE_LOAD_STATISTICS)
if (!featureEnabled())
return;
if (auto* resourceLoadStatistics = m_networkSession->resourceLoadStatistics())
resourceLoadStatistics->markAllUnattributedPrivateClickMeasurementAsExpiredForTesting();
#endif
}
bool PrivateClickMeasurementManager::featureEnabled() const
{
return m_networkSession && m_networkProcess->privateClickMeasurementEnabled() && !m_sessionID.isEphemeral();
}
bool PrivateClickMeasurementManager::debugModeEnabled() const
{
return m_networkProcess->privateClickMeasurementDebugModeEnabled() && !m_sessionID.isEphemeral();
}
void PrivateClickMeasurementManager::markAttributedPrivateClickMeasurementsAsExpiredForTesting(CompletionHandler<void()>&& completionHandler)
{
#if ENABLE(RESOURCE_LOAD_STATISTICS)
if (!featureEnabled()) {
completionHandler();
return;
}
if (auto* resourceLoadStatistics = m_networkSession->resourceLoadStatistics()) {
resourceLoadStatistics->markAttributedPrivateClickMeasurementsAsExpiredForTesting(WTFMove(completionHandler));
return;
}
#endif
completionHandler();
}
}