2 * Copyright (C) 2016-2018 Apple Inc. All rights reserved.
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions
7 * 1. Redistributions of source code must retain the above copyright
8 * notice, this list of conditions and the following disclaimer.
9 * 2. Redistributions in binary form must reproduce the above copyright
10 * notice, this list of conditions and the following disclaimer in the
11 * documentation and/or other materials provided with the distribution.
13 * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23 * THE POSSIBILITY OF SUCH DAMAGE.
27 #include "ResourceLoadObserver.h"
29 #include "DeprecatedGlobalSettings.h"
32 #include "FrameLoader.h"
33 #include "HTMLFrameOwnerElement.h"
36 #include "ResourceLoadStatistics.h"
37 #include "ResourceRequest.h"
38 #include "ResourceResponse.h"
39 #include "ScriptExecutionContext.h"
40 #include "SecurityOrigin.h"
46 template<typename T> static inline String primaryDomain(const T& value)
48 return ResourceLoadStatistics::primaryDomain(value);
51 static const Seconds minimumNotificationInterval { 5_s };
53 ResourceLoadObserver& ResourceLoadObserver::shared()
55 static NeverDestroyed<ResourceLoadObserver> resourceLoadObserver;
56 return resourceLoadObserver;
59 void ResourceLoadObserver::setNotificationCallback(WTF::Function<void (Vector<ResourceLoadStatistics>&&)>&& notificationCallback)
61 ASSERT(!m_notificationCallback);
62 m_notificationCallback = WTFMove(notificationCallback);
65 void ResourceLoadObserver::setRequestStorageAccessUnderOpenerCallback(WTF::Function<void(const String& domainInNeedOfStorageAccess, uint64_t openerPageID, const String& openerDomain, bool isTriggeredByUserGesture)>&& callback)
67 ASSERT(!m_requestStorageAccessUnderOpenerCallback);
68 m_requestStorageAccessUnderOpenerCallback = WTFMove(callback);
71 ResourceLoadObserver::ResourceLoadObserver()
72 : m_notificationTimer(*this, &ResourceLoadObserver::notifyObserver)
76 static inline bool is3xxRedirect(const ResourceResponse& response)
78 return response.httpStatusCode() >= 300 && response.httpStatusCode() <= 399;
81 bool ResourceLoadObserver::shouldLog(bool usesEphemeralSession) const
83 return DeprecatedGlobalSettings::resourceLoadStatisticsEnabled() && !usesEphemeralSession && m_notificationCallback;
86 void ResourceLoadObserver::logSubresourceLoading(const Frame* frame, const ResourceRequest& newRequest, const ResourceResponse& redirectResponse)
88 ASSERT(frame->page());
90 auto* page = frame->page();
91 if (!shouldLog(page->usesEphemeralSession()))
94 bool isRedirect = is3xxRedirect(redirectResponse);
95 const URL& sourceURL = redirectResponse.url();
96 const URL& targetURL = newRequest.url();
97 const URL& mainFrameURL = frame ? frame->mainFrame().document()->url() : URL();
99 auto targetHost = targetURL.host();
100 auto mainFrameHost = mainFrameURL.host();
102 if (targetHost.isEmpty() || mainFrameHost.isEmpty() || targetHost == mainFrameHost || (isRedirect && targetHost == sourceURL.host()))
105 auto targetPrimaryDomain = primaryDomain(targetURL);
106 auto mainFramePrimaryDomain = primaryDomain(mainFrameURL);
107 auto sourcePrimaryDomain = primaryDomain(sourceURL);
109 if (targetPrimaryDomain == mainFramePrimaryDomain || (isRedirect && targetPrimaryDomain == sourcePrimaryDomain))
112 bool shouldCallNotificationCallback = false;
114 auto& targetStatistics = ensureResourceStatisticsForPrimaryDomain(targetPrimaryDomain);
115 targetStatistics.lastSeen = ResourceLoadStatistics::reduceTimeResolution(WallTime::now());
116 if (targetStatistics.subresourceUnderTopFrameOrigins.add(mainFramePrimaryDomain).isNewEntry)
117 shouldCallNotificationCallback = true;
121 auto& redirectingOriginStatistics = ensureResourceStatisticsForPrimaryDomain(sourcePrimaryDomain);
122 bool isNewRedirectToEntry = redirectingOriginStatistics.subresourceUniqueRedirectsTo.add(targetPrimaryDomain).isNewEntry;
123 auto& targetStatistics = ensureResourceStatisticsForPrimaryDomain(targetPrimaryDomain);
124 bool isNewRedirectFromEntry = targetStatistics.subresourceUniqueRedirectsFrom.add(sourcePrimaryDomain).isNewEntry;
126 if (isNewRedirectToEntry || isNewRedirectFromEntry)
127 shouldCallNotificationCallback = true;
130 if (shouldCallNotificationCallback)
131 scheduleNotificationIfNeeded();
134 void ResourceLoadObserver::logWebSocketLoading(const URL& targetURL, const URL& mainFrameURL, bool usesEphemeralSession)
136 if (!shouldLog(usesEphemeralSession))
139 auto targetHost = targetURL.host();
140 auto mainFrameHost = mainFrameURL.host();
142 if (targetHost.isEmpty() || mainFrameHost.isEmpty() || targetHost == mainFrameHost)
145 auto targetPrimaryDomain = primaryDomain(targetURL);
146 auto mainFramePrimaryDomain = primaryDomain(mainFrameURL);
148 if (targetPrimaryDomain == mainFramePrimaryDomain)
151 auto& targetStatistics = ensureResourceStatisticsForPrimaryDomain(targetPrimaryDomain);
152 targetStatistics.lastSeen = ResourceLoadStatistics::reduceTimeResolution(WallTime::now());
153 if (targetStatistics.subresourceUnderTopFrameOrigins.add(mainFramePrimaryDomain).isNewEntry)
154 scheduleNotificationIfNeeded();
157 void ResourceLoadObserver::logUserInteractionWithReducedTimeResolution(const Document& document)
159 ASSERT(document.page());
161 if (!shouldLog(document.page()->usesEphemeralSession()))
164 auto& url = document.url();
165 if (url.isBlankURL() || url.isEmpty())
168 auto domain = primaryDomain(url);
169 auto newTime = ResourceLoadStatistics::reduceTimeResolution(WallTime::now());
170 auto lastReportedUserInteraction = m_lastReportedUserInteractionMap.get(domain);
171 if (newTime == lastReportedUserInteraction)
174 m_lastReportedUserInteractionMap.set(domain, newTime);
176 auto& statistics = ensureResourceStatisticsForPrimaryDomain(domain);
177 statistics.hadUserInteraction = true;
178 statistics.lastSeen = newTime;
179 statistics.mostRecentUserInteractionTime = newTime;
181 #if HAVE(CFNETWORK_STORAGE_PARTITIONING)
182 if (auto* opener = document.frame()->loader().opener()) {
183 if (auto* openerDocument = opener->document()) {
184 if (auto* openerFrame = openerDocument->frame()) {
185 if (auto openerPageID = openerFrame->loader().client().pageID()) {
186 requestStorageAccessUnderOpener(domain, openerPageID.value(), *openerDocument, true);
193 m_notificationTimer.stop();
196 #if HAVE(CFNETWORK_STORAGE_PARTITIONING) && !RELEASE_LOG_DISABLED
197 if (shouldLogUserInteraction()) {
198 auto counter = ++m_loggingCounter;
199 #define LOCAL_LOG(str, ...) \
200 RELEASE_LOG(ResourceLoadStatistics, "ResourceLoadObserver::logUserInteraction: counter = %" PRIu64 ": " str, counter, ##__VA_ARGS__)
202 auto escapeForJSON = [](String s) {
203 s.replace('\\', "\\\\").replace('"', "\\\"");
206 auto escapedURL = escapeForJSON(url.string());
207 auto escapedDomain = escapeForJSON(domain);
209 LOCAL_LOG(R"({ "url": "%{public}s",)", escapedURL.utf8().data());
210 LOCAL_LOG(R"( "domain" : "%{public}s",)", escapedDomain.utf8().data());
211 LOCAL_LOG(R"( "until" : %f })", newTime.secondsSinceEpoch().seconds());
218 void ResourceLoadObserver::logWindowCreation(const URL& popupUrl, uint64_t openerPageID, Document& openerDocument)
220 #if HAVE(CFNETWORK_STORAGE_PARTITIONING)
221 requestStorageAccessUnderOpener(primaryDomain(popupUrl), openerPageID, openerDocument, false);
223 UNUSED_PARAM(popupUrl);
224 UNUSED_PARAM(openerPageID);
225 UNUSED_PARAM(openerDocument);
229 #if HAVE(CFNETWORK_STORAGE_PARTITIONING)
230 void ResourceLoadObserver::requestStorageAccessUnderOpener(const String& domainInNeedOfStorageAccess, uint64_t openerPageID, Document& openerDocument, bool isTriggeredByUserGesture)
232 auto openerUrl = openerDocument.url();
233 auto openerPrimaryDomain = primaryDomain(openerUrl);
234 if (domainInNeedOfStorageAccess != openerPrimaryDomain
235 && !openerDocument.hasRequestedPageSpecificStorageAccessWithUserInteraction(domainInNeedOfStorageAccess)
236 && !equalIgnoringASCIICase(openerUrl.string(), blankURL())) {
237 m_requestStorageAccessUnderOpenerCallback(domainInNeedOfStorageAccess, openerPageID, openerPrimaryDomain, isTriggeredByUserGesture);
238 // Remember user interaction-based requests since they don't need to be repeated.
239 if (isTriggeredByUserGesture)
240 openerDocument.setHasRequestedPageSpecificStorageAccessWithUserInteraction(domainInNeedOfStorageAccess);
245 ResourceLoadStatistics& ResourceLoadObserver::ensureResourceStatisticsForPrimaryDomain(const String& primaryDomain)
247 auto addResult = m_resourceStatisticsMap.ensure(primaryDomain, [&primaryDomain] {
248 return ResourceLoadStatistics(primaryDomain);
250 return addResult.iterator->value;
253 void ResourceLoadObserver::scheduleNotificationIfNeeded()
255 ASSERT(m_notificationCallback);
256 if (m_resourceStatisticsMap.isEmpty()) {
257 m_notificationTimer.stop();
261 if (!m_notificationTimer.isActive())
262 m_notificationTimer.startOneShot(minimumNotificationInterval);
265 void ResourceLoadObserver::notifyObserver()
267 ASSERT(m_notificationCallback);
268 m_notificationTimer.stop();
269 m_notificationCallback(takeStatistics());
272 String ResourceLoadObserver::statisticsForOrigin(const String& origin)
274 auto iter = m_resourceStatisticsMap.find(origin);
275 if (iter == m_resourceStatisticsMap.end())
276 return emptyString();
278 return "Statistics for " + origin + ":\n" + iter->value.toString();
281 Vector<ResourceLoadStatistics> ResourceLoadObserver::takeStatistics()
283 Vector<ResourceLoadStatistics> statistics;
284 statistics.reserveInitialCapacity(m_resourceStatisticsMap.size());
285 for (auto& statistic : m_resourceStatisticsMap.values())
286 statistics.uncheckedAppend(WTFMove(statistic));
288 m_resourceStatisticsMap.clear();
293 void ResourceLoadObserver::clearState()
295 m_notificationTimer.stop();
296 m_resourceStatisticsMap.clear();
297 m_lastReportedUserInteractionMap.clear();
300 URL ResourceLoadObserver::nonNullOwnerURL(const Document& document) const
302 auto url = document.url();
303 auto* frame = document.frame();
304 auto host = document.url().host();
306 while ((host.isNull() || host.isEmpty()) && frame && !frame->isMainFrame()) {
307 auto* ownerElement = frame->ownerElement();
309 ASSERT(ownerElement != nullptr);
311 auto& doc = ownerElement->document();
320 } // namespace WebCore