Compile out Web API Statistics Collection
[WebKit-https.git] / Source / WebCore / loader / ResourceLoadObserver.cpp
1 /*
2  * Copyright (C) 2016-2018 Apple Inc. All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
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.
12  *
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.
24  */
25
26 #include "config.h"
27 #include "ResourceLoadObserver.h"
28
29 #include "DeprecatedGlobalSettings.h"
30 #include "Document.h"
31 #include "Frame.h"
32 #include "FrameLoader.h"
33 #include "HTMLFrameOwnerElement.h"
34 #include "Logging.h"
35 #include "Page.h"
36 #include "ResourceLoadStatistics.h"
37 #include "ResourceRequest.h"
38 #include "ResourceResponse.h"
39 #include "RuntimeEnabledFeatures.h"
40 #include "ScriptExecutionContext.h"
41 #include "SecurityOrigin.h"
42 #include "Settings.h"
43 #include <wtf/URL.h>
44
45 namespace WebCore {
46
47 template<typename T> static inline String primaryDomain(const T& value)
48 {
49     return ResourceLoadStatistics::primaryDomain(value);
50 }
51
52 static const Seconds minimumNotificationInterval { 5_s };
53
54 ResourceLoadObserver& ResourceLoadObserver::shared()
55 {
56     static NeverDestroyed<ResourceLoadObserver> resourceLoadObserver;
57     return resourceLoadObserver;
58 }
59
60 void ResourceLoadObserver::setNotificationCallback(WTF::Function<void (Vector<ResourceLoadStatistics>&&)>&& notificationCallback)
61 {
62     ASSERT(!m_notificationCallback);
63     m_notificationCallback = WTFMove(notificationCallback);
64 }
65
66 void ResourceLoadObserver::setRequestStorageAccessUnderOpenerCallback(WTF::Function<void(const String& domainInNeedOfStorageAccess, uint64_t openerPageID, const String& openerDomain)>&& callback)
67 {
68     ASSERT(!m_requestStorageAccessUnderOpenerCallback);
69     m_requestStorageAccessUnderOpenerCallback = WTFMove(callback);
70 }
71
72 ResourceLoadObserver::ResourceLoadObserver()
73     : m_notificationTimer(*this, &ResourceLoadObserver::notifyObserver)
74 {
75 }
76
77 static inline bool is3xxRedirect(const ResourceResponse& response)
78 {
79     return response.httpStatusCode() >= 300 && response.httpStatusCode() <= 399;
80 }
81
82 bool ResourceLoadObserver::shouldLog(bool usesEphemeralSession) const
83 {
84     return DeprecatedGlobalSettings::resourceLoadStatisticsEnabled() && !usesEphemeralSession && m_notificationCallback;
85 }
86
87 void ResourceLoadObserver::logSubresourceLoading(const Frame* frame, const ResourceRequest& newRequest, const ResourceResponse& redirectResponse)
88 {
89     ASSERT(frame->page());
90
91     if (!frame)
92         return;
93
94     auto* page = frame->page();
95     if (!page || !shouldLog(page->usesEphemeralSession()))
96         return;
97
98     bool isRedirect = is3xxRedirect(redirectResponse);
99     const URL& sourceURL = redirectResponse.url();
100     const URL& targetURL = newRequest.url();
101     const URL& mainFrameURL = frame ? frame->mainFrame().document()->url() : URL();
102     
103     auto targetHost = targetURL.host();
104     auto mainFrameHost = mainFrameURL.host();
105
106     if (targetHost.isEmpty() || mainFrameHost.isEmpty() || targetHost == mainFrameHost || (isRedirect && targetHost == sourceURL.host()))
107         return;
108
109     auto targetPrimaryDomain = primaryDomain(targetURL);
110     auto mainFramePrimaryDomain = primaryDomain(mainFrameURL);
111     auto sourcePrimaryDomain = primaryDomain(sourceURL);
112
113     if (targetPrimaryDomain == mainFramePrimaryDomain || (isRedirect && targetPrimaryDomain == sourcePrimaryDomain))
114         return;
115
116     bool shouldCallNotificationCallback = false;
117     {
118         auto& targetStatistics = ensureResourceStatisticsForPrimaryDomain(targetPrimaryDomain);
119         targetStatistics.lastSeen = ResourceLoadStatistics::reduceTimeResolution(WallTime::now());
120         if (targetStatistics.subresourceUnderTopFrameOrigins.add(mainFramePrimaryDomain).isNewEntry)
121             shouldCallNotificationCallback = true;
122     }
123
124     if (isRedirect) {
125         auto& redirectingOriginStatistics = ensureResourceStatisticsForPrimaryDomain(sourcePrimaryDomain);
126         bool isNewRedirectToEntry = redirectingOriginStatistics.subresourceUniqueRedirectsTo.add(targetPrimaryDomain).isNewEntry;
127         auto& targetStatistics = ensureResourceStatisticsForPrimaryDomain(targetPrimaryDomain);
128         bool isNewRedirectFromEntry = targetStatistics.subresourceUniqueRedirectsFrom.add(sourcePrimaryDomain).isNewEntry;
129
130         if (isNewRedirectToEntry || isNewRedirectFromEntry)
131             shouldCallNotificationCallback = true;
132     }
133
134     if (shouldCallNotificationCallback)
135         scheduleNotificationIfNeeded();
136 }
137
138 void ResourceLoadObserver::logWebSocketLoading(const URL& targetURL, const URL& mainFrameURL, bool usesEphemeralSession)
139 {
140     if (!shouldLog(usesEphemeralSession))
141         return;
142
143     auto targetHost = targetURL.host();
144     auto mainFrameHost = mainFrameURL.host();
145     
146     if (targetHost.isEmpty() || mainFrameHost.isEmpty() || targetHost == mainFrameHost)
147         return;
148     
149     auto targetPrimaryDomain = primaryDomain(targetURL);
150     auto mainFramePrimaryDomain = primaryDomain(mainFrameURL);
151
152     if (targetPrimaryDomain == mainFramePrimaryDomain)
153         return;
154
155     auto& targetStatistics = ensureResourceStatisticsForPrimaryDomain(targetPrimaryDomain);
156     targetStatistics.lastSeen = ResourceLoadStatistics::reduceTimeResolution(WallTime::now());
157     if (targetStatistics.subresourceUnderTopFrameOrigins.add(mainFramePrimaryDomain).isNewEntry)
158         scheduleNotificationIfNeeded();
159 }
160
161 void ResourceLoadObserver::logUserInteractionWithReducedTimeResolution(const Document& document)
162 {
163     if (!shouldLog(document.sessionID().isEphemeral()))
164         return;
165
166     auto& url = document.url();
167     if (url.protocolIsAbout() || url.isEmpty())
168         return;
169
170     auto domain = primaryDomain(url);
171     auto newTime = ResourceLoadStatistics::reduceTimeResolution(WallTime::now());
172     auto lastReportedUserInteraction = m_lastReportedUserInteractionMap.get(domain);
173     if (newTime == lastReportedUserInteraction)
174         return;
175
176     m_lastReportedUserInteractionMap.set(domain, newTime);
177
178     auto& statistics = ensureResourceStatisticsForPrimaryDomain(domain);
179     statistics.hadUserInteraction = true;
180     statistics.lastSeen = newTime;
181     statistics.mostRecentUserInteractionTime = newTime;
182
183 #if ENABLE(RESOURCE_LOAD_STATISTICS)
184     if (auto* opener = document.frame()->loader().opener()) {
185         if (auto* openerDocument = opener->document()) {
186             if (auto* openerFrame = openerDocument->frame()) {
187                 if (auto openerPageID = openerFrame->loader().client().pageID()) {
188                     requestStorageAccessUnderOpener(domain, openerPageID.value(), *openerDocument);
189                 }
190             }
191         }
192     }
193 #endif
194
195     m_notificationTimer.stop();
196     notifyObserver();
197
198 #if ENABLE(RESOURCE_LOAD_STATISTICS) && !RELEASE_LOG_DISABLED
199     if (shouldLogUserInteraction()) {
200         auto counter = ++m_loggingCounter;
201 #define LOCAL_LOG(str, ...) \
202         RELEASE_LOG(ResourceLoadStatistics, "ResourceLoadObserver::logUserInteraction: counter = %" PRIu64 ": " str, counter, ##__VA_ARGS__)
203
204         auto escapeForJSON = [](String s) {
205             s.replace('\\', "\\\\").replace('"', "\\\"");
206             return s;
207         };
208         auto escapedURL = escapeForJSON(url.string());
209         auto escapedDomain = escapeForJSON(domain);
210
211         LOCAL_LOG(R"({ "url": "%{public}s",)", escapedURL.utf8().data());
212         LOCAL_LOG(R"(  "domain" : "%{public}s",)", escapedDomain.utf8().data());
213         LOCAL_LOG(R"(  "until" : %f })", newTime.secondsSinceEpoch().seconds());
214
215 #undef LOCAL_LOG
216     }
217 #endif
218 }
219
220 #if ENABLE(RESOURCE_LOAD_STATISTICS)
221 void ResourceLoadObserver::requestStorageAccessUnderOpener(const String& domainInNeedOfStorageAccess, uint64_t openerPageID, Document& openerDocument)
222 {
223     auto openerUrl = openerDocument.url();
224     auto openerPrimaryDomain = primaryDomain(openerUrl);
225     if (domainInNeedOfStorageAccess != openerPrimaryDomain
226         && !openerDocument.hasRequestedPageSpecificStorageAccessWithUserInteraction(domainInNeedOfStorageAccess)
227         && !equalIgnoringASCIICase(openerUrl.string(), WTF::blankURL())) {
228         m_requestStorageAccessUnderOpenerCallback(domainInNeedOfStorageAccess, openerPageID, openerPrimaryDomain);
229         // Remember user interaction-based requests since they don't need to be repeated.
230         openerDocument.setHasRequestedPageSpecificStorageAccessWithUserInteraction(domainInNeedOfStorageAccess);
231     }
232 }
233 #endif
234
235 void ResourceLoadObserver::logFontLoad(const Document& document, const String& familyName, bool loadStatus)
236 {
237 #if ENABLE(WEB_API_STATISTICS)
238     if (!shouldLog(document.sessionID().isEphemeral()))
239         return;
240     auto registrableDomain = primaryDomain(document.url());
241     auto& statistics = ensureResourceStatisticsForPrimaryDomain(registrableDomain);
242     bool shouldCallNotificationCallback = false;
243     if (!loadStatus) {
244         if (statistics.fontsFailedToLoad.add(familyName).isNewEntry)
245             shouldCallNotificationCallback = true;
246     } else {
247         if (statistics.fontsSuccessfullyLoaded.add(familyName).isNewEntry)
248             shouldCallNotificationCallback = true;
249     }
250     auto mainFrameRegistrableDomain = primaryDomain(document.topDocument().url());
251     if (statistics.topFrameRegistrableDomainsWhichAccessedWebAPIs.add(mainFrameRegistrableDomain).isNewEntry)
252         shouldCallNotificationCallback = true;
253     if (shouldCallNotificationCallback)
254         scheduleNotificationIfNeeded();
255 #else
256     UNUSED_PARAM(document);
257     UNUSED_PARAM(familyName);
258     UNUSED_PARAM(loadStatus);
259 #endif
260 }
261     
262 void ResourceLoadObserver::logCanvasRead(const Document& document)
263 {
264 #if ENABLE(WEB_API_STATISTICS)
265     if (!shouldLog(document.sessionID().isEphemeral()))
266         return;
267     auto registrableDomain = primaryDomain(document.url());
268     auto& statistics = ensureResourceStatisticsForPrimaryDomain(registrableDomain);
269     auto mainFrameRegistrableDomain = primaryDomain(document.topDocument().url());
270     statistics.canvasActivityRecord.wasDataRead = true;
271     if (statistics.topFrameRegistrableDomainsWhichAccessedWebAPIs.add(mainFrameRegistrableDomain).isNewEntry)
272         scheduleNotificationIfNeeded();
273 #else
274     UNUSED_PARAM(document);
275 #endif
276 }
277
278 void ResourceLoadObserver::logCanvasWriteOrMeasure(const Document& document, const String& textWritten)
279 {
280 #if ENABLE(WEB_API_STATISTICS)
281     if (!shouldLog(document.sessionID().isEphemeral()))
282         return;
283     auto registrableDomain = primaryDomain(document.url());
284     auto& statistics = ensureResourceStatisticsForPrimaryDomain(registrableDomain);
285     bool shouldCallNotificationCallback = false;
286     auto mainFrameRegistrableDomain = primaryDomain(document.topDocument().url());
287     if (statistics.canvasActivityRecord.recordWrittenOrMeasuredText(textWritten))
288         shouldCallNotificationCallback = true;
289     if (statistics.topFrameRegistrableDomainsWhichAccessedWebAPIs.add(mainFrameRegistrableDomain).isNewEntry)
290         shouldCallNotificationCallback = true;
291     if (shouldCallNotificationCallback)
292         scheduleNotificationIfNeeded();
293 #else
294     UNUSED_PARAM(document);
295     UNUSED_PARAM(textWritten);
296 #endif
297 }
298     
299 void ResourceLoadObserver::logNavigatorAPIAccessed(const Document& document, const ResourceLoadStatistics::NavigatorAPI functionName)
300 {
301 #if ENABLE(WEB_API_STATISTICS)
302     if (!shouldLog(document.sessionID().isEphemeral()))
303         return;
304     auto registrableDomain = primaryDomain(document.url());
305     auto& statistics = ensureResourceStatisticsForPrimaryDomain(registrableDomain);
306     bool shouldCallNotificationCallback = false;
307     if (!statistics.navigatorFunctionsAccessed.contains(functionName)) {
308         statistics.navigatorFunctionsAccessed.add(functionName);
309         shouldCallNotificationCallback = true;
310     }
311     auto mainFrameRegistrableDomain = primaryDomain(document.topDocument().url());
312     if (statistics.topFrameRegistrableDomainsWhichAccessedWebAPIs.add(mainFrameRegistrableDomain).isNewEntry)
313         shouldCallNotificationCallback = true;
314     if (shouldCallNotificationCallback)
315         scheduleNotificationIfNeeded();
316 #else
317     UNUSED_PARAM(document);
318     UNUSED_PARAM(functionName);
319 #endif
320 }
321     
322 void ResourceLoadObserver::logScreenAPIAccessed(const Document& document, const ResourceLoadStatistics::ScreenAPI functionName)
323 {
324 #if ENABLE(WEB_API_STATISTICS)
325     if (!shouldLog(document.sessionID().isEphemeral()))
326         return;
327     auto registrableDomain = primaryDomain(document.url());
328     auto& statistics = ensureResourceStatisticsForPrimaryDomain(registrableDomain);
329     bool shouldCallNotificationCallback = false;
330     if (!statistics.screenFunctionsAccessed.contains(functionName)) {
331         statistics.screenFunctionsAccessed.add(functionName);
332         shouldCallNotificationCallback = true;
333     }
334     auto mainFrameRegistrableDomain = primaryDomain(document.topDocument().url());
335     if (statistics.topFrameRegistrableDomainsWhichAccessedWebAPIs.add(mainFrameRegistrableDomain).isNewEntry)
336         shouldCallNotificationCallback = true;
337     if (shouldCallNotificationCallback)
338         scheduleNotificationIfNeeded();
339 #else
340     UNUSED_PARAM(document);
341     UNUSED_PARAM(functionName);
342 #endif
343 }
344     
345 ResourceLoadStatistics& ResourceLoadObserver::ensureResourceStatisticsForPrimaryDomain(const String& primaryDomain)
346 {
347     auto addResult = m_resourceStatisticsMap.ensure(primaryDomain, [&primaryDomain] {
348         return ResourceLoadStatistics(primaryDomain);
349     });
350     return addResult.iterator->value;
351 }
352
353 void ResourceLoadObserver::scheduleNotificationIfNeeded()
354 {
355     ASSERT(m_notificationCallback);
356     if (m_resourceStatisticsMap.isEmpty()) {
357         m_notificationTimer.stop();
358         return;
359     }
360
361     if (!m_notificationTimer.isActive())
362         m_notificationTimer.startOneShot(minimumNotificationInterval);
363 }
364
365 void ResourceLoadObserver::notifyObserver()
366 {
367     ASSERT(m_notificationCallback);
368     m_notificationTimer.stop();
369     m_notificationCallback(takeStatistics());
370 }
371
372 String ResourceLoadObserver::statisticsForOrigin(const String& origin)
373 {
374     auto iter = m_resourceStatisticsMap.find(origin);
375     if (iter == m_resourceStatisticsMap.end())
376         return emptyString();
377
378     return "Statistics for " + origin + ":\n" + iter->value.toString();
379 }
380
381 Vector<ResourceLoadStatistics> ResourceLoadObserver::takeStatistics()
382 {
383     Vector<ResourceLoadStatistics> statistics;
384     statistics.reserveInitialCapacity(m_resourceStatisticsMap.size());
385     for (auto& statistic : m_resourceStatisticsMap.values())
386         statistics.uncheckedAppend(WTFMove(statistic));
387
388     m_resourceStatisticsMap.clear();
389
390     return statistics;
391 }
392
393 void ResourceLoadObserver::clearState()
394 {
395     m_notificationTimer.stop();
396     m_resourceStatisticsMap.clear();
397     m_lastReportedUserInteractionMap.clear();
398 }
399
400 URL ResourceLoadObserver::nonNullOwnerURL(const Document& document) const
401 {
402     auto url = document.url();
403     auto* frame = document.frame();
404     auto host = document.url().host();
405
406     while ((host.isNull() || host.isEmpty()) && frame && !frame->isMainFrame()) {
407         auto* ownerElement = frame->ownerElement();
408
409         ASSERT(ownerElement != nullptr);
410         
411         auto& doc = ownerElement->document();
412         frame = doc.frame();
413         url = doc.url();
414         host = url.host();
415     }
416
417     return url;
418 }
419
420 } // namespace WebCore