Bug 1768190 - Forbid same tex unit different sampler types even if unbound. r=gfx...
[gecko.git] / toolkit / components / thumbnails / PageThumbs.jsm
blob7699e7ac0453157d261f34d5876adc93a055228f
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 file,
3  * You can obtain one at http://mozilla.org/MPL/2.0/. */
5 "use strict";
7 var EXPORTED_SYMBOLS = ["PageThumbs", "PageThumbsStorage"];
9 const PREF_STORAGE_VERSION = "browser.pagethumbnails.storage_version";
10 const LATEST_STORAGE_VERSION = 3;
12 const EXPIRATION_MIN_CHUNK_SIZE = 50;
13 const EXPIRATION_INTERVAL_SECS = 3600;
15 // If a request for a thumbnail comes in and we find one that is "stale"
16 // (or don't find one at all) we automatically queue a request to generate a
17 // new one.
18 const MAX_THUMBNAIL_AGE_SECS = 172800; // 2 days == 60*60*24*2 == 172800 secs.
20 /**
21  * Name of the directory in the profile that contains the thumbnails.
22  */
23 const THUMBNAIL_DIRECTORY = "thumbnails";
25 const { XPCOMUtils } = ChromeUtils.importESModule(
26   "resource://gre/modules/XPCOMUtils.sys.mjs"
28 const { BasePromiseWorker } = ChromeUtils.import(
29   "resource://gre/modules/PromiseWorker.jsm"
32 const lazy = {};
34 XPCOMUtils.defineLazyModuleGetters(lazy, {
35   PageThumbUtils: "resource://gre/modules/PageThumbUtils.jsm",
36   PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.jsm",
37 });
39 XPCOMUtils.defineLazyServiceGetter(
40   lazy,
41   "gUpdateTimerManager",
42   "@mozilla.org/updates/timer-manager;1",
43   "nsIUpdateTimerManager"
46 XPCOMUtils.defineLazyServiceGetter(
47   lazy,
48   "PageThumbsStorageService",
49   "@mozilla.org/thumbnails/pagethumbs-service;1",
50   "nsIPageThumbsStorageService"
53 /**
54  * Utilities for dealing with promises.
55  */
56 const TaskUtils = {
57   /**
58    * Read the bytes from a blob, asynchronously.
59    *
60    * @return {Promise}
61    * @resolve {ArrayBuffer} In case of success, the bytes contained in the blob.
62    * @reject {DOMException} In case of error, the underlying DOMException.
63    */
64   readBlob: function readBlob(blob) {
65     return new Promise((resolve, reject) => {
66       let reader = new FileReader();
67       reader.onloadend = function onloadend() {
68         if (reader.readyState != FileReader.DONE) {
69           reject(reader.error);
70         } else {
71           resolve(reader.result);
72         }
73       };
74       reader.readAsArrayBuffer(blob);
75     });
76   },
79 /**
80  * Singleton providing functionality for capturing web page thumbnails and for
81  * accessing them if already cached.
82  */
83 var PageThumbs = {
84   _initialized: false,
86   /**
87    * The calculated width and height of the thumbnails.
88    */
89   _thumbnailWidth: 0,
90   _thumbnailHeight: 0,
92   /**
93    * The scheme to use for thumbnail urls.
94    */
95   get scheme() {
96     return "moz-page-thumb";
97   },
99   /**
100    * The static host to use for thumbnail urls.
101    */
102   get staticHost() {
103     return "thumbnails";
104   },
106   /**
107    * The thumbnails' image type.
108    */
109   get contentType() {
110     return "image/png";
111   },
113   init: function PageThumbs_init() {
114     if (!this._initialized) {
115       this._initialized = true;
117       this._placesObserver = new PlacesWeakCallbackWrapper(
118         this.handlePlacesEvents.bind(this)
119       );
120       PlacesObservers.addListener(
121         ["history-cleared", "page-removed"],
122         this._placesObserver
123       );
125       // Migrate the underlying storage, if needed.
126       PageThumbsStorageMigrator.migrate();
127       PageThumbsExpiration.init();
128     }
129   },
131   handlePlacesEvents(events) {
132     for (const event of events) {
133       switch (event.type) {
134         case "history-cleared": {
135           PageThumbsStorage.wipe();
136           break;
137         }
138         case "page-removed": {
139           if (event.isRemovedFromStore) {
140             PageThumbsStorage.remove(event.url);
141           }
142           break;
143         }
144       }
145     }
146   },
148   uninit: function PageThumbs_uninit() {
149     if (this._initialized) {
150       this._initialized = false;
151     }
152   },
154   /**
155    * Gets the thumbnail image's url for a given web page's url.
156    * @param aUrl The web page's url that is depicted in the thumbnail.
157    * @return The thumbnail image's url.
158    */
159   getThumbnailURL: function PageThumbs_getThumbnailURL(aUrl) {
160     return (
161       this.scheme +
162       "://" +
163       this.staticHost +
164       "/?url=" +
165       encodeURIComponent(aUrl) +
166       "&revision=" +
167       PageThumbsStorage.getRevision(aUrl)
168     );
169   },
171   /**
172    * Gets the path of the thumbnail file for a given web page's
173    * url. This file may or may not exist depending on whether the
174    * thumbnail has been captured or not.
175    *
176    * @param aUrl The web page's url.
177    * @return The path of the thumbnail file.
178    */
179   getThumbnailPath: function PageThumbs_getThumbnailPath(aUrl) {
180     return lazy.PageThumbsStorageService.getFilePathForURL(aUrl);
181   },
183   /**
184    * Asynchronously returns a thumbnail as a blob for the given
185    * window.
186    *
187    * @param aBrowser The <browser> to capture a thumbnail from.
188    * @param aArgs See captureToCanvas for accepted arguments.
189    * @return {Promise}
190    * @resolve {Blob} The thumbnail, as a Blob.
191    */
192   captureToBlob: function PageThumbs_captureToBlob(aBrowser, aArgs) {
193     if (!this._prefEnabled()) {
194       return null;
195     }
197     return new Promise(resolve => {
198       let canvas = this.createCanvas(aBrowser.ownerGlobal);
199       this.captureToCanvas(aBrowser, canvas, aArgs)
200         .then(() => {
201           canvas.toBlob(blob => {
202             resolve(blob, this.contentType);
203           });
204         })
205         .catch(e => Cu.reportError(e));
206     });
207   },
209   /**
210    * Captures a thumbnail from a given window and draws it to the given canvas.
211    * Note, when dealing with remote content, this api draws into the passed
212    * canvas asynchronously. Pass aCallback to receive an async callback after
213    * canvas painting has completed.
214    * @param aBrowser The browser to capture a thumbnail from.
215    * @param aCanvas The canvas to draw to. The thumbnail will be scaled to match
216    *   the dimensions of this canvas. If callers pass a 0x0 canvas, the canvas
217    *   will be resized to default thumbnail dimensions just prior to painting.
218    * @param aArgs (optional) Additional named parameters:
219    *   fullScale - request that a non-downscaled image be returned.
220    *   isImage - indicate that this should be treated as an image url.
221    *   backgroundColor - background color to draw behind images.
222    *   targetWidth - desired width for images.
223    *   isBackgroundThumb - true if request is from the background thumb service.
224    *   fullViewport - request that a screenshot for the viewport be
225    *     captured. This makes it possible to get a screenshot that reflects
226    *     the current scroll position of aBrowser.
227    * @param aSkipTelemetry skip recording telemetry
228    */
229   async captureToCanvas(aBrowser, aCanvas, aArgs, aSkipTelemetry = false) {
230     let telemetryCaptureTime = new Date();
231     let args = {
232       fullScale: aArgs ? aArgs.fullScale : false,
233       isImage: aArgs ? aArgs.isImage : false,
234       backgroundColor:
235         aArgs?.backgroundColor ?? lazy.PageThumbUtils.THUMBNAIL_BG_COLOR,
236       targetWidth:
237         aArgs?.targetWidth ?? lazy.PageThumbUtils.THUMBNAIL_DEFAULT_SIZE,
238       isBackgroundThumb: aArgs ? aArgs.isBackgroundThumb : false,
239       fullViewport: aArgs?.fullViewport ?? false,
240     };
242     return this._captureToCanvas(aBrowser, aCanvas, args).then(() => {
243       if (!aSkipTelemetry) {
244         Services.telemetry
245           .getHistogramById("FX_THUMBNAILS_CAPTURE_TIME_MS")
246           .add(new Date() - telemetryCaptureTime);
247       }
248       return aCanvas;
249     });
250   },
252   /**
253    * Asynchronously check the state of aBrowser to see if it passes a set of
254    * predefined security checks. Consumers should refrain from storing
255    * thumbnails if these checks fail. Note the final result of this call is
256    * transitory as it is based on current navigation state and the type of
257    * content being displayed.
258    *
259    * @param aBrowser The target browser
260    */
261   async shouldStoreThumbnail(aBrowser) {
262     // Don't capture in private browsing mode.
263     if (lazy.PrivateBrowsingUtils.isBrowserPrivate(aBrowser)) {
264       return false;
265     }
266     if (aBrowser.isRemoteBrowser) {
267       if (aBrowser.browsingContext.currentWindowGlobal) {
268         let thumbnailsActor = aBrowser.browsingContext.currentWindowGlobal.getActor(
269           "Thumbnails"
270         );
271         return thumbnailsActor
272           .sendQuery("Browser:Thumbnail:CheckState")
273           .catch(err => {
274             return false;
275           });
276       }
277       return false;
278     }
279     return lazy.PageThumbUtils.shouldStoreContentThumbnail(
280       aBrowser.contentDocument,
281       aBrowser.docShell
282     );
283   },
285   // The background thumbnail service captures to canvas but doesn't want to
286   // participate in this service's telemetry, which is why this method exists.
287   async _captureToCanvas(aBrowser, aCanvas, aArgs) {
288     if (aBrowser.isRemoteBrowser) {
289       let thumbnail = await this._captureRemoteThumbnail(
290         aBrowser,
291         aCanvas.width,
292         aCanvas.height,
293         aArgs
294       );
296       // 'thumbnail' can be null if the browser has navigated away after starting
297       // the thumbnail request, so we check it here.
298       if (thumbnail) {
299         let ctx = thumbnail.getContext("2d");
300         let imgData = ctx.getImageData(0, 0, thumbnail.width, thumbnail.height);
301         aCanvas.width = thumbnail.width;
302         aCanvas.height = thumbnail.height;
303         aCanvas.getContext("2d").putImageData(imgData, 0, 0);
304       }
306       return aCanvas;
307     }
308     // The content is a local page, grab a thumbnail sync.
309     await lazy.PageThumbUtils.createSnapshotThumbnail(aBrowser, aCanvas, aArgs);
310     return aCanvas;
311   },
313   /**
314    * Asynchrnously render an appropriately scaled thumbnail to canvas.
315    *
316    * @param aBrowser The browser to capture a thumbnail from.
317    * @param aWidth The desired canvas width.
318    * @param aHeight The desired canvas height.
319    * @param aArgs (optional) Additional named parameters:
320    *   fullScale - request that a non-downscaled image be returned.
321    *   isImage - indicate that this should be treated as an image url.
322    *   backgroundColor - background color to draw behind images.
323    *   targetWidth - desired width for images.
324    *   isBackgroundThumb - true if request is from the background thumb service.
325    *   fullViewport - request that a screenshot for the viewport be
326    *     captured. This makes it possible to get a screenshot that reflects
327    *     the current scroll position of aBrowser.
328    * @return a promise
329    */
330   async _captureRemoteThumbnail(aBrowser, aWidth, aHeight, aArgs) {
331     if (!aBrowser.browsingContext || !aBrowser.parentElement) {
332       return null;
333     }
335     let thumbnailsActor = aBrowser.browsingContext.currentWindowGlobal.getActor(
336       aArgs.isBackgroundThumb ? "BackgroundThumbnails" : "Thumbnails"
337     );
338     let contentInfo = await thumbnailsActor.sendQuery(
339       "Browser:Thumbnail:ContentInfo",
340       {
341         isImage: aArgs.isImage,
342         targetWidth: aArgs.targetWidth,
343         backgroundColor: aArgs.backgroundColor,
344       }
345     );
347     let contentWidth = contentInfo.width;
348     let contentHeight = contentInfo.height;
349     if (contentWidth == 0 || contentHeight == 0) {
350       throw new Error("IMAGE_ZERO_DIMENSION");
351     }
353     let doc = aBrowser.parentElement.ownerDocument;
354     let thumbnail = doc.createElementNS(
355       lazy.PageThumbUtils.HTML_NAMESPACE,
356       "canvas"
357     );
359     let image;
360     if (contentInfo.imageData) {
361       thumbnail.width = contentWidth;
362       thumbnail.height = contentHeight;
364       image = new aBrowser.ownerGlobal.Image();
365       await new Promise(resolve => {
366         image.onload = resolve;
367         image.src = contentInfo.imageData;
368       });
369     } else {
370       let fullScale = aArgs ? aArgs.fullScale : false;
371       let scale = fullScale
372         ? 1
373         : Math.min(Math.max(aWidth / contentWidth, aHeight / contentHeight), 1);
375       image = await aBrowser.drawSnapshot(
376         0,
377         0,
378         contentWidth,
379         contentHeight,
380         scale,
381         aArgs.backgroundColor,
382         aArgs.fullViewport
383       );
385       thumbnail.width = fullScale ? contentWidth : aWidth;
386       thumbnail.height = fullScale ? contentHeight : aHeight;
387     }
389     thumbnail.getContext("2d").drawImage(image, 0, 0);
391     return thumbnail;
392   },
394   /**
395    * Captures a thumbnail for the given browser and stores it to the cache.
396    * @param aBrowser The browser to capture a thumbnail for.
397    */
398   captureAndStore: async function PageThumbs_captureAndStore(aBrowser) {
399     if (!this._prefEnabled()) {
400       return;
401     }
403     let url = aBrowser.currentURI.spec;
404     let originalURL;
405     let channelError = false;
407     if (!aBrowser.isRemoteBrowser) {
408       let channel = aBrowser.docShell.currentDocumentChannel;
409       originalURL = channel.originalURI.spec;
410       // see if this was an error response.
411       channelError = lazy.PageThumbUtils.isChannelErrorResponse(channel);
412     } else {
413       let thumbnailsActor = aBrowser.browsingContext.currentWindowGlobal.getActor(
414         "Thumbnails"
415       );
416       let resp = await thumbnailsActor.sendQuery(
417         "Browser:Thumbnail:GetOriginalURL"
418       );
420       originalURL = resp.originalURL || url;
421       channelError = resp.channelError;
422     }
424     try {
425       let blob = await this.captureToBlob(aBrowser);
426       let buffer = await TaskUtils.readBlob(blob);
427       await this._store(originalURL, url, buffer, channelError);
428     } catch (ex) {
429       Cu.reportError("Exception thrown during thumbnail capture: '" + ex + "'");
430     }
431   },
433   /**
434    * Checks if an existing thumbnail for the specified URL is either missing
435    * or stale, and if so, captures and stores it.  Once the thumbnail is stored,
436    * an observer service notification will be sent, so consumers should observe
437    * such notifications if they want to be notified of an updated thumbnail.
438    *
439    * @param aBrowser The content window of this browser will be captured.
440    */
441   captureAndStoreIfStale: async function PageThumbs_captureAndStoreIfStale(
442     aBrowser
443   ) {
444     if (!aBrowser.currentURI) {
445       return false;
446     }
447     let url = aBrowser.currentURI.spec;
448     let recent;
449     try {
450       recent = await PageThumbsStorage.isFileRecentForURL(url);
451     } catch {
452       return false;
453     }
454     if (
455       !recent &&
456       // Careful, the call to PageThumbsStorage is async, so the browser may
457       // have navigated away from the URL or even closed.
458       aBrowser.currentURI &&
459       aBrowser.currentURI.spec == url
460     ) {
461       await this.captureAndStore(aBrowser);
462     }
463     return true;
464   },
466   /**
467    * Stores data to disk for the given URLs.
468    *
469    * NB: The background thumbnail service calls this, too.
470    *
471    * @param aOriginalURL The URL with which the capture was initiated.
472    * @param aFinalURL The URL to which aOriginalURL ultimately resolved.
473    * @param aData An ArrayBuffer containing the image data.
474    * @param aNoOverwrite If true and files for the URLs already exist, the files
475    *                     will not be overwritten.
476    */
477   _store: async function PageThumbs__store(
478     aOriginalURL,
479     aFinalURL,
480     aData,
481     aNoOverwrite
482   ) {
483     let telemetryStoreTime = new Date();
484     await PageThumbsStorage.writeData(aFinalURL, aData, aNoOverwrite);
485     Services.telemetry
486       .getHistogramById("FX_THUMBNAILS_STORE_TIME_MS")
487       .add(new Date() - telemetryStoreTime);
489     Services.obs.notifyObservers(null, "page-thumbnail:create", aFinalURL);
490     // We've been redirected. Create a copy of the current thumbnail for
491     // the redirect source. We need to do this because:
492     //
493     // 1) Users can drag any kind of links onto the newtab page. If those
494     //    links redirect to a different URL then we want to be able to
495     //    provide thumbnails for both of them.
496     //
497     // 2) The newtab page should actually display redirect targets, only.
498     //    Because of bug 559175 this information can get lost when using
499     //    Sync and therefore also redirect sources appear on the newtab
500     //    page. We also want thumbnails for those.
501     if (aFinalURL != aOriginalURL) {
502       await PageThumbsStorage.copy(aFinalURL, aOriginalURL, aNoOverwrite);
503       Services.obs.notifyObservers(null, "page-thumbnail:create", aOriginalURL);
504     }
505   },
507   /**
508    * Register an expiration filter.
509    *
510    * When thumbnails are going to expire, each registered filter is asked for a
511    * list of thumbnails to keep.
512    *
513    * The filter (if it is a callable) or its filterForThumbnailExpiration method
514    * (if the filter is an object) is called with a single argument.  The
515    * argument is a callback function.  The filter must call the callback
516    * function and pass it an array of zero or more URLs.  (It may do so
517    * asynchronously.)  Thumbnails for those URLs will be except from expiration.
518    *
519    * @param aFilter callable, or object with filterForThumbnailExpiration method
520    */
521   addExpirationFilter: function PageThumbs_addExpirationFilter(aFilter) {
522     PageThumbsExpiration.addFilter(aFilter);
523   },
525   /**
526    * Unregister an expiration filter.
527    * @param aFilter A filter that was previously passed to addExpirationFilter.
528    */
529   removeExpirationFilter: function PageThumbs_removeExpirationFilter(aFilter) {
530     PageThumbsExpiration.removeFilter(aFilter);
531   },
533   /**
534    * Creates a new hidden canvas element.
535    * @param aWindow The document of this window will be used to create the
536    *                canvas.  If not given, the hidden window will be used.
537    * @return The newly created canvas.
538    */
539   createCanvas: function PageThumbs_createCanvas(aWindow) {
540     return lazy.PageThumbUtils.createCanvas(aWindow);
541   },
543   _prefEnabled: function PageThumbs_prefEnabled() {
544     try {
545       return !Services.prefs.getBoolPref(
546         "browser.pagethumbnails.capturing_disabled"
547       );
548     } catch (e) {
549       return true;
550     }
551   },
554 var PageThumbsStorage = {
555   ensurePath: function Storage_ensurePath() {
556     // Create the directory (ignore any error if the directory
557     // already exists). As all writes are done from the PageThumbsWorker
558     // thread, which serializes its operations, this ensures that
559     // future operations can proceed without having to check whether
560     // the directory exists.
561     return PageThumbsWorker.post("makeDir", [
562       lazy.PageThumbsStorageService.path,
563       { ignoreExisting: true },
564     ]).catch(function onError(aReason) {
565       Cu.reportError("Could not create thumbnails directory" + aReason);
566     });
567   },
569   _revisionTable: {},
571   // Generate an arbitrary revision tag, i.e. one that can't be used to
572   // infer URL frecency.
573   updateRevision(aURL) {
574     // Initialize with a random value and increment on each update. Wrap around
575     // modulo _revisionRange, so that even small values carry no meaning.
576     let rev = this._revisionTable[aURL];
577     if (rev == null) {
578       rev = Math.floor(Math.random() * this._revisionRange);
579     }
580     this._revisionTable[aURL] = (rev + 1) % this._revisionRange;
581   },
583   // If two thumbnails with the same URL and revision are in cache at the
584   // same time, the image loader may pick the stale thumbnail in some cases.
585   // Therefore _revisionRange must be large enough to prevent this, e.g.
586   // in the pathological case image.cache.size (5MB by default) could fill
587   // with (abnormally small) 10KB thumbnail images if the browser session
588   // runs long enough (though this is unlikely as thumbnails are usually
589   // only updated every MAX_THUMBNAIL_AGE_SECS).
590   _revisionRange: 8192,
592   /**
593    * Return a revision tag for the thumbnail stored for a given URL.
594    *
595    * @param aURL The URL spec string
596    * @return A revision tag for the corresponding thumbnail. Returns a changed
597    * value whenever the stored thumbnail changes.
598    */
599   getRevision(aURL) {
600     let rev = this._revisionTable[aURL];
601     if (rev == null) {
602       this.updateRevision(aURL);
603       rev = this._revisionTable[aURL];
604     }
605     return rev;
606   },
608   /**
609    * Write the contents of a thumbnail, off the main thread.
610    *
611    * @param {string} aURL The url for which to store a thumbnail.
612    * @param {ArrayBuffer} aData The data to store in the thumbnail, as
613    * an ArrayBuffer. This array buffer will be detached and cannot be
614    * reused after the copy.
615    * @param {boolean} aNoOverwrite If true and the thumbnail's file already
616    * exists, the file will not be overwritten.
617    *
618    * @return {Promise}
619    */
620   writeData: function Storage_writeData(aURL, aData, aNoOverwrite) {
621     let path = lazy.PageThumbsStorageService.getFilePathForURL(aURL);
622     this.ensurePath();
623     aData = new Uint8Array(aData);
624     let msg = [
625       path,
626       aData,
627       {
628         tmpPath: path + ".tmp",
629         mode: aNoOverwrite ? "create" : "overwrite",
630       },
631     ];
632     return PageThumbsWorker.post(
633       "writeAtomic",
634       msg,
635       msg /* we don't want that message garbage-collected,
636            as OS.Shared.Type.void_t.in_ptr.toMsg uses C-level
637            memory tricks to enforce zero-copy*/
638     ).then(
639       () => this.updateRevision(aURL),
640       this._eatNoOverwriteError(aNoOverwrite)
641     );
642   },
644   /**
645    * Copy a thumbnail, off the main thread.
646    *
647    * @param {string} aSourceURL The url of the thumbnail to copy.
648    * @param {string} aTargetURL The url of the target thumbnail.
649    * @param {boolean} aNoOverwrite If true and the target file already exists,
650    * the file will not be overwritten.
651    *
652    * @return {Promise}
653    */
654   copy: function Storage_copy(aSourceURL, aTargetURL, aNoOverwrite) {
655     this.ensurePath();
656     let sourceFile = lazy.PageThumbsStorageService.getFilePathForURL(
657       aSourceURL
658     );
659     let targetFile = lazy.PageThumbsStorageService.getFilePathForURL(
660       aTargetURL
661     );
662     let options = { noOverwrite: aNoOverwrite };
663     return PageThumbsWorker.post("copy", [
664       sourceFile,
665       targetFile,
666       options,
667     ]).then(
668       () => this.updateRevision(aTargetURL),
669       this._eatNoOverwriteError(aNoOverwrite)
670     );
671   },
673   /**
674    * Remove a single thumbnail, off the main thread.
675    *
676    * @return {Promise}
677    */
678   remove: function Storage_remove(aURL) {
679     return PageThumbsWorker.post("remove", [
680       lazy.PageThumbsStorageService.getFilePathForURL(aURL),
681     ]);
682   },
684   /**
685    * Remove all thumbnails, off the main thread.
686    *
687    * @return {Promise}
688    */
689   wipe: async function Storage_wipe() {
690     //
691     // This operation may be launched during shutdown, so we need to
692     // take a few precautions to ensure that:
693     //
694     // 1. it is not interrupted by shutdown, in which case we
695     //    could be leaving privacy-sensitive files on disk;
696     // 2. it is not launched too late during shutdown, in which
697     //    case this could cause shutdown freezes (see bug 1005487,
698     //    which will eventually be fixed by bug 965309)
699     //
701     let blocker = () => undefined;
703     // The following operation will rise an error if we have already
704     // reached profileBeforeChange, in which case it is too late
705     // to clear the thumbnail wipe.
706     IOUtils.profileBeforeChange.addBlocker(
707       "PageThumbs: removing all thumbnails",
708       blocker
709     );
711     // Start the work only now that `profileBeforeChange` has had
712     // a chance to throw an error.
714     let promise = PageThumbsWorker.post("wipe", [
715       lazy.PageThumbsStorageService.path,
716     ]);
717     try {
718       await promise;
719     } finally {
720       // Generally, we will be done much before profileBeforeChange,
721       // so let's not hoard blockers.
722       IOUtils.profileBeforeChange.removeBlocker(blocker);
723     }
724   },
726   fileExistsForURL: function Storage_fileExistsForURL(aURL) {
727     return PageThumbsWorker.post("exists", [
728       lazy.PageThumbsStorageService.getFilePathForURL(aURL),
729     ]);
730   },
732   isFileRecentForURL: function Storage_isFileRecentForURL(aURL) {
733     return PageThumbsWorker.post("isFileRecent", [
734       lazy.PageThumbsStorageService.getFilePathForURL(aURL),
735       MAX_THUMBNAIL_AGE_SECS,
736     ]);
737   },
739   /**
740    * For functions that take a noOverwrite option, IOUtils throws an error if
741    * the target file exists and noOverwrite is true.  We don't consider that an
742    * error, and we don't want such errors propagated.
743    *
744    * @param {aNoOverwrite} The noOverwrite option used in the IOUtils operation.
745    *
746    * @return {function} A function that should be passed as the second argument
747    * to then() (the `onError` argument).
748    */
749   _eatNoOverwriteError: function Storage__eatNoOverwriteError(aNoOverwrite) {
750     return function onError(err) {
751       if (
752         !aNoOverwrite ||
753         !DOMException.isInstance(err) ||
754         err.name !== "TypeMismatchError"
755       ) {
756         throw err;
757       }
758     };
759   },
762 var PageThumbsStorageMigrator = {
763   get currentVersion() {
764     try {
765       return Services.prefs.getIntPref(PREF_STORAGE_VERSION);
766     } catch (e) {
767       // The pref doesn't exist, yet. Return version 0.
768       return 0;
769     }
770   },
772   set currentVersion(aVersion) {
773     Services.prefs.setIntPref(PREF_STORAGE_VERSION, aVersion);
774   },
776   migrate: function Migrator_migrate() {
777     let version = this.currentVersion;
779     // Storage version 1 never made it to beta.
780     // At the time of writing only Windows had (ProfD != ProfLD) and we
781     // needed to move thumbnails from the roaming profile to the locale
782     // one so that they're not needlessly included in backups and/or
783     // written via SMB.
785     // Storage version 2 also never made it to beta.
786     // The thumbnail folder structure has been changed and old thumbnails
787     // were not migrated. Instead, we just renamed the current folder to
788     // "<name>-old" and will remove it later.
790     if (version < 3) {
791       this.migrateToVersion3();
792     }
794     this.currentVersion = LATEST_STORAGE_VERSION;
795   },
797   /**
798    * Bug 239254 added support for having the disk cache and thumbnail
799    * directories on a local path (i.e. ~/.cache/) under Linux. We'll first
800    * try to move the old thumbnails to their new location. If that's not
801    * possible (because ProfD might be on a different file system than
802    * ProfLD) we'll just discard them.
803    *
804    * @param {string*} local The path to the local profile directory.
805    * Used for testing. Default argument is good for all non-testing uses.
806    * @param {string*} roaming The path to the roaming profile directory.
807    * Used for testing. Default argument is good for all non-testing uses.
808    */
809   migrateToVersion3: function Migrator_migrateToVersion3(
810     local = Services.dirsvc.get("ProfLD", Ci.nsIFile).path,
811     roaming = Services.dirsvc.get("ProfD", Ci.nsIFile).path
812   ) {
813     PageThumbsWorker.post("moveOrDeleteAllThumbnails", [
814       PathUtils.join(roaming, THUMBNAIL_DIRECTORY),
815       PathUtils.join(local, THUMBNAIL_DIRECTORY),
816     ]);
817   },
820 var PageThumbsExpiration = {
821   _filters: [],
823   init: function Expiration_init() {
824     lazy.gUpdateTimerManager.registerTimer(
825       "browser-cleanup-thumbnails",
826       this,
827       EXPIRATION_INTERVAL_SECS
828     );
829   },
831   addFilter: function Expiration_addFilter(aFilter) {
832     this._filters.push(aFilter);
833   },
835   removeFilter: function Expiration_removeFilter(aFilter) {
836     let index = this._filters.indexOf(aFilter);
837     if (index > -1) {
838       this._filters.splice(index, 1);
839     }
840   },
842   notify: function Expiration_notify(aTimer) {
843     let urls = [];
844     let filtersToWaitFor = this._filters.length;
846     let expire = () => {
847       this.expireThumbnails(urls);
848     };
850     // No registered filters.
851     if (!filtersToWaitFor) {
852       expire();
853       return;
854     }
856     function filterCallback(aURLs) {
857       urls = urls.concat(aURLs);
858       if (--filtersToWaitFor == 0) {
859         expire();
860       }
861     }
863     for (let filter of this._filters) {
864       if (typeof filter == "function") {
865         filter(filterCallback);
866       } else {
867         filter.filterForThumbnailExpiration(filterCallback);
868       }
869     }
870   },
872   expireThumbnails: function Expiration_expireThumbnails(aURLsToKeep) {
873     let keep = aURLsToKeep.map(url =>
874       lazy.PageThumbsStorageService.getLeafNameForURL(url)
875     );
876     let msg = [
877       lazy.PageThumbsStorageService.path,
878       keep,
879       EXPIRATION_MIN_CHUNK_SIZE,
880     ];
882     return PageThumbsWorker.post("expireFilesInDirectory", msg);
883   },
887  * Interface to a dedicated thread handling I/O
888  */
889 var PageThumbsWorker = new BasePromiseWorker(
890   "resource://gre/modules/PageThumbsWorker.js"