blob: ba49b460006835dc7a3c04678f9aed377a6aa0b3 [file] [log] [blame]
/*
* Copyright (C) 2019 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
* THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "config.h"
#include "AdClickAttributionManager.h"
#include "Logging.h"
#include <WebCore/FetchOptions.h>
#include <WebCore/FormData.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 Source = AdClickAttribution::Source;
using Destination = AdClickAttribution::Destination;
using DestinationMap = HashMap<Destination, AdClickAttribution>;
using Conversion = AdClickAttribution::Conversion;
constexpr Seconds debugModeSecondsUntilSend { 60_s };
void AdClickAttributionManager::storeUnconverted(AdClickAttribution&& attribution)
{
clearExpired();
RELEASE_LOG_INFO_IF(debugModeEnabled(), AdClickAttribution, "Storing an ad click.");
m_unconvertedAdClickAttributionMap.set(std::make_pair(attribution.source(), attribution.destination()), WTFMove(attribution));
}
void AdClickAttributionManager::handleConversion(Conversion&& conversion, const URL& requestURL, const WebCore::ResourceRequest& redirectRequest)
{
if (m_sessionID.isEphemeral())
return;
RegistrableDomain redirectDomain { redirectRequest.url() };
auto& firstPartyURL = redirectRequest.firstPartyForCookies();
if (!redirectDomain.matches(requestURL)) {
RELEASE_LOG_INFO_IF(debugModeEnabled(), AdClickAttribution, "Conversion was not accepted because the HTTP redirect was not same-site.");
return;
}
if (redirectDomain.matches(firstPartyURL)) {
RELEASE_LOG_INFO_IF(debugModeEnabled(), AdClickAttribution, "Conversion was not accepted because it was requested in an HTTP redirect that is same-site as the first-party.");
return;
}
convert(AdClickAttribution::Source { WTFMove(redirectDomain) }, AdClickAttribution::Destination { firstPartyURL }, WTFMove(conversion));
}
void AdClickAttributionManager::startTimer(Seconds seconds)
{
m_firePendingConversionRequestsTimer.startOneShot(m_isRunningTest ? 0_s : seconds);
}
void AdClickAttributionManager::convert(const Source& source, const Destination& destination, Conversion&& conversion)
{
clearExpired();
if (!conversion.isValid()) {
RELEASE_LOG_INFO_IF(debugModeEnabled(), AdClickAttribution, "Got an invalid conversion.");
return;
}
#if !RELEASE_LOG_DISABLED
auto conversionData = conversion.data;
auto conversionPriority = conversion.priority;
#endif
RELEASE_LOG_INFO_IF(debugModeEnabled(), AdClickAttribution, "Got a conversion with conversion data: %{public}u and priority: %{public}u.", conversionData, conversionPriority);
auto secondsUntilSend = Seconds::infinity();
auto pair = std::make_pair(source, destination);
auto previouslyUnconvertedAttribution = m_unconvertedAdClickAttributionMap.take(pair);
auto previouslyConvertedAttributionIter = m_convertedAdClickAttributionMap.find(pair);
if (!previouslyUnconvertedAttribution.isEmpty()) {
// Always convert the pending attribution and remove it from the unconverted map.
if (auto optionalSecondsUntilSend = previouslyUnconvertedAttribution.convertAndGetEarliestTimeToSend(WTFMove(conversion))) {
secondsUntilSend = *optionalSecondsUntilSend;
ASSERT(secondsUntilSend != Seconds::infinity());
RELEASE_LOG_INFO_IF(debugModeEnabled(), AdClickAttribution, "Converted a stored ad click with conversion data: %{public}u and priority: %{public}u.", conversionData, conversionPriority);
}
if (previouslyConvertedAttributionIter == m_convertedAdClickAttributionMap.end())
m_convertedAdClickAttributionMap.add(pair, WTFMove(previouslyUnconvertedAttribution));
else if (previouslyUnconvertedAttribution.hasHigherPriorityThan(previouslyConvertedAttributionIter->value)) {
// If the newly converted attribution has higher priority, replace the old one.
m_convertedAdClickAttributionMap.set(pair, WTFMove(previouslyUnconvertedAttribution));
RELEASE_LOG_INFO_IF(debugModeEnabled(), AdClickAttribution, "Replaced a previously converted ad click with a new one with conversion data: %{public}u and priority: %{public}u because it had higher priority.", conversionData, conversionPriority);
}
} else if (previouslyConvertedAttributionIter != m_convertedAdClickAttributionMap.end()) {
// If we have no newly converted attribution, re-convert the old one to respect the new priority.
if (auto optionalSecondsUntilSend = previouslyConvertedAttributionIter->value.convertAndGetEarliestTimeToSend(WTFMove(conversion))) {
secondsUntilSend = *optionalSecondsUntilSend;
ASSERT(secondsUntilSend != Seconds::infinity());
RELEASE_LOG_INFO_IF(debugModeEnabled(), AdClickAttribution, "Re-converted an ad click with a new one with conversion data: %{public}u and priority: %{public}u because it had higher priority.", conversionData, conversionPriority);
}
}
if (secondsUntilSend == Seconds::infinity())
return;
if (m_firePendingConversionRequestsTimer.isActive() && m_firePendingConversionRequestsTimer.nextFireInterval() < secondsUntilSend)
return;
if (debugModeEnabled()) {
RELEASE_LOG_INFO(AdClickAttribution, "Setting timer for firing conversion requests to the debug mode timeout of %{public}f seconds where the regular timeout would have been %{public}f seconds.", debugModeSecondsUntilSend.seconds(), secondsUntilSend.seconds());
secondsUntilSend = debugModeSecondsUntilSend;
}
startTimer(secondsUntilSend);
}
void AdClickAttributionManager::fireConversionRequest(const AdClickAttribution& attribution)
{
auto conversionURL = m_conversionBaseURLForTesting ? attribution.urlForTesting(*m_conversionBaseURLForTesting) : attribution.url();
if (conversionURL.isEmpty() || !conversionURL.isValid())
return;
auto conversionReferrerURL = attribution.referrer();
if (conversionReferrerURL.isEmpty() || !conversionReferrerURL.isValid())
return;
ResourceRequest request { conversionURL };
request.setHTTPMethod("POST"_s);
request.setHTTPHeaderField(HTTPHeaderName::CacheControl, "max-age=0"_s);
request.setHTTPReferrer(conversionReferrerURL.string());
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.sourceOrigin = SecurityOrigin::create(conversionReferrerURL);
loadParameters.parentPID = presentingApplicationPID();
loadParameters.storedCredentialsPolicy = StoredCredentialsPolicy::EphemeralStateless;
loadParameters.options = options;
loadParameters.shouldClearReferrerOnHTTPSToHTTPRedirect = true;
loadParameters.shouldRestrictHTTPResponseAccess = false;
#if ENABLE(CONTENT_EXTENSIONS)
loadParameters.mainDocumentURL = WTFMove(conversionReferrerURL);
#endif
RELEASE_LOG_INFO_IF(debugModeEnabled(), AdClickAttribution, "About to fire an attribution request for a conversion.");
m_pingLoadFunction(WTFMove(loadParameters), [](const WebCore::ResourceError& error, const WebCore::ResourceResponse& response) {
#if PLATFORM(COCOA)
RELEASE_LOG_ERROR_IF(!error.isNull(), AdClickAttribution, "Received error: '%{public}s' for ad click attribution request.", error.localizedDescription().utf8().data());
#else
RELEASE_LOG_ERROR_IF(!error.isNull(), AdClickAttribution, "Received error: '%s' for ad click attribution request.", error.localizedDescription().utf8().data());
#endif
UNUSED_PARAM(response);
UNUSED_PARAM(error);
});
}
void AdClickAttributionManager::firePendingConversionRequests()
{
auto nextTimeToFire = Seconds::infinity();
for (auto& attribution : m_convertedAdClickAttributionMap.values()) {
if (attribution.wasConversionSent()) {
ASSERT_NOT_REACHED();
continue;
}
auto earliestTimeToSend = attribution.earliestTimeToSend();
if (!earliestTimeToSend) {
ASSERT_NOT_REACHED();
continue;
}
auto now = WallTime::now();
if (*earliestTimeToSend <= now || m_isRunningTest || debugModeEnabled()) {
fireConversionRequest(attribution);
attribution.markConversionAsSent();
continue;
}
auto seconds = *earliestTimeToSend - now;
nextTimeToFire = std::min(nextTimeToFire, seconds);
}
m_convertedAdClickAttributionMap.removeIf([](auto& keyAndValue) {
return keyAndValue.value.wasConversionSent();
});
if (nextTimeToFire < Seconds::infinity())
startTimer(nextTimeToFire);
}
void AdClickAttributionManager::clear()
{
m_firePendingConversionRequestsTimer.stop();
m_unconvertedAdClickAttributionMap.clear();
m_convertedAdClickAttributionMap.clear();
}
void AdClickAttributionManager::clearForRegistrableDomain(const RegistrableDomain& domain)
{
m_unconvertedAdClickAttributionMap.removeIf([&domain](auto& keyAndValue) {
return keyAndValue.key.first.registrableDomain == domain || keyAndValue.key.second.registrableDomain == domain;
});
m_convertedAdClickAttributionMap.removeIf([&domain](auto& keyAndValue) {
return keyAndValue.key.first.registrableDomain == domain || keyAndValue.key.second.registrableDomain == domain;
});
}
void AdClickAttributionManager::clearExpired()
{
m_unconvertedAdClickAttributionMap.removeIf([](auto& keyAndValue) {
return keyAndValue.value.hasExpired();
});
}
void AdClickAttributionManager::toString(CompletionHandler<void(String)>&& completionHandler) const
{
if (m_unconvertedAdClickAttributionMap.isEmpty() && m_convertedAdClickAttributionMap.isEmpty())
return completionHandler("\nNo stored Ad Click Attribution data.\n"_s);
unsigned unconvertedAttributionNumber = 0;
StringBuilder builder;
for (auto& attribution : m_unconvertedAdClickAttributionMap.values()) {
if (!unconvertedAttributionNumber)
builder.appendLiteral("Unconverted Ad Click Attributions:\n");
else
builder.append('\n');
builder.appendLiteral("WebCore::AdClickAttribution ");
builder.appendNumber(++unconvertedAttributionNumber);
builder.append('\n');
builder.append(attribution.toString());
}
unsigned convertedAttributionNumber = 0;
for (auto& attribution : m_convertedAdClickAttributionMap.values()) {
if (unconvertedAttributionNumber)
builder.append('\n');
if (!convertedAttributionNumber)
builder.appendLiteral("Converted Ad Click Attributions:\n");
else
builder.append('\n');
builder.appendLiteral("WebCore::AdClickAttribution ");
builder.appendNumber(++convertedAttributionNumber + unconvertedAttributionNumber);
builder.append('\n');
builder.append(attribution.toString());
}
completionHandler(builder.toString());
}
void AdClickAttributionManager::setConversionURLForTesting(URL&& testURL)
{
if (testURL.isEmpty())
m_conversionBaseURLForTesting = { };
else
m_conversionBaseURLForTesting = WTFMove(testURL);
}
void AdClickAttributionManager::markAllUnconvertedAsExpiredForTesting()
{
for (auto& attribution : m_unconvertedAdClickAttributionMap.values())
attribution.markAsExpired();
}
bool AdClickAttributionManager::debugModeEnabled() const
{
return RuntimeEnabledFeatures::sharedFeatures().adClickAttributionDebugModeEnabled() && !m_sessionID.isEphemeral();
}
} // namespace WebKit