1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 // vim:cindent:ts=2:et:sw=2:
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/Logging.h"
10 
11 #include "nsFontFaceLoader.h"
12 
13 #include "nsError.h"
14 #include "nsContentUtils.h"
15 #include "mozilla/Preferences.h"
16 #include "mozilla/Telemetry.h"
17 #include "FontFaceSet.h"
18 #include "nsPresContext.h"
19 #include "nsIPrincipal.h"
20 #include "nsIScriptSecurityManager.h"
21 #include "nsIHttpChannel.h"
22 #include "nsIContentPolicy.h"
23 #include "nsContentPolicyUtils.h"
24 
25 #include "mozilla/gfx/2D.h"
26 
27 using namespace mozilla;
28 using namespace mozilla::dom;
29 
30 #define LOG(args) MOZ_LOG(gfxUserFontSet::GetUserFontsLog(), mozilla::LogLevel::Debug, args)
31 #define LOG_ENABLED() MOZ_LOG_TEST(gfxUserFontSet::GetUserFontsLog(), \
32                                   LogLevel::Debug)
33 
34 static uint32_t
GetFallbackDelay()35 GetFallbackDelay()
36 {
37   return Preferences::GetInt("gfx.downloadable_fonts.fallback_delay", 3000);
38 }
39 
40 static uint32_t
GetShortFallbackDelay()41 GetShortFallbackDelay()
42 {
43   return Preferences::GetInt("gfx.downloadable_fonts.fallback_delay_short", 100);
44 }
45 
nsFontFaceLoader(gfxUserFontEntry * aUserFontEntry,nsIURI * aFontURI,FontFaceSet * aFontFaceSet,nsIChannel * aChannel)46 nsFontFaceLoader::nsFontFaceLoader(gfxUserFontEntry* aUserFontEntry,
47                                    nsIURI* aFontURI,
48                                    FontFaceSet* aFontFaceSet,
49                                    nsIChannel* aChannel)
50   : mUserFontEntry(aUserFontEntry),
51     mFontURI(aFontURI),
52     mFontFaceSet(aFontFaceSet),
53     mChannel(aChannel)
54 {
55   mStartTime = TimeStamp::Now();
56 }
57 
~nsFontFaceLoader()58 nsFontFaceLoader::~nsFontFaceLoader()
59 {
60   if (mUserFontEntry) {
61     mUserFontEntry->mLoader = nullptr;
62   }
63   if (mLoadTimer) {
64     mLoadTimer->Cancel();
65     mLoadTimer = nullptr;
66   }
67   if (mFontFaceSet) {
68     mFontFaceSet->RemoveLoader(this);
69   }
70 }
71 
72 void
StartedLoading(nsIStreamLoader * aStreamLoader)73 nsFontFaceLoader::StartedLoading(nsIStreamLoader* aStreamLoader)
74 {
75   int32_t loadTimeout;
76   uint8_t fontDisplay = GetFontDisplay();
77   if (fontDisplay == NS_FONT_DISPLAY_AUTO ||
78       fontDisplay == NS_FONT_DISPLAY_BLOCK) {
79     loadTimeout = GetFallbackDelay();
80   } else {
81     loadTimeout = GetShortFallbackDelay();
82   }
83 
84   if (loadTimeout > 0) {
85     mLoadTimer = do_CreateInstance("@mozilla.org/timer;1");
86     if (mLoadTimer) {
87       mLoadTimer->InitWithFuncCallback(LoadTimerCallback,
88                                        static_cast<void*>(this),
89                                        loadTimeout,
90                                        nsITimer::TYPE_ONE_SHOT);
91     }
92   } else {
93     mUserFontEntry->mFontDataLoadingState = gfxUserFontEntry::LOADING_SLOWLY;
94   }
95   mStreamLoader = aStreamLoader;
96 }
97 
98 /* static */ void
LoadTimerCallback(nsITimer * aTimer,void * aClosure)99 nsFontFaceLoader::LoadTimerCallback(nsITimer* aTimer, void* aClosure)
100 {
101   nsFontFaceLoader* loader = static_cast<nsFontFaceLoader*>(aClosure);
102 
103   if (!loader->mFontFaceSet) {
104     // We've been canceled
105     return;
106   }
107 
108   gfxUserFontEntry* ufe = loader->mUserFontEntry.get();
109   uint8_t fontDisplay = loader->GetFontDisplay();
110 
111   // Depending upon the value of the font-display descriptor for the font,
112   // their may be one or two timeouts associated with each font. The LOADING_SLOWLY
113   // state indicates that the fallback font is shown. The LOADING_TIMED_OUT
114   // state indicates that the fallback font is shown *and* the downloaded font
115   // resource will not replace the fallback font when the load completes.
116 
117   bool updateUserFontSet = true;
118   switch (fontDisplay) {
119     case NS_FONT_DISPLAY_AUTO:
120     case NS_FONT_DISPLAY_BLOCK:
121       // If the entry is loading, check whether it's >75% done; if so,
122       // we allow another timeout period before showing a fallback font.
123       if (ufe->mFontDataLoadingState == gfxUserFontEntry::LOADING_STARTED) {
124         int64_t contentLength;
125         uint32_t numBytesRead;
126         if (NS_SUCCEEDED(loader->mChannel->GetContentLength(&contentLength)) &&
127             contentLength > 0 &&
128             contentLength < UINT32_MAX &&
129             NS_SUCCEEDED(loader->mStreamLoader->GetNumBytesRead(&numBytesRead)) &&
130             numBytesRead > 3 * (uint32_t(contentLength) >> 2))
131         {
132           // More than 3/4 the data has been downloaded, so allow 50% extra
133           // time and hope the remainder will arrive before the additional
134           // time expires.
135           ufe->mFontDataLoadingState = gfxUserFontEntry::LOADING_ALMOST_DONE;
136           uint32_t delay;
137           loader->mLoadTimer->GetDelay(&delay);
138           loader->mLoadTimer->InitWithFuncCallback(LoadTimerCallback,
139                                                    static_cast<void*>(loader),
140                                                    delay >> 1,
141                                                    nsITimer::TYPE_ONE_SHOT);
142           updateUserFontSet = false;
143           LOG(("userfonts (%p) 75%% done, resetting timer\n", loader));
144         }
145       }
146       if (updateUserFontSet) {
147         ufe->mFontDataLoadingState = gfxUserFontEntry::LOADING_SLOWLY;
148       }
149       break;
150     case NS_FONT_DISPLAY_SWAP:
151       ufe->mFontDataLoadingState = gfxUserFontEntry::LOADING_SLOWLY;
152       break;
153     case NS_FONT_DISPLAY_FALLBACK: {
154       if (ufe->mFontDataLoadingState == gfxUserFontEntry::LOADING_STARTED) {
155         ufe->mFontDataLoadingState = gfxUserFontEntry::LOADING_SLOWLY;
156       } else {
157         ufe->mFontDataLoadingState = gfxUserFontEntry::LOADING_TIMED_OUT;
158         updateUserFontSet = false;
159       }
160       break;
161     }
162     case NS_FONT_DISPLAY_OPTIONAL:
163       ufe->mFontDataLoadingState = gfxUserFontEntry::LOADING_TIMED_OUT;
164       break;
165 
166     default:
167       NS_NOTREACHED("strange font-display value");
168       break;
169   }
170 
171   // If the font is not 75% loaded, or if we've already timed out once
172   // before, we mark this entry as "loading slowly", so the fallback
173   // font will be used in the meantime, and tell the context to refresh.
174   if (updateUserFontSet) {
175     nsTArray<gfxUserFontSet*> fontSets;
176     ufe->GetUserFontSets(fontSets);
177     for (gfxUserFontSet* fontSet : fontSets) {
178       nsPresContext* ctx = FontFaceSet::GetPresContextFor(fontSet);
179       if (ctx) {
180         fontSet->IncrementGeneration();
181         ctx->UserFontSetUpdated(ufe);
182         LOG(("userfonts (%p) timeout reflow for pres context %p display %d\n",
183              loader, ctx, fontDisplay));
184       }
185     }
186   }
187 }
188 
NS_IMPL_ISUPPORTS(nsFontFaceLoader,nsIStreamLoaderObserver)189 NS_IMPL_ISUPPORTS(nsFontFaceLoader, nsIStreamLoaderObserver)
190 
191 NS_IMETHODIMP
192 nsFontFaceLoader::OnStreamComplete(nsIStreamLoader* aLoader,
193                                    nsISupports* aContext,
194                                    nsresult aStatus,
195                                    uint32_t aStringLen,
196                                    const uint8_t* aString)
197 {
198   if (!mFontFaceSet) {
199     // We've been canceled
200     return aStatus;
201   }
202 
203   mFontFaceSet->RemoveLoader(this);
204 
205   TimeStamp doneTime = TimeStamp::Now();
206   TimeDuration downloadTime = doneTime - mStartTime;
207   uint32_t downloadTimeMS = uint32_t(downloadTime.ToMilliseconds());
208   Telemetry::Accumulate(Telemetry::WEBFONT_DOWNLOAD_TIME, downloadTimeMS);
209 
210   if (GetFontDisplay() == NS_FONT_DISPLAY_FALLBACK) {
211     uint32_t loadTimeout = GetFallbackDelay();
212     if (downloadTimeMS > loadTimeout &&
213         (mUserFontEntry->mFontDataLoadingState ==
214          gfxUserFontEntry::LOADING_SLOWLY)) {
215       mUserFontEntry->mFontDataLoadingState =
216         gfxUserFontEntry::LOADING_TIMED_OUT;
217     }
218   }
219 
220   if (LOG_ENABLED()) {
221     if (NS_SUCCEEDED(aStatus)) {
222       LOG(("userfonts (%p) download completed - font uri: (%s) time: %d ms\n",
223            this, mFontURI->GetSpecOrDefault().get(), downloadTimeMS));
224     } else {
225       LOG(("userfonts (%p) download failed - font uri: (%s) error: %8.8x\n",
226            this, mFontURI->GetSpecOrDefault().get(), aStatus));
227     }
228   }
229 
230   if (NS_SUCCEEDED(aStatus)) {
231     // for HTTP requests, check whether the request _actually_ succeeded;
232     // the "request status" in aStatus does not necessarily indicate this,
233     // because HTTP responses such as 404 (Not Found) will still result in
234     // a success code and potentially an HTML error page from the server
235     // as the resulting data. We don't want to use that as a font.
236     nsCOMPtr<nsIRequest> request;
237     nsCOMPtr<nsIHttpChannel> httpChannel;
238     aLoader->GetRequest(getter_AddRefs(request));
239     httpChannel = do_QueryInterface(request);
240     if (httpChannel) {
241       bool succeeded;
242       nsresult rv = httpChannel->GetRequestSucceeded(&succeeded);
243       if (NS_SUCCEEDED(rv) && !succeeded) {
244         aStatus = NS_ERROR_NOT_AVAILABLE;
245       }
246     }
247   }
248 
249   // The userFontEntry is responsible for freeing the downloaded data
250   // (aString) when finished with it; the pointer is no longer valid
251   // after FontDataDownloadComplete returns.
252   // This is called even in the case of a failed download (HTTP 404, etc),
253   // as there may still be data to be freed (e.g. an error page),
254   // and we need to load the next source.
255   bool fontUpdate =
256     mUserFontEntry->FontDataDownloadComplete(aString, aStringLen, aStatus);
257 
258   mFontFaceSet->GetUserFontSet()->RecordFontLoadDone(aStringLen, doneTime);
259 
260   // when new font loaded, need to reflow
261   if (fontUpdate) {
262     nsTArray<gfxUserFontSet*> fontSets;
263     mUserFontEntry->GetUserFontSets(fontSets);
264     for (gfxUserFontSet* fontSet : fontSets) {
265       nsPresContext* ctx = FontFaceSet::GetPresContextFor(fontSet);
266       if (ctx) {
267         // Update layout for the presence of the new font.  Since this is
268         // asynchronous, reflows will coalesce.
269         ctx->UserFontSetUpdated(mUserFontEntry);
270         LOG(("userfonts (%p) reflow for pres context %p\n", this, ctx));
271       }
272     }
273   }
274 
275   // done with font set
276   mFontFaceSet = nullptr;
277   if (mLoadTimer) {
278     mLoadTimer->Cancel();
279     mLoadTimer = nullptr;
280   }
281 
282   return NS_SUCCESS_ADOPTED_DATA;
283 }
284 
285 void
Cancel()286 nsFontFaceLoader::Cancel()
287 {
288   mUserFontEntry->mFontDataLoadingState = gfxUserFontEntry::NOT_LOADING;
289   mUserFontEntry->mLoader = nullptr;
290   mFontFaceSet = nullptr;
291   if (mLoadTimer) {
292     mLoadTimer->Cancel();
293     mLoadTimer = nullptr;
294   }
295   mChannel->Cancel(NS_BINDING_ABORTED);
296 }
297 
298 uint8_t
GetFontDisplay()299 nsFontFaceLoader::GetFontDisplay()
300 {
301   uint8_t fontDisplay = NS_FONT_DISPLAY_AUTO;
302   if (Preferences::GetBool("layout.css.font-display.enabled")) {
303     fontDisplay = mUserFontEntry->GetFontDisplay();
304   }
305   return fontDisplay;
306 }
307