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/. */
5 #include "PreloaderBase.h"
7 #include "mozilla/dom/Document.h"
8 #include "mozilla/Telemetry.h"
9 #include "nsContentUtils.h"
10 #include "nsIAsyncVerifyRedirectCallback.h"
11 #include "nsIHttpChannel.h"
12 #include "nsILoadGroup.h"
13 #include "nsIInterfaceRequestorUtils.h"
14 #include "nsIRedirectResultListener.h"
15 #include "nsNetUtil.h"
17 // Change this if we want to cancel and remove the associated preload on removal
18 // of all <link rel=preload> tags from the tree.
19 constexpr static bool kCancelAndRemovePreloadOnZeroReferences
= false;
23 PreloaderBase::UsageTimer::UsageTimer(PreloaderBase
* aPreload
,
24 dom::Document
* aDocument
)
25 : mDocument(aDocument
), mPreload(aPreload
) {}
27 class PreloaderBase::RedirectSink final
: public nsIInterfaceRequestor
,
28 public nsIChannelEventSink
,
29 public nsIRedirectResultListener
{
30 RedirectSink() = delete;
31 virtual ~RedirectSink();
34 NS_DECL_THREADSAFE_ISUPPORTS
35 NS_DECL_NSIINTERFACEREQUESTOR
36 NS_DECL_NSICHANNELEVENTSINK
37 NS_DECL_NSIREDIRECTRESULTLISTENER
39 RedirectSink(PreloaderBase
* aPreloader
, nsIInterfaceRequestor
* aCallbacks
);
42 MainThreadWeakPtr
<PreloaderBase
> mPreloader
;
43 nsCOMPtr
<nsIInterfaceRequestor
> mCallbacks
;
44 nsCOMPtr
<nsIChannel
> mRedirectChannel
;
47 PreloaderBase::RedirectSink::RedirectSink(PreloaderBase
* aPreloader
,
48 nsIInterfaceRequestor
* aCallbacks
)
49 : mPreloader(aPreloader
), mCallbacks(aCallbacks
) {}
51 PreloaderBase::RedirectSink::~RedirectSink() = default;
53 NS_IMPL_ISUPPORTS(PreloaderBase::RedirectSink
, nsIInterfaceRequestor
,
54 nsIChannelEventSink
, nsIRedirectResultListener
)
56 NS_IMETHODIMP
PreloaderBase::RedirectSink::AsyncOnChannelRedirect(
57 nsIChannel
* aOldChannel
, nsIChannel
* aNewChannel
, uint32_t aFlags
,
58 nsIAsyncVerifyRedirectCallback
* aCallback
) {
59 MOZ_DIAGNOSTIC_ASSERT(NS_IsMainThread());
61 mRedirectChannel
= aNewChannel
;
63 // Deliberately adding this before confirmation.
65 aNewChannel
->GetOriginalURI(getter_AddRefs(uri
));
67 mPreloader
->mRedirectRecords
.AppendElement(
68 RedirectRecord(aFlags
, uri
.forget()));
72 nsCOMPtr
<nsIChannelEventSink
> sink(do_GetInterface(mCallbacks
));
74 return sink
->AsyncOnChannelRedirect(aOldChannel
, aNewChannel
, aFlags
,
79 aCallback
->OnRedirectVerifyCallback(NS_OK
);
83 NS_IMETHODIMP
PreloaderBase::RedirectSink::OnRedirectResult(nsresult status
) {
84 if (NS_SUCCEEDED(status
) && mRedirectChannel
) {
85 mPreloader
->mChannel
= std::move(mRedirectChannel
);
87 mRedirectChannel
= nullptr;
91 nsCOMPtr
<nsIRedirectResultListener
> sink(do_GetInterface(mCallbacks
));
93 return sink
->OnRedirectResult(status
);
100 NS_IMETHODIMP
PreloaderBase::RedirectSink::GetInterface(const nsIID
& aIID
,
102 NS_ENSURE_ARG_POINTER(aResult
);
104 if (aIID
.Equals(NS_GET_IID(nsIChannelEventSink
)) ||
105 aIID
.Equals(NS_GET_IID(nsIRedirectResultListener
))) {
106 return QueryInterface(aIID
, aResult
);
110 return mCallbacks
->GetInterface(aIID
, aResult
);
114 return NS_ERROR_NO_INTERFACE
;
117 PreloaderBase::~PreloaderBase() { MOZ_ASSERT(NS_IsMainThread()); }
120 void PreloaderBase::AddLoadBackgroundFlag(nsIChannel
* aChannel
) {
121 nsLoadFlags loadFlags
;
122 aChannel
->GetLoadFlags(&loadFlags
);
123 aChannel
->SetLoadFlags(loadFlags
| nsIRequest::LOAD_BACKGROUND
);
126 void PreloaderBase::NotifyOpen(const PreloadHashKey
& aKey
,
127 dom::Document
* aDocument
, bool aIsPreload
,
130 DebugOnly
<bool> alreadyRegistered
=
131 aDocument
->Preloads().RegisterPreload(aKey
, this);
132 // This means there is already a preload registered under this key in this
133 // document. We only allow replacement when this is a regular load.
134 // Otherwise, this should never happen and is a suspected misuse of the API.
135 MOZ_ASSERT_IF(alreadyRegistered
, !aIsPreload
);
139 mIsUsed
= !aIsPreload
;
141 // Start usage timer for rel="preload", but not for rel="modulepreload"
142 // because modules may be loaded for functionality the user does not
143 // immediately interact with after page load (e.g. a docs search box)
144 if (!aIsModule
&& !mIsUsed
&& !mUsageTimer
) {
145 auto callback
= MakeRefPtr
<UsageTimer
>(this, aDocument
);
146 NS_NewTimerWithCallback(getter_AddRefs(mUsageTimer
), callback
, 10000,
147 nsITimer::TYPE_ONE_SHOT
);
150 ReportUsageTelemetry();
153 void PreloaderBase::NotifyOpen(const PreloadHashKey
& aKey
, nsIChannel
* aChannel
,
154 dom::Document
* aDocument
, bool aIsPreload
,
156 NotifyOpen(aKey
, aDocument
, aIsPreload
, aIsModule
);
159 nsCOMPtr
<nsIInterfaceRequestor
> callbacks
;
160 mChannel
->GetNotificationCallbacks(getter_AddRefs(callbacks
));
161 RefPtr
<RedirectSink
> sink(new RedirectSink(this, callbacks
));
162 mChannel
->SetNotificationCallbacks(sink
);
165 void PreloaderBase::NotifyUsage(dom::Document
* aDocument
,
166 LoadBackground aLoadBackground
) {
167 if (!mIsUsed
&& mChannel
&& aLoadBackground
== LoadBackground::Drop
) {
168 nsLoadFlags loadFlags
;
169 mChannel
->GetLoadFlags(&loadFlags
);
171 // Preloads are initially set the LOAD_BACKGROUND flag. When becoming
172 // regular loads by hitting its consuming tag, we need to drop that flag,
173 // which also means to re-add the request from/to it's loadgroup to reflect
175 if (loadFlags
& nsIRequest::LOAD_BACKGROUND
) {
176 nsCOMPtr
<nsILoadGroup
> loadGroup
;
177 mChannel
->GetLoadGroup(getter_AddRefs(loadGroup
));
181 mChannel
->GetStatus(&status
);
183 nsresult rv
= loadGroup
->RemoveRequest(mChannel
, nullptr, status
);
184 mChannel
->SetLoadFlags(loadFlags
& ~nsIRequest::LOAD_BACKGROUND
);
185 if (NS_SUCCEEDED(rv
)) {
186 loadGroup
->AddRequest(mChannel
, nullptr);
193 ReportUsageTelemetry();
195 if (mIsEarlyHintsPreload
) {
196 aDocument
->Preloads().SetEarlyHintUsed();
200 void PreloaderBase::RemoveSelf(dom::Document
* aDocument
) {
202 aDocument
->Preloads().DeregisterPreload(mKey
);
206 void PreloaderBase::NotifyRestart(dom::Document
* aDocument
,
207 PreloaderBase
* aNewPreloader
) {
208 RemoveSelf(aDocument
);
209 mKey
= PreloadHashKey();
214 aNewPreloader
->mNodes
= std::move(mNodes
);
218 void PreloaderBase::NotifyStart(nsIRequest
* aRequest
) {
219 // If there is no channel assigned on this preloader, we are not between
220 // channel switching, so we can freely update the mShouldFireLoadEvent using
221 // the given channel.
222 if (mChannel
&& !SameCOMIdentity(aRequest
, mChannel
)) {
226 nsCOMPtr
<nsIHttpChannel
> httpChannel
= do_QueryInterface(aRequest
);
231 // if the load is cross origin without CORS, or the CORS access is rejected,
232 // always fire load event to avoid leaking site information.
234 nsCOMPtr
<nsILoadInfo
> loadInfo
= httpChannel
->LoadInfo();
235 mShouldFireLoadEvent
=
236 loadInfo
->GetTainting() == LoadTainting::Opaque
||
237 (loadInfo
->GetTainting() == LoadTainting::CORS
&&
238 (NS_FAILED(httpChannel
->GetStatus(&rv
)) || NS_FAILED(rv
)));
241 void PreloaderBase::NotifyStop(nsIRequest
* aRequest
, nsresult aStatus
) {
242 // Filter out notifications that may be arriving from the old channel before
243 // restarting this request.
244 if (!SameCOMIdentity(aRequest
, mChannel
)) {
251 void PreloaderBase::NotifyStop(nsresult aStatus
) {
252 mOnStopStatus
.emplace(aStatus
);
254 nsTArray
<nsWeakPtr
> nodes
= std::move(mNodes
);
256 for (nsWeakPtr
& weak
: nodes
) {
257 nsCOMPtr
<nsINode
> node
= do_QueryReferent(weak
);
259 NotifyNodeEvent(node
);
266 void PreloaderBase::AddLinkPreloadNode(nsINode
* aNode
) {
268 return NotifyNodeEvent(aNode
);
271 mNodes
.AppendElement(do_GetWeakReference(aNode
));
274 void PreloaderBase::RemoveLinkPreloadNode(nsINode
* aNode
) {
275 // Note that do_GetWeakReference returns the internal weak proxy, which is
276 // always the same, so we can use it to search the array using default
278 nsWeakPtr node
= do_GetWeakReference(aNode
);
279 mNodes
.RemoveElement(node
);
281 if (kCancelAndRemovePreloadOnZeroReferences
&& mNodes
.Length() == 0 &&
283 // Keep a reference, because the following call may release us. The caller
284 // may use a WeakPtr to access this.
285 RefPtr
<PreloaderBase
> self(this);
286 RemoveSelf(aNode
->OwnerDoc());
289 mChannel
->CancelWithReason(NS_BINDING_ABORTED
,
290 "PreloaderBase::RemoveLinkPreloadNode"_ns
);
295 void PreloaderBase::NotifyNodeEvent(nsINode
* aNode
) {
296 PreloadService::NotifyNodeEvent(
297 aNode
, mShouldFireLoadEvent
|| NS_SUCCEEDED(*mOnStopStatus
));
300 void PreloaderBase::CancelUsageTimer() {
302 mUsageTimer
->Cancel();
303 mUsageTimer
= nullptr;
307 void PreloaderBase::ReportUsageTelemetry() {
308 if (mUsageTelementryReported
) {
311 mUsageTelementryReported
= true;
313 if (mKey
.As() == PreloadHashKey::ResourceType::NONE
) {
317 // The labels are structured as type1-used, type1-unused, type2-used, ...
318 // The first "as" resource type is NONE with value 0.
319 auto index
= (static_cast<uint32_t>(mKey
.As()) - 1) * 2;
324 auto label
= static_cast<Telemetry::LABELS_REL_PRELOAD_MISS_RATIO
>(index
);
325 Telemetry::AccumulateCategorical(label
);
328 nsresult
PreloaderBase::AsyncConsume(nsIStreamListener
* aListener
) {
329 // We want to return an error so that consumers can't ever use a preload to
330 // consume data unless it's properly implemented.
331 return NS_ERROR_NOT_IMPLEMENTED
;
334 // PreloaderBase::RedirectRecord
336 nsCString
PreloaderBase::RedirectRecord::Spec() const {
337 nsCOMPtr
<nsIURI
> noFragment
;
338 NS_GetURIWithoutRef(mURI
, getter_AddRefs(noFragment
));
339 MOZ_ASSERT(noFragment
);
340 return noFragment
->GetSpecOrDefault();
343 nsCString
PreloaderBase::RedirectRecord::Fragment() const {
345 mURI
->GetRef(fragment
);
349 // PreloaderBase::UsageTimer
351 NS_IMPL_ISUPPORTS(PreloaderBase::UsageTimer
, nsITimerCallback
, nsINamed
)
353 NS_IMETHODIMP
PreloaderBase::UsageTimer::Notify(nsITimer
* aTimer
) {
354 if (!mPreload
|| !mDocument
) {
358 MOZ_ASSERT(aTimer
== mPreload
->mUsageTimer
);
359 mPreload
->mUsageTimer
= nullptr;
361 if (mPreload
->IsUsed()) {
362 // Left in the hashtable, but marked as used. This is a valid case, and we
363 // don't want to emit a warning for this preload then.
367 mPreload
->ReportUsageTelemetry();
369 // PreloadHashKey overrides GetKey, we need to use the nsURIHashKey one to get
371 nsIURI
* uri
= static_cast<nsURIHashKey
*>(&mPreload
->mKey
)->GetKey();
377 NS_GetSanitizedURIStringFromURI(uri
, spec
);
378 nsContentUtils::ReportToConsole(nsIScriptError::warningFlag
, "DOM"_ns
,
379 mDocument
, nsContentUtils::eDOM_PROPERTIES
,
380 "UnusedLinkPreloadPending",
381 nsTArray
<nsString
>({std::move(spec
)}));
386 PreloaderBase::UsageTimer::GetName(nsACString
& aName
) {
387 aName
.AssignLiteral("PreloaderBase::UsageTimer");
391 } // namespace mozilla