1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5 #include "PreloaderBase.h"
6
7 #include "mozilla/dom/Document.h"
8 #include "mozilla/Telemetry.h"
9 #include "nsContentUtils.h"
10 #include "nsIAsyncVerifyRedirectCallback.h"
11 #include "nsIChannel.h"
12 #include "nsILoadGroup.h"
13 #include "nsIInterfaceRequestorUtils.h"
14 #include "nsIRedirectResultListener.h"
15
16 // Change this if we want to cancel and remove the associated preload on removal
17 // of all <link rel=preload> tags from the tree.
18 constexpr static bool kCancelAndRemovePreloadOnZeroReferences = false;
19
20 namespace mozilla {
21
UsageTimer(PreloaderBase * aPreload,dom::Document * aDocument)22 PreloaderBase::UsageTimer::UsageTimer(PreloaderBase* aPreload,
23 dom::Document* aDocument)
24 : mDocument(aDocument), mPreload(aPreload) {}
25
26 class PreloaderBase::RedirectSink final : public nsIInterfaceRequestor,
27 public nsIChannelEventSink,
28 public nsIRedirectResultListener {
29 RedirectSink() = delete;
30 virtual ~RedirectSink();
31
32 public:
33 NS_DECL_THREADSAFE_ISUPPORTS
34 NS_DECL_NSIINTERFACEREQUESTOR
35 NS_DECL_NSICHANNELEVENTSINK
36 NS_DECL_NSIREDIRECTRESULTLISTENER
37
38 RedirectSink(PreloaderBase* aPreloader, nsIInterfaceRequestor* aCallbacks);
39
40 private:
41 MainThreadWeakPtr<PreloaderBase> mPreloader;
42 nsCOMPtr<nsIInterfaceRequestor> mCallbacks;
43 nsCOMPtr<nsIChannel> mRedirectChannel;
44 };
45
RedirectSink(PreloaderBase * aPreloader,nsIInterfaceRequestor * aCallbacks)46 PreloaderBase::RedirectSink::RedirectSink(PreloaderBase* aPreloader,
47 nsIInterfaceRequestor* aCallbacks)
48 : mPreloader(aPreloader), mCallbacks(aCallbacks) {}
49
50 PreloaderBase::RedirectSink::~RedirectSink() = default;
51
NS_IMPL_ISUPPORTS(PreloaderBase::RedirectSink,nsIInterfaceRequestor,nsIChannelEventSink,nsIRedirectResultListener)52 NS_IMPL_ISUPPORTS(PreloaderBase::RedirectSink, nsIInterfaceRequestor,
53 nsIChannelEventSink, nsIRedirectResultListener)
54
55 NS_IMETHODIMP PreloaderBase::RedirectSink::AsyncOnChannelRedirect(
56 nsIChannel* aOldChannel, nsIChannel* aNewChannel, uint32_t aFlags,
57 nsIAsyncVerifyRedirectCallback* aCallback) {
58 MOZ_DIAGNOSTIC_ASSERT(NS_IsMainThread());
59
60 mRedirectChannel = aNewChannel;
61
62 // Deliberately adding this before confirmation.
63 nsCOMPtr<nsIURI> uri;
64 aNewChannel->GetOriginalURI(getter_AddRefs(uri));
65 if (mPreloader) {
66 mPreloader->mRedirectRecords.AppendElement(
67 RedirectRecord(aFlags, uri.forget()));
68 }
69
70 if (mCallbacks) {
71 nsCOMPtr<nsIChannelEventSink> sink(do_GetInterface(mCallbacks));
72 if (sink) {
73 return sink->AsyncOnChannelRedirect(aOldChannel, aNewChannel, aFlags,
74 aCallback);
75 }
76 }
77
78 aCallback->OnRedirectVerifyCallback(NS_OK);
79 return NS_OK;
80 }
81
OnRedirectResult(bool proceeding)82 NS_IMETHODIMP PreloaderBase::RedirectSink::OnRedirectResult(bool proceeding) {
83 if (proceeding && mRedirectChannel) {
84 mPreloader->mChannel = std::move(mRedirectChannel);
85 } else {
86 mRedirectChannel = nullptr;
87 }
88
89 if (mCallbacks) {
90 nsCOMPtr<nsIRedirectResultListener> sink(do_GetInterface(mCallbacks));
91 if (sink) {
92 return sink->OnRedirectResult(proceeding);
93 }
94 }
95
96 return NS_OK;
97 }
98
GetInterface(const nsIID & aIID,void ** aResult)99 NS_IMETHODIMP PreloaderBase::RedirectSink::GetInterface(const nsIID& aIID,
100 void** aResult) {
101 NS_ENSURE_ARG_POINTER(aResult);
102
103 if (aIID.Equals(NS_GET_IID(nsIChannelEventSink)) ||
104 aIID.Equals(NS_GET_IID(nsIRedirectResultListener))) {
105 return QueryInterface(aIID, aResult);
106 }
107
108 if (mCallbacks) {
109 return mCallbacks->GetInterface(aIID, aResult);
110 }
111
112 *aResult = nullptr;
113 return NS_ERROR_NO_INTERFACE;
114 }
115
~PreloaderBase()116 PreloaderBase::~PreloaderBase() { MOZ_ASSERT(NS_IsMainThread()); }
117
118 // static
AddLoadBackgroundFlag(nsIChannel * aChannel)119 void PreloaderBase::AddLoadBackgroundFlag(nsIChannel* aChannel) {
120 nsLoadFlags loadFlags;
121 aChannel->GetLoadFlags(&loadFlags);
122 aChannel->SetLoadFlags(loadFlags | nsIRequest::LOAD_BACKGROUND);
123 }
124
NotifyOpen(const PreloadHashKey & aKey,dom::Document * aDocument,bool aIsPreload)125 void PreloaderBase::NotifyOpen(const PreloadHashKey& aKey,
126 dom::Document* aDocument, bool aIsPreload) {
127 if (aDocument) {
128 DebugOnly<bool> alreadyRegistered =
129 aDocument->Preloads().RegisterPreload(aKey, this);
130 // This means there is already a preload registered under this key in this
131 // document. We only allow replacement when this is a regular load.
132 // Otherwise, this should never happen and is a suspected misuse of the API.
133 MOZ_ASSERT_IF(alreadyRegistered, !aIsPreload);
134 }
135
136 mKey = aKey;
137 mIsUsed = !aIsPreload;
138
139 if (!mIsUsed && !mUsageTimer) {
140 auto callback = MakeRefPtr<UsageTimer>(this, aDocument);
141 NS_NewTimerWithCallback(getter_AddRefs(mUsageTimer), callback, 10000,
142 nsITimer::TYPE_ONE_SHOT);
143 }
144
145 ReportUsageTelemetry();
146 }
147
NotifyOpen(const PreloadHashKey & aKey,nsIChannel * aChannel,dom::Document * aDocument,bool aIsPreload)148 void PreloaderBase::NotifyOpen(const PreloadHashKey& aKey, nsIChannel* aChannel,
149 dom::Document* aDocument, bool aIsPreload) {
150 NotifyOpen(aKey, aDocument, aIsPreload);
151 mChannel = aChannel;
152
153 nsCOMPtr<nsIInterfaceRequestor> callbacks;
154 mChannel->GetNotificationCallbacks(getter_AddRefs(callbacks));
155 RefPtr<RedirectSink> sink(new RedirectSink(this, callbacks));
156 mChannel->SetNotificationCallbacks(sink);
157 }
158
NotifyUsage(LoadBackground aLoadBackground)159 void PreloaderBase::NotifyUsage(LoadBackground aLoadBackground) {
160 if (!mIsUsed && mChannel && aLoadBackground == LoadBackground::Drop) {
161 nsLoadFlags loadFlags;
162 mChannel->GetLoadFlags(&loadFlags);
163
164 // Preloads are initially set the LOAD_BACKGROUND flag. When becoming
165 // regular loads by hitting its consuming tag, we need to drop that flag,
166 // which also means to re-add the request from/to it's loadgroup to reflect
167 // that flag change.
168 if (loadFlags & nsIRequest::LOAD_BACKGROUND) {
169 nsCOMPtr<nsILoadGroup> loadGroup;
170 mChannel->GetLoadGroup(getter_AddRefs(loadGroup));
171
172 if (loadGroup) {
173 nsresult status;
174 mChannel->GetStatus(&status);
175
176 nsresult rv = loadGroup->RemoveRequest(mChannel, nullptr, status);
177 mChannel->SetLoadFlags(loadFlags & ~nsIRequest::LOAD_BACKGROUND);
178 if (NS_SUCCEEDED(rv)) {
179 loadGroup->AddRequest(mChannel, nullptr);
180 }
181 }
182 }
183 }
184
185 mIsUsed = true;
186 ReportUsageTelemetry();
187 CancelUsageTimer();
188 }
189
RemoveSelf(dom::Document * aDocument)190 void PreloaderBase::RemoveSelf(dom::Document* aDocument) {
191 if (aDocument) {
192 aDocument->Preloads().DeregisterPreload(mKey);
193 }
194 }
195
NotifyRestart(dom::Document * aDocument,PreloaderBase * aNewPreloader)196 void PreloaderBase::NotifyRestart(dom::Document* aDocument,
197 PreloaderBase* aNewPreloader) {
198 RemoveSelf(aDocument);
199 mKey = PreloadHashKey();
200
201 CancelUsageTimer();
202
203 if (aNewPreloader) {
204 aNewPreloader->mNodes = std::move(mNodes);
205 }
206 }
207
NotifyStart(nsIRequest * aRequest)208 void PreloaderBase::NotifyStart(nsIRequest* aRequest) {
209 // If there is no channel assigned on this preloader, we are not between
210 // channel switching, so we can freely update the mShouldFireLoadEvent using
211 // the given channel.
212 if (mChannel && !SameCOMIdentity(aRequest, mChannel)) {
213 return;
214 }
215
216 nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(aRequest);
217 if (!httpChannel) {
218 return;
219 }
220
221 // if the load is cross origin without CORS, or the CORS access is rejected,
222 // always fire load event to avoid leaking site information.
223 nsresult rv;
224 nsCOMPtr<nsILoadInfo> loadInfo = httpChannel->LoadInfo();
225 mShouldFireLoadEvent =
226 loadInfo->GetTainting() == LoadTainting::Opaque ||
227 (loadInfo->GetTainting() == LoadTainting::CORS &&
228 (NS_FAILED(httpChannel->GetStatus(&rv)) || NS_FAILED(rv)));
229 }
230
NotifyStop(nsIRequest * aRequest,nsresult aStatus)231 void PreloaderBase::NotifyStop(nsIRequest* aRequest, nsresult aStatus) {
232 // Filter out notifications that may be arriving from the old channel before
233 // restarting this request.
234 if (!SameCOMIdentity(aRequest, mChannel)) {
235 return;
236 }
237
238 NotifyStop(aStatus);
239 }
240
NotifyStop(nsresult aStatus)241 void PreloaderBase::NotifyStop(nsresult aStatus) {
242 mOnStopStatus.emplace(aStatus);
243
244 nsTArray<nsWeakPtr> nodes = std::move(mNodes);
245
246 for (nsWeakPtr& weak : nodes) {
247 nsCOMPtr<nsINode> node = do_QueryReferent(weak);
248 if (node) {
249 NotifyNodeEvent(node);
250 }
251 }
252
253 mChannel = nullptr;
254 }
255
NotifyValidating()256 void PreloaderBase::NotifyValidating() { mOnStopStatus.reset(); }
257
NotifyValidated(nsresult aStatus)258 void PreloaderBase::NotifyValidated(nsresult aStatus) {
259 NotifyStop(nullptr, aStatus);
260 }
261
AddLinkPreloadNode(nsINode * aNode)262 void PreloaderBase::AddLinkPreloadNode(nsINode* aNode) {
263 if (mOnStopStatus) {
264 return NotifyNodeEvent(aNode);
265 }
266
267 mNodes.AppendElement(do_GetWeakReference(aNode));
268 }
269
RemoveLinkPreloadNode(nsINode * aNode)270 void PreloaderBase::RemoveLinkPreloadNode(nsINode* aNode) {
271 // Note that do_GetWeakReference returns the internal weak proxy, which is
272 // always the same, so we can use it to search the array using default
273 // comparator.
274 nsWeakPtr node = do_GetWeakReference(aNode);
275 mNodes.RemoveElement(node);
276
277 if (kCancelAndRemovePreloadOnZeroReferences && mNodes.Length() == 0 &&
278 !mIsUsed) {
279 // Keep a reference, because the following call may release us. The caller
280 // may use a WeakPtr to access this.
281 RefPtr<PreloaderBase> self(this);
282 RemoveSelf(aNode->OwnerDoc());
283
284 if (mChannel) {
285 mChannel->Cancel(NS_BINDING_ABORTED);
286 }
287 }
288 }
289
NotifyNodeEvent(nsINode * aNode)290 void PreloaderBase::NotifyNodeEvent(nsINode* aNode) {
291 PreloadService::NotifyNodeEvent(
292 aNode, mShouldFireLoadEvent || NS_SUCCEEDED(*mOnStopStatus));
293 }
294
CancelUsageTimer()295 void PreloaderBase::CancelUsageTimer() {
296 if (mUsageTimer) {
297 mUsageTimer->Cancel();
298 mUsageTimer = nullptr;
299 }
300 }
301
ReportUsageTelemetry()302 void PreloaderBase::ReportUsageTelemetry() {
303 if (mUsageTelementryReported) {
304 return;
305 }
306 mUsageTelementryReported = true;
307
308 if (mKey.As() == PreloadHashKey::ResourceType::NONE) {
309 return;
310 }
311
312 // The labels are structured as type1-used, type1-unused, type2-used, ...
313 // The first "as" resource type is NONE with value 0.
314 auto index = (static_cast<uint32_t>(mKey.As()) - 1) * 2;
315 if (!mIsUsed) {
316 ++index;
317 }
318
319 auto label = static_cast<Telemetry::LABELS_REL_PRELOAD_MISS_RATIO>(index);
320 Telemetry::AccumulateCategorical(label);
321 }
322
AsyncConsume(nsIStreamListener * aListener)323 nsresult PreloaderBase::AsyncConsume(nsIStreamListener* aListener) {
324 // We want to return an error so that consumers can't ever use a preload to
325 // consume data unless it's properly implemented.
326 return NS_ERROR_NOT_IMPLEMENTED;
327 }
328
329 // PreloaderBase::RedirectRecord
330
Spec() const331 nsCString PreloaderBase::RedirectRecord::Spec() const {
332 nsCOMPtr<nsIURI> noFragment;
333 NS_GetURIWithoutRef(mURI, getter_AddRefs(noFragment));
334 MOZ_ASSERT(noFragment);
335 return noFragment->GetSpecOrDefault();
336 }
337
Fragment() const338 nsCString PreloaderBase::RedirectRecord::Fragment() const {
339 nsCString fragment;
340 mURI->GetRef(fragment);
341 return fragment;
342 }
343
344 // PreloaderBase::UsageTimer
345
NS_IMPL_ISUPPORTS(PreloaderBase::UsageTimer,nsITimerCallback,nsINamed)346 NS_IMPL_ISUPPORTS(PreloaderBase::UsageTimer, nsITimerCallback, nsINamed)
347
348 NS_IMETHODIMP PreloaderBase::UsageTimer::Notify(nsITimer* aTimer) {
349 if (!mPreload || !mDocument) {
350 return NS_OK;
351 }
352
353 MOZ_ASSERT(aTimer == mPreload->mUsageTimer);
354 mPreload->mUsageTimer = nullptr;
355
356 if (mPreload->IsUsed()) {
357 // Left in the hashtable, but marked as used. This is a valid case, and we
358 // don't want to emit a warning for this preload then.
359 return NS_OK;
360 }
361
362 mPreload->ReportUsageTelemetry();
363
364 // PreloadHashKey overrides GetKey, we need to use the nsURIHashKey one to get
365 // the URI.
366 nsIURI* uri = static_cast<nsURIHashKey*>(&mPreload->mKey)->GetKey();
367 if (!uri) {
368 return NS_OK;
369 }
370
371 nsString spec;
372 NS_GetSanitizedURIStringFromURI(uri, spec);
373 nsContentUtils::ReportToConsole(nsIScriptError::warningFlag, "DOM"_ns,
374 mDocument, nsContentUtils::eDOM_PROPERTIES,
375 "UnusedLinkPreloadPending",
376 nsTArray<nsString>({std::move(spec)}));
377 return NS_OK;
378 }
379
380 NS_IMETHODIMP
GetName(nsACString & aName)381 PreloaderBase::UsageTimer::GetName(nsACString& aName) {
382 aName.AssignLiteral("PreloaderBase::UsageTimer");
383 return NS_OK;
384 }
385
386 } // namespace mozilla
387