1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4  * License, v. 2.0. If a copy of the MPL was not distributed with this
5  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 
7 /* code for loading in @font-face defined font data */
8 
9 #include "mozilla/IntegerPrintfMacros.h"
10 #include "mozilla/Logging.h"
11 
12 #include "nsFontFaceLoader.h"
13 
14 #include "nsError.h"
15 #include "mozilla/AutoRestore.h"
16 #include "mozilla/Preferences.h"
17 #include "mozilla/StaticPrefs_layout.h"
18 #include "mozilla/Telemetry.h"
19 #include "mozilla/Unused.h"
20 #include "FontFaceSet.h"
21 #include "nsPresContext.h"
22 #include "nsIHttpChannel.h"
23 #include "nsIThreadRetargetableRequest.h"
24 #include "nsContentPolicyUtils.h"
25 #include "nsNetCID.h"
26 
27 #include "mozilla/gfx/2D.h"
28 
29 using namespace mozilla;
30 using namespace mozilla::dom;
31 
32 #define LOG(args) \
33   MOZ_LOG(gfxUserFontSet::GetUserFontsLog(), mozilla::LogLevel::Debug, args)
34 #define LOG_ENABLED() \
35   MOZ_LOG_TEST(gfxUserFontSet::GetUserFontsLog(), LogLevel::Debug)
36 
GetFallbackDelay()37 static uint32_t GetFallbackDelay() {
38   return Preferences::GetInt("gfx.downloadable_fonts.fallback_delay", 3000);
39 }
40 
GetShortFallbackDelay()41 static uint32_t GetShortFallbackDelay() {
42   return Preferences::GetInt("gfx.downloadable_fonts.fallback_delay_short",
43                              100);
44 }
45 
nsFontFaceLoader(gfxUserFontEntry * aUserFontEntry,uint32_t aSrcIndex,FontFaceSet * aFontFaceSet,nsIChannel * aChannel)46 nsFontFaceLoader::nsFontFaceLoader(gfxUserFontEntry* aUserFontEntry,
47                                    uint32_t aSrcIndex,
48                                    FontFaceSet* aFontFaceSet,
49                                    nsIChannel* aChannel)
50     : mUserFontEntry(aUserFontEntry),
51       mFontFaceSet(aFontFaceSet),
52       mChannel(aChannel),
53       mStreamLoader(nullptr),
54       mSrcIndex(aSrcIndex) {
55   MOZ_ASSERT(mFontFaceSet,
56              "We should get a valid FontFaceSet from the caller!");
57 
58   const gfxFontFaceSrc& src = aUserFontEntry->SourceAt(mSrcIndex);
59   MOZ_ASSERT(src.mSourceType == gfxFontFaceSrc::eSourceType_URL);
60 
61   mFontURI = src.mURI->get();
62   mStartTime = TimeStamp::Now();
63 
64   // We add an explicit load block rather than just rely on the network
65   // request's block, since we need to do some OMT work after the load
66   // is finished before we unblock load.
67   mFontFaceSet->Document()->BlockOnload();
68 }
69 
~nsFontFaceLoader()70 nsFontFaceLoader::~nsFontFaceLoader() {
71   MOZ_DIAGNOSTIC_ASSERT(!mInLoadTimerCallback);
72   MOZ_DIAGNOSTIC_ASSERT(!mInStreamComplete);
73   if (mUserFontEntry) {
74     mUserFontEntry->mLoader = nullptr;
75   }
76   if (mLoadTimer) {
77     mLoadTimer->Cancel();
78     mLoadTimer = nullptr;
79   }
80   if (mFontFaceSet) {
81     mFontFaceSet->RemoveLoader(this);
82     mFontFaceSet->Document()->UnblockOnload(false);
83   }
84 }
85 
StartedLoading(nsIStreamLoader * aStreamLoader)86 void nsFontFaceLoader::StartedLoading(nsIStreamLoader* aStreamLoader) {
87   int32_t loadTimeout;
88   StyleFontDisplay fontDisplay = GetFontDisplay();
89   if (fontDisplay == StyleFontDisplay::Auto ||
90       fontDisplay == StyleFontDisplay::Block) {
91     loadTimeout = GetFallbackDelay();
92   } else {
93     loadTimeout = GetShortFallbackDelay();
94   }
95 
96   if (loadTimeout > 0) {
97     NS_NewTimerWithFuncCallback(
98         getter_AddRefs(mLoadTimer), LoadTimerCallback, static_cast<void*>(this),
99         loadTimeout, nsITimer::TYPE_ONE_SHOT, "LoadTimerCallback",
100         mFontFaceSet->Document()->EventTargetFor(TaskCategory::Other));
101   } else {
102     mUserFontEntry->mFontDataLoadingState = gfxUserFontEntry::LOADING_SLOWLY;
103   }
104   mStreamLoader = aStreamLoader;
105 }
106 
107 /* static */
LoadTimerCallback(nsITimer * aTimer,void * aClosure)108 void nsFontFaceLoader::LoadTimerCallback(nsITimer* aTimer, void* aClosure) {
109   nsFontFaceLoader* loader = static_cast<nsFontFaceLoader*>(aClosure);
110 
111   MOZ_DIAGNOSTIC_ASSERT(!loader->mInLoadTimerCallback);
112   MOZ_DIAGNOSTIC_ASSERT(!loader->mInStreamComplete);
113   AutoRestore<bool> scope{loader->mInLoadTimerCallback};
114   loader->mInLoadTimerCallback = true;
115 
116   if (!loader->mFontFaceSet) {
117     // We've been canceled
118     return;
119   }
120 
121   gfxUserFontEntry* ufe = loader->mUserFontEntry.get();
122   StyleFontDisplay fontDisplay = loader->GetFontDisplay();
123 
124   // Depending upon the value of the font-display descriptor for the font,
125   // their may be one or two timeouts associated with each font. The
126   // LOADING_SLOWLY state indicates that the fallback font is shown. The
127   // LOADING_TIMED_OUT state indicates that the fallback font is shown *and* the
128   // downloaded font resource will not replace the fallback font when the load
129   // completes.
130 
131   bool updateUserFontSet = true;
132   switch (fontDisplay) {
133     case StyleFontDisplay::Auto:
134     case StyleFontDisplay::Block:
135       // If the entry is loading, check whether it's >75% done; if so,
136       // we allow another timeout period before showing a fallback font.
137       if (ufe->mFontDataLoadingState == gfxUserFontEntry::LOADING_STARTED) {
138         int64_t contentLength;
139         uint32_t numBytesRead;
140         if (NS_SUCCEEDED(loader->mChannel->GetContentLength(&contentLength)) &&
141             contentLength > 0 && contentLength < UINT32_MAX &&
142             NS_SUCCEEDED(
143                 loader->mStreamLoader->GetNumBytesRead(&numBytesRead)) &&
144             numBytesRead > 3 * (uint32_t(contentLength) >> 2)) {
145           // More than 3/4 the data has been downloaded, so allow 50% extra
146           // time and hope the remainder will arrive before the additional
147           // time expires.
148           ufe->mFontDataLoadingState = gfxUserFontEntry::LOADING_ALMOST_DONE;
149           uint32_t delay;
150           loader->mLoadTimer->GetDelay(&delay);
151           loader->mLoadTimer->InitWithNamedFuncCallback(
152               LoadTimerCallback, static_cast<void*>(loader), delay >> 1,
153               nsITimer::TYPE_ONE_SHOT, "nsFontFaceLoader::LoadTimerCallback");
154           updateUserFontSet = false;
155           LOG(("userfonts (%p) 75%% done, resetting timer\n", loader));
156         }
157       }
158       if (updateUserFontSet) {
159         ufe->mFontDataLoadingState = gfxUserFontEntry::LOADING_SLOWLY;
160       }
161       break;
162     case StyleFontDisplay::Swap:
163       ufe->mFontDataLoadingState = gfxUserFontEntry::LOADING_SLOWLY;
164       break;
165     case StyleFontDisplay::Fallback: {
166       if (ufe->mFontDataLoadingState == gfxUserFontEntry::LOADING_STARTED) {
167         ufe->mFontDataLoadingState = gfxUserFontEntry::LOADING_SLOWLY;
168       } else {
169         ufe->mFontDataLoadingState = gfxUserFontEntry::LOADING_TIMED_OUT;
170         updateUserFontSet = false;
171       }
172       break;
173     }
174     case StyleFontDisplay::Optional:
175       ufe->mFontDataLoadingState = gfxUserFontEntry::LOADING_TIMED_OUT;
176       break;
177 
178     default:
179       MOZ_ASSERT_UNREACHABLE("strange font-display value");
180       break;
181   }
182 
183   // If the font is not 75% loaded, or if we've already timed out once
184   // before, we mark this entry as "loading slowly", so the fallback
185   // font will be used in the meantime, and tell the context to refresh.
186   if (updateUserFontSet) {
187     nsTArray<gfxUserFontSet*> fontSets;
188     ufe->GetUserFontSets(fontSets);
189     for (gfxUserFontSet* fontSet : fontSets) {
190       nsPresContext* ctx = FontFaceSet::GetPresContextFor(fontSet);
191       if (ctx) {
192         fontSet->IncrementGeneration();
193         ctx->UserFontSetUpdated(ufe);
194         LOG(("userfonts (%p) timeout reflow for pres context %p display %d\n",
195              loader, ctx, static_cast<int>(fontDisplay)));
196       }
197     }
198   }
199 }
200 
NS_IMPL_ISUPPORTS(nsFontFaceLoader,nsIStreamLoaderObserver,nsIRequestObserver)201 NS_IMPL_ISUPPORTS(nsFontFaceLoader, nsIStreamLoaderObserver, nsIRequestObserver)
202 
203 // nsIStreamLoaderObserver
204 NS_IMETHODIMP
205 nsFontFaceLoader::OnStreamComplete(nsIStreamLoader* aLoader,
206                                    nsISupports* aContext, nsresult aStatus,
207                                    uint32_t aStringLen,
208                                    const uint8_t* aString) {
209   MOZ_ASSERT(NS_IsMainThread());
210   MOZ_DIAGNOSTIC_ASSERT(!mInLoadTimerCallback);
211   MOZ_DIAGNOSTIC_ASSERT(!mInStreamComplete);
212 
213   AutoRestore<bool> scope{mInStreamComplete};
214   mInStreamComplete = true;
215 
216   DropChannel();
217 
218   if (mLoadTimer) {
219     mLoadTimer->Cancel();
220     mLoadTimer = nullptr;
221   }
222 
223   if (!mFontFaceSet) {
224     // We've been canceled
225     return aStatus;
226   }
227 
228   TimeStamp doneTime = TimeStamp::Now();
229   TimeDuration downloadTime = doneTime - mStartTime;
230   uint32_t downloadTimeMS = uint32_t(downloadTime.ToMilliseconds());
231   Telemetry::Accumulate(Telemetry::WEBFONT_DOWNLOAD_TIME, downloadTimeMS);
232 
233   if (GetFontDisplay() == StyleFontDisplay::Fallback) {
234     uint32_t loadTimeout = GetFallbackDelay();
235     if (downloadTimeMS > loadTimeout &&
236         (mUserFontEntry->mFontDataLoadingState ==
237          gfxUserFontEntry::LOADING_SLOWLY)) {
238       mUserFontEntry->mFontDataLoadingState =
239           gfxUserFontEntry::LOADING_TIMED_OUT;
240     }
241   }
242 
243   if (LOG_ENABLED()) {
244     if (NS_SUCCEEDED(aStatus)) {
245       LOG(("userfonts (%p) download completed - font uri: (%s) time: %d ms\n",
246            this, mFontURI->GetSpecOrDefault().get(), downloadTimeMS));
247     } else {
248       LOG(("userfonts (%p) download failed - font uri: (%s) error: %8.8" PRIx32
249            "\n",
250            this, mFontURI->GetSpecOrDefault().get(),
251            static_cast<uint32_t>(aStatus)));
252     }
253   }
254 
255   if (NS_SUCCEEDED(aStatus)) {
256     // for HTTP requests, check whether the request _actually_ succeeded;
257     // the "request status" in aStatus does not necessarily indicate this,
258     // because HTTP responses such as 404 (Not Found) will still result in
259     // a success code and potentially an HTML error page from the server
260     // as the resulting data. We don't want to use that as a font.
261     nsCOMPtr<nsIRequest> request;
262     nsCOMPtr<nsIHttpChannel> httpChannel;
263     aLoader->GetRequest(getter_AddRefs(request));
264     httpChannel = do_QueryInterface(request);
265     if (httpChannel) {
266       bool succeeded;
267       nsresult rv = httpChannel->GetRequestSucceeded(&succeeded);
268       if (NS_SUCCEEDED(rv) && !succeeded) {
269         aStatus = NS_ERROR_NOT_AVAILABLE;
270       }
271     }
272   }
273 
274   mFontFaceSet->GetUserFontSet()->RecordFontLoadDone(aStringLen, doneTime);
275 
276   // The userFontEntry is responsible for freeing the downloaded data
277   // (aString) when finished with it; the pointer is no longer valid
278   // after FontDataDownloadComplete returns.
279   // This is called even in the case of a failed download (HTTP 404, etc),
280   // as there may still be data to be freed (e.g. an error page),
281   // and we need to load the next source.
282 
283   // FontDataDownloadComplete will load the platform font on a worker thread,
284   // and will call FontLoadComplete when it has finished its work.
285   mUserFontEntry->FontDataDownloadComplete(mSrcIndex, aString, aStringLen,
286                                            aStatus, this);
287   return NS_SUCCESS_ADOPTED_DATA;
288 }
289 
FontLoadComplete()290 nsresult nsFontFaceLoader::FontLoadComplete() {
291   MOZ_ASSERT(NS_IsMainThread());
292 
293   if (!mFontFaceSet) {
294     // We've been canceled
295     return NS_OK;
296   }
297 
298   // when new font loaded, need to reflow
299   nsTArray<gfxUserFontSet*> fontSets;
300   mUserFontEntry->GetUserFontSets(fontSets);
301   for (gfxUserFontSet* fontSet : fontSets) {
302     nsPresContext* ctx = FontFaceSet::GetPresContextFor(fontSet);
303     if (ctx) {
304       // Update layout for the presence of the new font.  Since this is
305       // asynchronous, reflows will coalesce.
306       ctx->UserFontSetUpdated(mUserFontEntry);
307       LOG(("userfonts (%p) reflow for pres context %p\n", this, ctx));
308     }
309   }
310 
311   MOZ_DIAGNOSTIC_ASSERT(mFontFaceSet);
312   mFontFaceSet->RemoveLoader(this);
313   mFontFaceSet->Document()->UnblockOnload(false);
314   mFontFaceSet = nullptr;
315 
316   return NS_OK;
317 }
318 
319 // nsIRequestObserver
320 NS_IMETHODIMP
OnStartRequest(nsIRequest * aRequest)321 nsFontFaceLoader::OnStartRequest(nsIRequest* aRequest) {
322   MOZ_ASSERT(NS_IsMainThread());
323 
324   nsCOMPtr<nsIThreadRetargetableRequest> req = do_QueryInterface(aRequest);
325   if (req) {
326     nsCOMPtr<nsIEventTarget> sts =
327         do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID);
328     Unused << NS_WARN_IF(NS_FAILED(req->RetargetDeliveryTo(sts)));
329   }
330   return NS_OK;
331 }
332 
333 NS_IMETHODIMP
OnStopRequest(nsIRequest * aRequest,nsresult aStatusCode)334 nsFontFaceLoader::OnStopRequest(nsIRequest* aRequest, nsresult aStatusCode) {
335   MOZ_ASSERT(NS_IsMainThread());
336   DropChannel();
337   return NS_OK;
338 }
339 
Cancel()340 void nsFontFaceLoader::Cancel() {
341   MOZ_DIAGNOSTIC_ASSERT(!mInLoadTimerCallback);
342   MOZ_DIAGNOSTIC_ASSERT(!mInStreamComplete);
343   MOZ_DIAGNOSTIC_ASSERT(mFontFaceSet);
344 
345   mUserFontEntry->LoadCanceled();
346   mUserFontEntry = nullptr;
347   mFontFaceSet->Document()->UnblockOnload(false);
348   mFontFaceSet = nullptr;
349   if (mLoadTimer) {
350     mLoadTimer->Cancel();
351     mLoadTimer = nullptr;
352   }
353   if (nsCOMPtr<nsIChannel> channel = std::move(mChannel)) {
354     channel->Cancel(NS_BINDING_ABORTED);
355   }
356 }
357 
GetFontDisplay()358 StyleFontDisplay nsFontFaceLoader::GetFontDisplay() {
359   if (!StaticPrefs::layout_css_font_display_enabled()) {
360     return StyleFontDisplay::Auto;
361   }
362   return mUserFontEntry->GetFontDisplay();
363 }
364 
365 #undef LOG
366 #undef LOG_ENABLED
367