| /* |
| * 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; |
| } |
| |
| } |