blob: 310d7125eed2e4f122e90139892d311aeb35e1f6 [file] [log] [blame]
/*
* Copyright (C) 2016-2017 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 "ResourceLoadStatisticsStore.h"
#include "KeyedCoding.h"
#include "Logging.h"
#include "NetworkStorageSession.h"
#include "PlatformStrategies.h"
#include "ResourceLoadStatistics.h"
#include "SharedBuffer.h"
#include "URL.h"
#include <wtf/CrossThreadCopier.h>
#include <wtf/CurrentTime.h>
#include <wtf/MainThread.h>
#include <wtf/NeverDestroyed.h>
#include <wtf/RunLoop.h>
namespace WebCore {
static const auto statisticsModelVersion = 4;
static const auto secondsPerHour = 3600;
static const auto secondsPerDay = 24 * secondsPerHour;
static auto timeToLiveUserInteraction = 30 * secondsPerDay;
static auto timeToLiveCookiePartitionFree = 1 * secondsPerDay;
static auto grandfatheringTime = 1 * secondsPerHour;
static auto minimumTimeBetweeenDataRecordsRemoval = 60;
Ref<ResourceLoadStatisticsStore> ResourceLoadStatisticsStore::create()
{
return adoptRef(*new ResourceLoadStatisticsStore());
}
bool ResourceLoadStatisticsStore::isPrevalentResource(const String& primaryDomain) const
{
auto locker = holdLock(m_statisticsLock);
auto mapEntry = m_resourceStatisticsMap.find(primaryDomain);
if (mapEntry == m_resourceStatisticsMap.end())
return false;
return mapEntry->value.isPrevalentResource;
}
ResourceLoadStatistics& ResourceLoadStatisticsStore::ensureResourceStatisticsForPrimaryDomain(const String& primaryDomain)
{
ASSERT(m_statisticsLock.isLocked());
auto addResult = m_resourceStatisticsMap.ensure(primaryDomain, [&primaryDomain] {
return ResourceLoadStatistics(primaryDomain);
});
return addResult.iterator->value;
}
void ResourceLoadStatisticsStore::setResourceStatisticsForPrimaryDomain(const String& primaryDomain, ResourceLoadStatistics&& statistics)
{
ASSERT(!isMainThread());
auto locker = holdLock(m_statisticsLock);
m_resourceStatisticsMap.set(primaryDomain, WTFMove(statistics));
}
typedef HashMap<String, ResourceLoadStatistics>::KeyValuePairType StatisticsValue;
std::unique_ptr<KeyedEncoder> ResourceLoadStatisticsStore::createEncoderFromData()
{
ASSERT(!isMainThread());
auto encoder = KeyedEncoder::encoder();
auto locker = holdLock(m_statisticsLock);
encoder->encodeUInt32("version", statisticsModelVersion);
encoder->encodeDouble("endOfGrandfatheringTimestamp", m_endOfGrandfatheringTimestamp);
encoder->encodeObjects("browsingStatistics", m_resourceStatisticsMap.begin(), m_resourceStatisticsMap.end(), [](KeyedEncoder& encoderInner, const StatisticsValue& origin) {
origin.value.encode(encoderInner);
});
return encoder;
}
void ResourceLoadStatisticsStore::readDataFromDecoder(KeyedDecoder& decoder)
{
ASSERT(!isMainThread());
ASSERT(m_statisticsLock.isLocked());
if (m_resourceStatisticsMap.size())
return;
unsigned version;
if (!decoder.decodeUInt32("version", version))
version = 1;
static const auto minimumVersionWithGrandfathering = 3;
if (version > minimumVersionWithGrandfathering) {
double endOfGrandfatheringTimestamp;
if (decoder.decodeDouble("endOfGrandfatheringTimestamp", endOfGrandfatheringTimestamp))
m_endOfGrandfatheringTimestamp = endOfGrandfatheringTimestamp;
else
m_endOfGrandfatheringTimestamp = 0;
}
Vector<ResourceLoadStatistics> loadedStatistics;
bool succeeded = decoder.decodeObjects("browsingStatistics", loadedStatistics, [version](KeyedDecoder& decoderInner, ResourceLoadStatistics& statistics) {
return statistics.decode(decoderInner, version);
});
if (!succeeded)
return;
Vector<String> prevalentResourceDomainsWithoutUserInteraction;
prevalentResourceDomainsWithoutUserInteraction.reserveInitialCapacity(loadedStatistics.size());
{
auto locker = holdLock(m_statisticsLock);
for (auto& statistics : loadedStatistics) {
if (statistics.isPrevalentResource && !statistics.hadUserInteraction) {
prevalentResourceDomainsWithoutUserInteraction.uncheckedAppend(statistics.highLevelDomain);
statistics.isMarkedForCookiePartitioning = true;
}
m_resourceStatisticsMap.set(statistics.highLevelDomain, statistics);
}
}
fireShouldPartitionCookiesHandler({ }, prevalentResourceDomainsWithoutUserInteraction, true);
}
void ResourceLoadStatisticsStore::clearInMemory()
{
ASSERT(!isMainThread());
{
auto locker = holdLock(m_statisticsLock);
m_resourceStatisticsMap.clear();
}
fireShouldPartitionCookiesHandler({ }, { }, true);
}
void ResourceLoadStatisticsStore::clearInMemoryAndPersistent()
{
ASSERT(!isMainThread());
clearInMemory();
if (m_writePersistentStoreHandler)
m_writePersistentStoreHandler();
if (m_grandfatherExistingWebsiteDataHandler)
m_grandfatherExistingWebsiteDataHandler();
}
String ResourceLoadStatisticsStore::statisticsForOrigin(const String& origin)
{
auto locker = holdLock(m_statisticsLock);
auto iter = m_resourceStatisticsMap.find(origin);
if (iter == m_resourceStatisticsMap.end())
return emptyString();
return "Statistics for " + origin + ":\n" + iter->value.toString();
}
Vector<ResourceLoadStatistics> ResourceLoadStatisticsStore::takeStatistics()
{
Vector<ResourceLoadStatistics> statistics;
{
auto locker = holdLock(m_statisticsLock);
statistics.reserveInitialCapacity(m_resourceStatisticsMap.size());
for (auto& statistic : m_resourceStatisticsMap.values())
statistics.uncheckedAppend(WTFMove(statistic));
m_resourceStatisticsMap.clear();
}
return statistics;
}
void ResourceLoadStatisticsStore::mergeStatistics(const Vector<ResourceLoadStatistics>& statistics)
{
auto locker = holdLock(m_statisticsLock);
for (auto& statistic : statistics) {
auto result = m_resourceStatisticsMap.ensure(statistic.highLevelDomain, [&statistic] {
return ResourceLoadStatistics(statistic.highLevelDomain);
});
result.iterator->value.merge(statistic);
}
}
void ResourceLoadStatisticsStore::setNotificationCallback(WTF::Function<void()>&& handler)
{
m_dataAddedHandler = WTFMove(handler);
}
void ResourceLoadStatisticsStore::setShouldPartitionCookiesCallback(WTF::Function<void(const Vector<String>& domainsToRemove, const Vector<String>& domainsToAdd, bool clearFirst)>&& handler)
{
m_shouldPartitionCookiesForDomainsHandler = WTFMove(handler);
}
void ResourceLoadStatisticsStore::setWritePersistentStoreCallback(WTF::Function<void()>&& handler)
{
m_writePersistentStoreHandler = WTFMove(handler);
}
void ResourceLoadStatisticsStore::setGrandfatherExistingWebsiteDataCallback(WTF::Function<void()>&& handler)
{
m_grandfatherExistingWebsiteDataHandler = WTFMove(handler);
}
void ResourceLoadStatisticsStore::fireDataModificationHandler()
{
ASSERT(!isMainThread());
RunLoop::main().dispatch([this, protectedThis = makeRef(*this)] () {
if (m_dataAddedHandler)
m_dataAddedHandler();
});
}
static inline bool shouldPartitionCookies(const ResourceLoadStatistics& statistic)
{
return statistic.isPrevalentResource
&& (!statistic.hadUserInteraction || currentTime() > statistic.mostRecentUserInteraction + timeToLiveCookiePartitionFree);
}
void ResourceLoadStatisticsStore::fireShouldPartitionCookiesHandler()
{
ASSERT(!isMainThread());
Vector<String> domainsToRemove;
Vector<String> domainsToAdd;
auto locker = holdLock(m_statisticsLock);
for (auto& resourceStatistic : m_resourceStatisticsMap.values()) {
bool shouldPartition = shouldPartitionCookies(resourceStatistic);
if (resourceStatistic.isMarkedForCookiePartitioning && !shouldPartition) {
resourceStatistic.isMarkedForCookiePartitioning = false;
domainsToRemove.append(resourceStatistic.highLevelDomain);
} else if (!resourceStatistic.isMarkedForCookiePartitioning && shouldPartition) {
resourceStatistic.isMarkedForCookiePartitioning = true;
domainsToAdd.append(resourceStatistic.highLevelDomain);
}
}
if (domainsToRemove.isEmpty() && domainsToAdd.isEmpty())
return;
RunLoop::main().dispatch([this, protectedThis = makeRef(*this), domainsToRemove = CrossThreadCopier<Vector<String>>::copy(domainsToRemove), domainsToAdd = CrossThreadCopier<Vector<String>>::copy(domainsToAdd)] () {
if (m_shouldPartitionCookiesForDomainsHandler)
m_shouldPartitionCookiesForDomainsHandler(domainsToRemove, domainsToAdd, false);
});
}
void ResourceLoadStatisticsStore::fireShouldPartitionCookiesHandler(const Vector<String>& domainsToRemove, const Vector<String>& domainsToAdd, bool clearFirst)
{
ASSERT(!isMainThread());
if (domainsToRemove.isEmpty() && domainsToAdd.isEmpty())
return;
RunLoop::main().dispatch([this, clearFirst, protectedThis = makeRef(*this), domainsToRemove = CrossThreadCopier<Vector<String>>::copy(domainsToRemove), domainsToAdd = CrossThreadCopier<Vector<String>>::copy(domainsToAdd)] () {
if (m_shouldPartitionCookiesForDomainsHandler)
m_shouldPartitionCookiesForDomainsHandler(domainsToRemove, domainsToAdd, clearFirst);
});
auto locker = holdLock(m_statisticsLock);
if (clearFirst) {
for (auto& resourceStatistic : m_resourceStatisticsMap.values())
resourceStatistic.isMarkedForCookiePartitioning = false;
} else {
for (auto& domain : domainsToRemove)
ensureResourceStatisticsForPrimaryDomain(domain).isMarkedForCookiePartitioning = false;
}
for (auto& domain : domainsToAdd)
ensureResourceStatisticsForPrimaryDomain(domain).isMarkedForCookiePartitioning = true;
}
void ResourceLoadStatisticsStore::setTimeToLiveUserInteraction(double seconds)
{
if (seconds >= 0)
timeToLiveUserInteraction = seconds;
}
void ResourceLoadStatisticsStore::setTimeToLiveCookiePartitionFree(double seconds)
{
if (seconds >= 0)
timeToLiveCookiePartitionFree = seconds;
}
void ResourceLoadStatisticsStore::setMinimumTimeBetweeenDataRecordsRemoval(double seconds)
{
if (seconds >= 0)
minimumTimeBetweeenDataRecordsRemoval = seconds;
}
void ResourceLoadStatisticsStore::setGrandfatheringTime(double seconds)
{
if (seconds >= 0)
grandfatheringTime = seconds;
}
void ResourceLoadStatisticsStore::processStatistics(WTF::Function<void(ResourceLoadStatistics&)>&& processFunction)
{
ASSERT(!isMainThread());
auto locker = holdLock(m_statisticsLock);
for (auto& resourceStatistic : m_resourceStatisticsMap.values())
processFunction(resourceStatistic);
}
bool ResourceLoadStatisticsStore::hasHadRecentUserInteraction(ResourceLoadStatistics& resourceStatistic) const
{
if (!resourceStatistic.hadUserInteraction)
return false;
if (currentTime() > resourceStatistic.mostRecentUserInteraction + timeToLiveUserInteraction) {
// Drop privacy sensitive data because we no longer need it.
// Set timestamp to 0.0 so that statistics merge will know
// it has been reset as opposed to its default -1.
resourceStatistic.mostRecentUserInteraction = 0;
resourceStatistic.hadUserInteraction = false;
return false;
}
return true;
}
Vector<String> ResourceLoadStatisticsStore::topPrivatelyControlledDomainsToRemoveWebsiteDataFor()
{
bool shouldCheckForGrandfathering = m_endOfGrandfatheringTimestamp > currentTime();
bool shouldClearGrandfathering = !shouldCheckForGrandfathering && m_endOfGrandfatheringTimestamp;
if (shouldClearGrandfathering)
m_endOfGrandfatheringTimestamp = 0;
Vector<String> prevalentResources;
auto locker = holdLock(m_statisticsLock);
for (auto& statistic : m_resourceStatisticsMap.values()) {
if (statistic.isPrevalentResource
&& !hasHadRecentUserInteraction(statistic)
&& (!shouldCheckForGrandfathering || !statistic.grandfathered))
prevalentResources.append(statistic.highLevelDomain);
if (shouldClearGrandfathering && statistic.grandfathered)
statistic.grandfathered = false;
}
return prevalentResources;
}
void ResourceLoadStatisticsStore::updateStatisticsForRemovedDataRecords(const Vector<String>& prevalentResourceDomains)
{
auto locker = holdLock(m_statisticsLock);
for (auto& prevalentResourceDomain : prevalentResourceDomains) {
ResourceLoadStatistics& statistic = ensureResourceStatisticsForPrimaryDomain(prevalentResourceDomain);
++statistic.dataRecordsRemoved;
}
}
void ResourceLoadStatisticsStore::handleFreshStartWithEmptyOrNoStore(HashSet<String>&& topPrivatelyControlledDomainsToGrandfather)
{
auto locker = holdLock(m_statisticsLock);
for (auto& topPrivatelyControlledDomain : topPrivatelyControlledDomainsToGrandfather) {
ResourceLoadStatistics& statistic = ensureResourceStatisticsForPrimaryDomain(topPrivatelyControlledDomain);
statistic.grandfathered = true;
}
m_endOfGrandfatheringTimestamp = std::floor(currentTime()) + grandfatheringTime;
}
bool ResourceLoadStatisticsStore::shouldRemoveDataRecords() const
{
ASSERT(!isMainThread());
if (m_dataRecordsRemovalPending)
return false;
if (m_lastTimeDataRecordsWereRemoved && currentTime() < m_lastTimeDataRecordsWereRemoved + minimumTimeBetweeenDataRecordsRemoval)
return false;
return true;
}
void ResourceLoadStatisticsStore::dataRecordsBeingRemoved()
{
ASSERT(!isMainThread());
m_lastTimeDataRecordsWereRemoved = currentTime();
m_dataRecordsRemovalPending = true;
}
void ResourceLoadStatisticsStore::dataRecordsWereRemoved()
{
ASSERT(!isMainThread());
m_dataRecordsRemovalPending = false;
}
WTF::RecursiveLockAdapter<Lock>& ResourceLoadStatisticsStore::statisticsLock()
{
return m_statisticsLock;
}
}