REGRESSION(223307): ASSERTION in WebCore::ResourceLoadObserver::logUserInteractionWit...
[WebKit-https.git] / Source / WebCore / loader / ResourceLoadObserver.cpp
1 /*
2  * Copyright (C) 2016-2017 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 "Document.h"
30 #include "Frame.h"
31 #include "FrameLoader.h"
32 #include "HTMLFrameOwnerElement.h"
33 #include "Logging.h"
34 #include "MainFrame.h"
35 #include "Page.h"
36 #include "ResourceLoadStatistics.h"
37 #include "ResourceRequest.h"
38 #include "ResourceResponse.h"
39 #include "SecurityOrigin.h"
40 #include "Settings.h"
41 #include "URL.h"
42
43 namespace WebCore {
44
45 template<typename T> static inline String primaryDomain(const T& value)
46 {
47     return ResourceLoadStatistics::primaryDomain(value);
48 }
49
50 static Seconds timestampResolution { 1_h };
51 static const Seconds minimumNotificationInterval { 5_s };
52
53 ResourceLoadObserver& ResourceLoadObserver::shared()
54 {
55     static NeverDestroyed<ResourceLoadObserver> resourceLoadObserver;
56     return resourceLoadObserver;
57 }
58
59 static bool shouldEnableSiteSpecificQuirks(Page* page)
60 {
61 #if PLATFORM(IOS)
62     UNUSED_PARAM(page);
63
64     // There is currently no way to toggle the needsSiteSpecificQuirks setting on iOS so we always enable
65     // the site-specific quirks on iOS.
66     return true;
67 #else
68     return page && page->settings().needsSiteSpecificQuirks();
69 #endif
70 }
71
72 // FIXME: Temporary fix for <rdar://problem/32343256> until content can be updated.
73 static bool areDomainsAssociated(Page* page, const String& firstDomain, const String& secondDomain)
74 {
75     static NeverDestroyed<HashMap<String, unsigned>> metaDomainIdentifiers = [] {
76         HashMap<String, unsigned> map;
77
78         // Domains owned by Dow Jones & Company, Inc.
79         const unsigned dowJonesIdentifier = 1;
80         map.add(ASCIILiteral("dowjones.com"), dowJonesIdentifier);
81         map.add(ASCIILiteral("wsj.com"), dowJonesIdentifier);
82         map.add(ASCIILiteral("barrons.com"), dowJonesIdentifier);
83         map.add(ASCIILiteral("marketwatch.com"), dowJonesIdentifier);
84         map.add(ASCIILiteral("wsjplus.com"), dowJonesIdentifier);
85
86         return map;
87     }();
88
89     if (firstDomain == secondDomain)
90         return true;
91
92     ASSERT(!equalIgnoringASCIICase(firstDomain, secondDomain));
93
94     if (!shouldEnableSiteSpecificQuirks(page))
95         return false;
96
97     unsigned firstMetaDomainIdentifier = metaDomainIdentifiers.get().get(firstDomain);
98     if (!firstMetaDomainIdentifier)
99         return false;
100
101     return firstMetaDomainIdentifier == metaDomainIdentifiers.get().get(secondDomain);
102 }
103
104 void ResourceLoadObserver::setNotificationCallback(WTF::Function<void (Vector<ResourceLoadStatistics>&&)>&& notificationCallback)
105 {
106     ASSERT(!m_notificationCallback);
107     m_notificationCallback = WTFMove(notificationCallback);
108 }
109
110 ResourceLoadObserver::ResourceLoadObserver()
111     : m_notificationTimer(*this, &ResourceLoadObserver::notifyObserver)
112 {
113 }
114
115 static inline bool is3xxRedirect(const ResourceResponse& response)
116 {
117     return response.httpStatusCode() >= 300 && response.httpStatusCode() <= 399;
118 }
119
120 bool ResourceLoadObserver::shouldLog(Page* page) const
121 {
122     // FIXME: Err on the safe side until we have sorted out what to do in worker contexts
123     if (!page)
124         return false;
125
126     return Settings::resourceLoadStatisticsEnabled() && !page->usesEphemeralSession() && m_notificationCallback;
127 }
128
129 static WallTime reduceToHourlyTimeResolution(WallTime time)
130 {
131     return WallTime::fromRawSeconds(std::floor(time.secondsSinceEpoch() / timestampResolution) * timestampResolution.seconds());
132 }
133
134 void ResourceLoadObserver::logFrameNavigation(const Frame& frame, const Frame& topFrame, const ResourceRequest& newRequest, const URL& redirectUrl)
135 {
136     ASSERT(frame.document());
137     ASSERT(topFrame.document());
138     ASSERT(topFrame.page());
139
140     if (frame.isMainFrame())
141         return;
142     
143     auto* page = topFrame.page();
144     if (!shouldLog(page))
145         return;
146
147     auto sourceURL = redirectUrl;
148     bool isRedirect = !redirectUrl.isNull();
149     if (!isRedirect)
150         sourceURL = nonNullOwnerURL(*frame.document());
151
152     auto& targetURL = newRequest.url();
153     auto& mainFrameURL = topFrame.document()->url();
154     
155     if (!targetURL.isValid() || !mainFrameURL.isValid())
156         return;
157
158     auto targetHost = targetURL.host();
159     auto mainFrameHost = mainFrameURL.host();
160
161     if (targetHost.isEmpty() || mainFrameHost.isEmpty() || targetHost == sourceURL.host())
162         return;
163
164     auto targetPrimaryDomain = primaryDomain(targetURL);
165     auto mainFramePrimaryDomain = primaryDomain(mainFrameURL);
166     auto sourcePrimaryDomain = primaryDomain(sourceURL);
167     bool shouldCallNotificationCallback = false;
168
169     if (targetHost != mainFrameHost
170         && !(areDomainsAssociated(page, targetPrimaryDomain, mainFramePrimaryDomain) || areDomainsAssociated(page, targetPrimaryDomain, sourcePrimaryDomain))) {
171         auto& targetStatistics = ensureResourceStatisticsForPrimaryDomain(targetPrimaryDomain);
172         targetStatistics.lastSeen = reduceToHourlyTimeResolution(WallTime::now());
173         if (targetStatistics.subframeUnderTopFrameOrigins.add(mainFramePrimaryDomain).isNewEntry)
174             shouldCallNotificationCallback = true;
175     }
176
177     if (isRedirect
178         && !areDomainsAssociated(page, sourcePrimaryDomain, targetPrimaryDomain)) {
179         auto& redirectingOriginStatistics = ensureResourceStatisticsForPrimaryDomain(sourcePrimaryDomain);
180         if (redirectingOriginStatistics.subresourceUniqueRedirectsTo.add(targetPrimaryDomain).isNewEntry)
181             shouldCallNotificationCallback = true;
182     }
183
184     if (shouldCallNotificationCallback)
185         scheduleNotificationIfNeeded();
186 }
187
188 // FIXME: This quirk was added to address <rdar://problem/33325881> and should be removed once content is fixed.
189 static bool resourceNeedsSSOQuirk(Page* page, const URL& url)
190 {
191     if (!shouldEnableSiteSpecificQuirks(page))
192         return false;
193
194     return equalIgnoringASCIICase(url.host(), "sp.auth.adobe.com");
195 }
196
197 void ResourceLoadObserver::logSubresourceLoading(const Frame* frame, const ResourceRequest& newRequest, const ResourceResponse& redirectResponse)
198 {
199     ASSERT(frame->page());
200
201     auto* page = frame->page();
202     if (!shouldLog(page))
203         return;
204
205     bool isRedirect = is3xxRedirect(redirectResponse);
206     const URL& sourceURL = redirectResponse.url();
207     const URL& targetURL = newRequest.url();
208     const URL& mainFrameURL = frame ? frame->mainFrame().document()->url() : URL();
209     
210     auto targetHost = targetURL.host();
211     auto mainFrameHost = mainFrameURL.host();
212
213     if (targetHost.isEmpty() || mainFrameHost.isEmpty() || targetHost == mainFrameHost || (isRedirect && targetHost == sourceURL.host()))
214         return;
215
216     auto targetPrimaryDomain = primaryDomain(targetURL);
217     auto mainFramePrimaryDomain = primaryDomain(mainFrameURL);
218     auto sourcePrimaryDomain = primaryDomain(sourceURL);
219     
220     if (areDomainsAssociated(page, targetPrimaryDomain, mainFramePrimaryDomain) || (isRedirect && areDomainsAssociated(page, targetPrimaryDomain, sourcePrimaryDomain)))
221         return;
222
223     if (resourceNeedsSSOQuirk(page, targetURL))
224         return;
225
226     bool shouldCallNotificationCallback = false;
227     {
228         auto& targetStatistics = ensureResourceStatisticsForPrimaryDomain(targetPrimaryDomain);
229         targetStatistics.lastSeen = reduceToHourlyTimeResolution(WallTime::now());
230         if (targetStatistics.subresourceUnderTopFrameOrigins.add(mainFramePrimaryDomain).isNewEntry)
231             shouldCallNotificationCallback = true;
232     }
233
234     if (isRedirect) {
235         auto& redirectingOriginStatistics = ensureResourceStatisticsForPrimaryDomain(sourcePrimaryDomain);
236         if (redirectingOriginStatistics.subresourceUniqueRedirectsTo.add(targetPrimaryDomain).isNewEntry)
237             shouldCallNotificationCallback = true;
238     }
239
240     if (shouldCallNotificationCallback)
241         scheduleNotificationIfNeeded();
242 }
243
244 void ResourceLoadObserver::logWebSocketLoading(const Frame* frame, const URL& targetURL)
245 {
246     // FIXME: Web sockets can run in detached frames. Decide how to count such connections.
247     // See LayoutTests/http/tests/websocket/construct-in-detached-frame.html
248     if (!frame)
249         return;
250
251     auto* page = frame->page();
252     if (!shouldLog(page))
253         return;
254
255     auto& mainFrameURL = frame->mainFrame().document()->url();
256
257     auto targetHost = targetURL.host();
258     auto mainFrameHost = mainFrameURL.host();
259     
260     if (targetHost.isEmpty() || mainFrameHost.isEmpty() || targetHost == mainFrameHost)
261         return;
262     
263     auto targetPrimaryDomain = primaryDomain(targetURL);
264     auto mainFramePrimaryDomain = primaryDomain(mainFrameURL);
265     
266     if (areDomainsAssociated(page, targetPrimaryDomain, mainFramePrimaryDomain))
267         return;
268
269     auto& targetStatistics = ensureResourceStatisticsForPrimaryDomain(targetPrimaryDomain);
270     targetStatistics.lastSeen = reduceToHourlyTimeResolution(WallTime::now());
271     if (targetStatistics.subresourceUnderTopFrameOrigins.add(mainFramePrimaryDomain).isNewEntry)
272         scheduleNotificationIfNeeded();
273 }
274
275 void ResourceLoadObserver::logUserInteractionWithReducedTimeResolution(const Document& document)
276 {
277     if (!shouldLog(document.page()))
278         return;
279
280     ASSERT(document.page());
281
282     auto& url = document.url();
283     if (url.isBlankURL() || url.isEmpty())
284         return;
285
286     auto domain = primaryDomain(url);
287     auto newTime = reduceToHourlyTimeResolution(WallTime::now());
288     auto lastReportedUserInteraction = m_lastReportedUserInteractionMap.get(domain);
289     if (newTime == lastReportedUserInteraction)
290         return;
291
292     m_lastReportedUserInteractionMap.set(domain, newTime);
293
294     auto& statistics = ensureResourceStatisticsForPrimaryDomain(domain);
295     statistics.hadUserInteraction = true;
296     statistics.lastSeen = newTime;
297     statistics.mostRecentUserInteractionTime = newTime;
298
299     m_notificationTimer.stop();
300     notifyObserver();
301 }
302
303 ResourceLoadStatistics& ResourceLoadObserver::ensureResourceStatisticsForPrimaryDomain(const String& primaryDomain)
304 {
305     auto addResult = m_resourceStatisticsMap.ensure(primaryDomain, [&primaryDomain] {
306         return ResourceLoadStatistics(primaryDomain);
307     });
308     return addResult.iterator->value;
309 }
310
311 void ResourceLoadObserver::scheduleNotificationIfNeeded()
312 {
313     ASSERT(m_notificationCallback);
314     if (m_resourceStatisticsMap.isEmpty()) {
315         m_notificationTimer.stop();
316         return;
317     }
318
319     if (!m_notificationTimer.isActive())
320         m_notificationTimer.startOneShot(minimumNotificationInterval);
321 }
322
323 void ResourceLoadObserver::notifyObserver()
324 {
325     ASSERT(m_notificationCallback);
326     m_notificationTimer.stop();
327     m_notificationCallback(takeStatistics());
328 }
329
330 String ResourceLoadObserver::statisticsForOrigin(const String& origin)
331 {
332     auto iter = m_resourceStatisticsMap.find(origin);
333     if (iter == m_resourceStatisticsMap.end())
334         return emptyString();
335
336     return "Statistics for " + origin + ":\n" + iter->value.toString();
337 }
338
339 Vector<ResourceLoadStatistics> ResourceLoadObserver::takeStatistics()
340 {
341     Vector<ResourceLoadStatistics> statistics;
342     statistics.reserveInitialCapacity(m_resourceStatisticsMap.size());
343     for (auto& statistic : m_resourceStatisticsMap.values())
344         statistics.uncheckedAppend(WTFMove(statistic));
345
346     m_resourceStatisticsMap.clear();
347
348     return statistics;
349 }
350
351 void ResourceLoadObserver::clearState()
352 {
353     m_notificationTimer.stop();
354     m_resourceStatisticsMap.clear();
355     m_lastReportedUserInteractionMap.clear();
356 }
357
358 URL ResourceLoadObserver::nonNullOwnerURL(const Document& document) const
359 {
360     auto url = document.url();
361     auto* frame = document.frame();
362     auto host = document.url().host();
363
364     while ((host.isNull() || host.isEmpty()) && !frame->isMainFrame()) {
365         auto* ownerElement = frame->ownerElement();
366
367         ASSERT(ownerElement != nullptr);
368         
369         auto& doc = ownerElement->document();
370         frame = doc.frame();
371         url = doc.url();
372         host = url.host();
373     }
374
375     return url;
376 }
377
378 } // namespace WebCore