/*
- * Copyright (C) 2016-2017 Apple Inc. All rights reserved.
+ * Copyright (C) 2016-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
#include "config.h"
#include "ResourceLoadObserver.h"
+#include "DeprecatedGlobalSettings.h"
#include "Document.h"
#include "Frame.h"
+#include "FrameLoader.h"
+#include "HTMLFrameOwnerElement.h"
#include "Logging.h"
-#include "MainFrame.h"
#include "Page.h"
#include "ResourceLoadStatistics.h"
#include "ResourceRequest.h"
#include "ResourceResponse.h"
+#include "RuntimeEnabledFeatures.h"
+#include "ScriptExecutionContext.h"
#include "SecurityOrigin.h"
#include "Settings.h"
-#include "URL.h"
+#include <wtf/URL.h>
namespace WebCore {
return ResourceLoadStatistics::primaryDomain(value);
}
-static Seconds timestampResolution { 1_h };
static const Seconds minimumNotificationInterval { 5_s };
ResourceLoadObserver& ResourceLoadObserver::shared()
return resourceLoadObserver;
}
-void ResourceLoadObserver::setShouldThrottleObserverNotifications(bool shouldThrottle)
-{
- m_shouldThrottleNotifications = shouldThrottle;
-
- if (!m_notificationTimer.isActive())
- return;
-
- // If we change the notification state, we need to restart any notifications
- // so they will be on the right schedule.
- m_notificationTimer.stop();
- scheduleNotificationIfNeeded();
-}
-
void ResourceLoadObserver::setNotificationCallback(WTF::Function<void (Vector<ResourceLoadStatistics>&&)>&& notificationCallback)
{
ASSERT(!m_notificationCallback);
m_notificationCallback = WTFMove(notificationCallback);
}
-ResourceLoadObserver::ResourceLoadObserver()
- : m_notificationTimer(*this, &ResourceLoadObserver::notificationTimerFired)
+void ResourceLoadObserver::setRequestStorageAccessUnderOpenerCallback(WTF::Function<void(const String& domainInNeedOfStorageAccess, uint64_t openerPageID, const String& openerDomain)>&& callback)
{
+ ASSERT(!m_requestStorageAccessUnderOpenerCallback);
+ m_requestStorageAccessUnderOpenerCallback = WTFMove(callback);
}
-static inline bool is3xxRedirect(const ResourceResponse& response)
+void ResourceLoadObserver::setLogUserInteractionNotificationCallback(Function<void(PAL::SessionID, const String&)>&& callback)
{
- return response.httpStatusCode() >= 300 && response.httpStatusCode() <= 399;
+ ASSERT(!m_logUserInteractionNotificationCallback);
+ m_logUserInteractionNotificationCallback = WTFMove(callback);
}
-bool ResourceLoadObserver::shouldLog(Page* page) const
+void ResourceLoadObserver::setLogWebSocketLoadingNotificationCallback(Function<void(PAL::SessionID, const String&, const String&, WallTime)>&& callback)
{
- // FIXME: Err on the safe side until we have sorted out what to do in worker contexts
- if (!page)
- return false;
-
- return Settings::resourceLoadStatisticsEnabled() && !page->usesEphemeralSession() && m_notificationCallback;
+ ASSERT(!m_logWebSocketLoadingNotificationCallback);
+ m_logWebSocketLoadingNotificationCallback = WTFMove(callback);
}
-void ResourceLoadObserver::logFrameNavigation(const Frame& frame, const Frame& topFrame, const ResourceRequest& newRequest)
+void ResourceLoadObserver::setLogSubresourceLoadingNotificationCallback(Function<void(PAL::SessionID, const String&, const String&, WallTime)>&& callback)
{
- ASSERT(frame.document());
- ASSERT(topFrame.document());
- ASSERT(topFrame.page());
-
- if (frame.isMainFrame())
- return;
-
- if (!shouldLog(topFrame.page()))
- return;
+ ASSERT(!m_logSubresourceLoadingNotificationCallback);
+ m_logSubresourceLoadingNotificationCallback = WTFMove(callback);
+}
- auto& sourceURL = frame.document()->url();
- auto& targetURL = newRequest.url();
- auto& mainFrameURL = topFrame.document()->url();
+void ResourceLoadObserver::setLogSubresourceRedirectNotificationCallback(Function<void(PAL::SessionID, const String&, const String&)>&& callback)
+{
+ ASSERT(!m_logSubresourceRedirectNotificationCallback);
+ m_logSubresourceRedirectNotificationCallback = WTFMove(callback);
+}
- if (!targetURL.isValid() || !mainFrameURL.isValid())
- return;
-
- auto targetHost = targetURL.host();
- auto mainFrameHost = mainFrameURL.host();
-
- if (targetHost.isEmpty() || mainFrameHost.isEmpty() || targetHost == mainFrameHost || targetHost == sourceURL.host())
- return;
+ResourceLoadObserver::ResourceLoadObserver()
+ : m_notificationTimer(*this, &ResourceLoadObserver::notifyObserver)
+{
+}
- auto targetPrimaryDomain = primaryDomain(targetURL);
- auto mainFramePrimaryDomain = primaryDomain(mainFrameURL);
- auto sourcePrimaryDomain = primaryDomain(sourceURL);
-
- if (targetPrimaryDomain == mainFramePrimaryDomain || targetPrimaryDomain == sourcePrimaryDomain)
- return;
+static inline bool is3xxRedirect(const ResourceResponse& response)
+{
+ return response.httpStatusCode() >= 300 && response.httpStatusCode() <= 399;
+}
- auto& targetStatistics = ensureResourceStatisticsForPrimaryDomain(targetPrimaryDomain);
- auto subframeUnderTopFrameOriginsResult = targetStatistics.subframeUnderTopFrameOrigins.add(mainFramePrimaryDomain);
- if (subframeUnderTopFrameOriginsResult.isNewEntry)
- scheduleNotificationIfNeeded();
+bool ResourceLoadObserver::shouldLog(bool usesEphemeralSession) const
+{
+ return DeprecatedGlobalSettings::resourceLoadStatisticsEnabled() && !usesEphemeralSession && m_notificationCallback;
}
-
+
void ResourceLoadObserver::logSubresourceLoading(const Frame* frame, const ResourceRequest& newRequest, const ResourceResponse& redirectResponse)
{
ASSERT(frame->page());
- if (!shouldLog(frame->page()))
+ if (!frame)
+ return;
+
+ auto* page = frame->page();
+ if (!page || !shouldLog(page->usesEphemeralSession()))
return;
bool isRedirect = is3xxRedirect(redirectResponse);
auto targetPrimaryDomain = primaryDomain(targetURL);
auto mainFramePrimaryDomain = primaryDomain(mainFrameURL);
auto sourcePrimaryDomain = primaryDomain(sourceURL);
-
+
if (targetPrimaryDomain == mainFramePrimaryDomain || (isRedirect && targetPrimaryDomain == sourcePrimaryDomain))
return;
bool shouldCallNotificationCallback = false;
{
auto& targetStatistics = ensureResourceStatisticsForPrimaryDomain(targetPrimaryDomain);
+ auto lastSeen = ResourceLoadStatistics::reduceTimeResolution(WallTime::now());
+ targetStatistics.lastSeen = lastSeen;
if (targetStatistics.subresourceUnderTopFrameOrigins.add(mainFramePrimaryDomain).isNewEntry)
shouldCallNotificationCallback = true;
+
+ m_logSubresourceLoadingNotificationCallback(page->sessionID(), targetPrimaryDomain, mainFramePrimaryDomain, lastSeen);
}
if (isRedirect) {
auto& redirectingOriginStatistics = ensureResourceStatisticsForPrimaryDomain(sourcePrimaryDomain);
- if (redirectingOriginStatistics.subresourceUniqueRedirectsTo.add(targetPrimaryDomain).isNewEntry)
+ bool isNewRedirectToEntry = redirectingOriginStatistics.subresourceUniqueRedirectsTo.add(targetPrimaryDomain).isNewEntry;
+ auto& targetStatistics = ensureResourceStatisticsForPrimaryDomain(targetPrimaryDomain);
+ bool isNewRedirectFromEntry = targetStatistics.subresourceUniqueRedirectsFrom.add(sourcePrimaryDomain).isNewEntry;
+
+ if (isNewRedirectToEntry || isNewRedirectFromEntry)
shouldCallNotificationCallback = true;
+
+ m_logSubresourceRedirectNotificationCallback(page->sessionID(), sourcePrimaryDomain, targetPrimaryDomain);
}
if (shouldCallNotificationCallback)
scheduleNotificationIfNeeded();
}
-void ResourceLoadObserver::logWebSocketLoading(const Frame* frame, const URL& targetURL)
+void ResourceLoadObserver::logWebSocketLoading(const URL& targetURL, const URL& mainFrameURL, PAL::SessionID sessionID)
{
- // FIXME: Web sockets can run in detached frames. Decide how to count such connections.
- // See LayoutTests/http/tests/websocket/construct-in-detached-frame.html
- if (!frame)
+ if (!shouldLog(sessionID.isEphemeral()))
return;
- if (!shouldLog(frame->page()))
- return;
-
- auto& mainFrameURL = frame->mainFrame().document()->url();
-
auto targetHost = targetURL.host();
auto mainFrameHost = mainFrameURL.host();
auto targetPrimaryDomain = primaryDomain(targetURL);
auto mainFramePrimaryDomain = primaryDomain(mainFrameURL);
-
+
if (targetPrimaryDomain == mainFramePrimaryDomain)
return;
+ auto lastSeen = ResourceLoadStatistics::reduceTimeResolution(WallTime::now());
+
auto& targetStatistics = ensureResourceStatisticsForPrimaryDomain(targetPrimaryDomain);
+ targetStatistics.lastSeen = lastSeen;
if (targetStatistics.subresourceUnderTopFrameOrigins.add(mainFramePrimaryDomain).isNewEntry)
scheduleNotificationIfNeeded();
-}
-static WallTime reduceTimeResolution(WallTime time)
-{
- return WallTime::fromRawSeconds(std::floor(time.secondsSinceEpoch() / timestampResolution) * timestampResolution.seconds());
+ m_logWebSocketLoadingNotificationCallback(sessionID, targetPrimaryDomain, mainFramePrimaryDomain, lastSeen);
}
void ResourceLoadObserver::logUserInteractionWithReducedTimeResolution(const Document& document)
{
- ASSERT(document.page());
-
- if (!shouldLog(document.page()))
+ if (!shouldLog(document.sessionID().isEphemeral()))
return;
auto& url = document.url();
- if (url.isBlankURL() || url.isEmpty())
+ if (url.protocolIsAbout() || url.isEmpty())
return;
- auto& statistics = ensureResourceStatisticsForPrimaryDomain(primaryDomain(url));
- auto newTime = reduceTimeResolution(WallTime::now());
- if (newTime == statistics.mostRecentUserInteractionTime)
+ auto domain = primaryDomain(url);
+ auto newTime = ResourceLoadStatistics::reduceTimeResolution(WallTime::now());
+ auto lastReportedUserInteraction = m_lastReportedUserInteractionMap.get(domain);
+ if (newTime == lastReportedUserInteraction)
return;
+ m_lastReportedUserInteractionMap.set(domain, newTime);
+
+ auto& statistics = ensureResourceStatisticsForPrimaryDomain(domain);
statistics.hadUserInteraction = true;
+ statistics.lastSeen = newTime;
statistics.mostRecentUserInteractionTime = newTime;
- scheduleNotificationIfNeeded();
+#if ENABLE(RESOURCE_LOAD_STATISTICS)
+ if (auto* frame = document.frame()) {
+ if (auto* opener = frame->loader().opener()) {
+ if (auto* openerDocument = opener->document()) {
+ if (auto* openerFrame = openerDocument->frame()) {
+ if (auto openerPageID = openerFrame->loader().client().pageID())
+ requestStorageAccessUnderOpener(domain, openerPageID.value(), *openerDocument);
+ }
+ }
+ }
+ }
+
+ m_logUserInteractionNotificationCallback(document.sessionID(), domain);
+#endif
+
+ m_notificationTimer.stop();
+ notifyObserver();
+
+#if ENABLE(RESOURCE_LOAD_STATISTICS) && !RELEASE_LOG_DISABLED
+ if (shouldLogUserInteraction()) {
+ auto counter = ++m_loggingCounter;
+#define LOCAL_LOG(str, ...) \
+ RELEASE_LOG(ResourceLoadStatistics, "ResourceLoadObserver::logUserInteraction: counter = %" PRIu64 ": " str, counter, ##__VA_ARGS__)
+
+ auto escapeForJSON = [](String s) {
+ s.replace('\\', "\\\\").replace('"', "\\\"");
+ return s;
+ };
+ auto escapedURL = escapeForJSON(url.string());
+ auto escapedDomain = escapeForJSON(domain);
+
+ LOCAL_LOG(R"({ "url": "%{public}s",)", escapedURL.utf8().data());
+ LOCAL_LOG(R"( "domain" : "%{public}s",)", escapedDomain.utf8().data());
+ LOCAL_LOG(R"( "until" : %f })", newTime.secondsSinceEpoch().seconds());
+
+#undef LOCAL_LOG
+ }
+#endif
+}
+
+#if ENABLE(RESOURCE_LOAD_STATISTICS)
+void ResourceLoadObserver::requestStorageAccessUnderOpener(const String& domainInNeedOfStorageAccess, uint64_t openerPageID, Document& openerDocument)
+{
+ auto openerUrl = openerDocument.url();
+ auto openerPrimaryDomain = primaryDomain(openerUrl);
+ if (domainInNeedOfStorageAccess != openerPrimaryDomain
+ && !openerDocument.hasRequestedPageSpecificStorageAccessWithUserInteraction(domainInNeedOfStorageAccess)
+ && !equalIgnoringASCIICase(openerUrl.string(), WTF::blankURL())) {
+ m_requestStorageAccessUnderOpenerCallback(domainInNeedOfStorageAccess, openerPageID, openerPrimaryDomain);
+ // Remember user interaction-based requests since they don't need to be repeated.
+ openerDocument.setHasRequestedPageSpecificStorageAccessWithUserInteraction(domainInNeedOfStorageAccess);
+ }
+}
+#endif
+
+void ResourceLoadObserver::logFontLoad(const Document& document, const String& familyName, bool loadStatus)
+{
+#if ENABLE(WEB_API_STATISTICS)
+ if (!shouldLog(document.sessionID().isEphemeral()))
+ return;
+ auto registrableDomain = primaryDomain(document.url());
+ auto& statistics = ensureResourceStatisticsForPrimaryDomain(registrableDomain);
+ bool shouldCallNotificationCallback = false;
+ if (!loadStatus) {
+ if (statistics.fontsFailedToLoad.add(familyName).isNewEntry)
+ shouldCallNotificationCallback = true;
+ } else {
+ if (statistics.fontsSuccessfullyLoaded.add(familyName).isNewEntry)
+ shouldCallNotificationCallback = true;
+ }
+ auto mainFrameRegistrableDomain = primaryDomain(document.topDocument().url());
+ if (statistics.topFrameRegistrableDomainsWhichAccessedWebAPIs.add(mainFrameRegistrableDomain).isNewEntry)
+ shouldCallNotificationCallback = true;
+ if (shouldCallNotificationCallback)
+ scheduleNotificationIfNeeded();
+#else
+ UNUSED_PARAM(document);
+ UNUSED_PARAM(familyName);
+ UNUSED_PARAM(loadStatus);
+#endif
+}
+
+void ResourceLoadObserver::logCanvasRead(const Document& document)
+{
+#if ENABLE(WEB_API_STATISTICS)
+ if (!shouldLog(document.sessionID().isEphemeral()))
+ return;
+ auto registrableDomain = primaryDomain(document.url());
+ auto& statistics = ensureResourceStatisticsForPrimaryDomain(registrableDomain);
+ auto mainFrameRegistrableDomain = primaryDomain(document.topDocument().url());
+ statistics.canvasActivityRecord.wasDataRead = true;
+ if (statistics.topFrameRegistrableDomainsWhichAccessedWebAPIs.add(mainFrameRegistrableDomain).isNewEntry)
+ scheduleNotificationIfNeeded();
+#else
+ UNUSED_PARAM(document);
+#endif
}
+void ResourceLoadObserver::logCanvasWriteOrMeasure(const Document& document, const String& textWritten)
+{
+#if ENABLE(WEB_API_STATISTICS)
+ if (!shouldLog(document.sessionID().isEphemeral()))
+ return;
+ auto registrableDomain = primaryDomain(document.url());
+ auto& statistics = ensureResourceStatisticsForPrimaryDomain(registrableDomain);
+ bool shouldCallNotificationCallback = false;
+ auto mainFrameRegistrableDomain = primaryDomain(document.topDocument().url());
+ if (statistics.canvasActivityRecord.recordWrittenOrMeasuredText(textWritten))
+ shouldCallNotificationCallback = true;
+ if (statistics.topFrameRegistrableDomainsWhichAccessedWebAPIs.add(mainFrameRegistrableDomain).isNewEntry)
+ shouldCallNotificationCallback = true;
+ if (shouldCallNotificationCallback)
+ scheduleNotificationIfNeeded();
+#else
+ UNUSED_PARAM(document);
+ UNUSED_PARAM(textWritten);
+#endif
+}
+
+void ResourceLoadObserver::logNavigatorAPIAccessed(const Document& document, const ResourceLoadStatistics::NavigatorAPI functionName)
+{
+#if ENABLE(WEB_API_STATISTICS)
+ if (!shouldLog(document.sessionID().isEphemeral()))
+ return;
+ auto registrableDomain = primaryDomain(document.url());
+ auto& statistics = ensureResourceStatisticsForPrimaryDomain(registrableDomain);
+ bool shouldCallNotificationCallback = false;
+ if (!statistics.navigatorFunctionsAccessed.contains(functionName)) {
+ statistics.navigatorFunctionsAccessed.add(functionName);
+ shouldCallNotificationCallback = true;
+ }
+ auto mainFrameRegistrableDomain = primaryDomain(document.topDocument().url());
+ if (statistics.topFrameRegistrableDomainsWhichAccessedWebAPIs.add(mainFrameRegistrableDomain).isNewEntry)
+ shouldCallNotificationCallback = true;
+ if (shouldCallNotificationCallback)
+ scheduleNotificationIfNeeded();
+#else
+ UNUSED_PARAM(document);
+ UNUSED_PARAM(functionName);
+#endif
+}
+
+void ResourceLoadObserver::logScreenAPIAccessed(const Document& document, const ResourceLoadStatistics::ScreenAPI functionName)
+{
+#if ENABLE(WEB_API_STATISTICS)
+ if (!shouldLog(document.sessionID().isEphemeral()))
+ return;
+ auto registrableDomain = primaryDomain(document.url());
+ auto& statistics = ensureResourceStatisticsForPrimaryDomain(registrableDomain);
+ bool shouldCallNotificationCallback = false;
+ if (!statistics.screenFunctionsAccessed.contains(functionName)) {
+ statistics.screenFunctionsAccessed.add(functionName);
+ shouldCallNotificationCallback = true;
+ }
+ auto mainFrameRegistrableDomain = primaryDomain(document.topDocument().url());
+ if (statistics.topFrameRegistrableDomainsWhichAccessedWebAPIs.add(mainFrameRegistrableDomain).isNewEntry)
+ shouldCallNotificationCallback = true;
+ if (shouldCallNotificationCallback)
+ scheduleNotificationIfNeeded();
+#else
+ UNUSED_PARAM(document);
+ UNUSED_PARAM(functionName);
+#endif
+}
+
ResourceLoadStatistics& ResourceLoadObserver::ensureResourceStatisticsForPrimaryDomain(const String& primaryDomain)
{
auto addResult = m_resourceStatisticsMap.ensure(primaryDomain, [&primaryDomain] {
}
if (!m_notificationTimer.isActive())
- m_notificationTimer.startOneShot(m_shouldThrottleNotifications ? minimumNotificationInterval : 0_s);
+ m_notificationTimer.startOneShot(minimumNotificationInterval);
}
-void ResourceLoadObserver::notificationTimerFired()
+void ResourceLoadObserver::notifyObserver()
{
ASSERT(m_notificationCallback);
+ m_notificationTimer.stop();
m_notificationCallback(takeStatistics());
}
return statistics;
}
+void ResourceLoadObserver::clearState()
+{
+ m_notificationTimer.stop();
+ m_resourceStatisticsMap.clear();
+ m_lastReportedUserInteractionMap.clear();
+}
+
+URL ResourceLoadObserver::nonNullOwnerURL(const Document& document) const
+{
+ auto url = document.url();
+ auto* frame = document.frame();
+ auto host = document.url().host();
+
+ while ((host.isNull() || host.isEmpty()) && frame && !frame->isMainFrame()) {
+ auto* ownerElement = frame->ownerElement();
+
+ ASSERT(ownerElement != nullptr);
+
+ auto& doc = ownerElement->document();
+ frame = doc.frame();
+ url = doc.url();
+ host = url.host();
+ }
+
+ return url;
+}
+
} // namespace WebCore