toolkit/components/thumbnails/PageThumbs.jsm

Sat, 03 Jan 2015 20:18:00 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Sat, 03 Jan 2015 20:18:00 +0100
branch
TOR_BUG_3246
changeset 7
129ffea94266
permissions
-rw-r--r--

Conditionally enable double key logic according to:
private browsing mode or privacy.thirdparty.isolate preference and
implement in GetCookieStringCommon and FindCookie where it counts...
With some reservations of how to convince FindCookie users to test
condition and pass a nullptr when disabling double key logic.

     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 this.EXPORTED_SYMBOLS = ["PageThumbs", "PageThumbsStorage"];
     9 const Cu = Components.utils;
    10 const Cc = Components.classes;
    11 const Ci = Components.interfaces;
    13 const HTML_NAMESPACE = "http://www.w3.org/1999/xhtml";
    14 const PREF_STORAGE_VERSION = "browser.pagethumbnails.storage_version";
    15 const LATEST_STORAGE_VERSION = 3;
    17 const EXPIRATION_MIN_CHUNK_SIZE = 50;
    18 const EXPIRATION_INTERVAL_SECS = 3600;
    20 // If a request for a thumbnail comes in and we find one that is "stale"
    21 // (or don't find one at all) we automatically queue a request to generate a
    22 // new one.
    23 const MAX_THUMBNAIL_AGE_SECS = 172800; // 2 days == 60*60*24*2 == 172800 secs.
    25 /**
    26  * Name of the directory in the profile that contains the thumbnails.
    27  */
    28 const THUMBNAIL_DIRECTORY = "thumbnails";
    30 /**
    31  * The default background color for page thumbnails.
    32  */
    33 const THUMBNAIL_BG_COLOR = "#fff";
    35 Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
    36 Cu.import("resource://gre/modules/osfile/_PromiseWorker.jsm", this);
    37 Cu.import("resource://gre/modules/Promise.jsm", this);
    38 Cu.import("resource://gre/modules/osfile.jsm", this);
    40 XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
    41   "resource://gre/modules/NetUtil.jsm");
    43 XPCOMUtils.defineLazyModuleGetter(this, "Services",
    44   "resource://gre/modules/Services.jsm");
    46 XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
    47   "resource://gre/modules/FileUtils.jsm");
    49 XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
    50   "resource://gre/modules/PlacesUtils.jsm");
    52 XPCOMUtils.defineLazyServiceGetter(this, "gUpdateTimerManager",
    53   "@mozilla.org/updates/timer-manager;1", "nsIUpdateTimerManager");
    55 XPCOMUtils.defineLazyGetter(this, "gCryptoHash", function () {
    56   return Cc["@mozilla.org/security/hash;1"].createInstance(Ci.nsICryptoHash);
    57 });
    59 XPCOMUtils.defineLazyGetter(this, "gUnicodeConverter", function () {
    60   let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
    61                     .createInstance(Ci.nsIScriptableUnicodeConverter);
    62   converter.charset = 'utf8';
    63   return converter;
    64 });
    66 XPCOMUtils.defineLazyModuleGetter(this, "Task",
    67   "resource://gre/modules/Task.jsm");
    68 XPCOMUtils.defineLazyModuleGetter(this, "Deprecated",
    69   "resource://gre/modules/Deprecated.jsm");
    70 XPCOMUtils.defineLazyModuleGetter(this, "AsyncShutdown",
    71   "resource://gre/modules/AsyncShutdown.jsm");
    73 /**
    74  * Utilities for dealing with promises and Task.jsm
    75  */
    76 const TaskUtils = {
    77   /**
    78    * Add logging to a promise.
    79    *
    80    * @param {Promise} promise
    81    * @return {Promise} A promise behaving as |promise|, but with additional
    82    * logging in case of uncaught error.
    83    */
    84   captureErrors: function captureErrors(promise) {
    85     return promise.then(
    86       null,
    87       function onError(reason) {
    88         Cu.reportError("Uncaught asynchronous error: " + reason + " at\n"
    89           + reason.stack + "\n");
    90         throw reason;
    91       }
    92     );
    93   },
    95   /**
    96    * Spawn a new Task from a generator.
    97    *
    98    * This function behaves as |Task.spawn|, with the exception that it
    99    * adds logging in case of uncaught error. For more information, see
   100    * the documentation of |Task.jsm|.
   101    *
   102    * @param {generator} gen Some generator.
   103    * @return {Promise} A promise built from |gen|, with the same semantics
   104    * as |Task.spawn(gen)|.
   105    */
   106   spawn: function spawn(gen) {
   107     return this.captureErrors(Task.spawn(gen));
   108   },
   109   /**
   110    * Read the bytes from a blob, asynchronously.
   111    *
   112    * @return {Promise}
   113    * @resolve {ArrayBuffer} In case of success, the bytes contained in the blob.
   114    * @reject {DOMError} In case of error, the underlying DOMError.
   115    */
   116   readBlob: function readBlob(blob) {
   117     let deferred = Promise.defer();
   118     let reader = Cc["@mozilla.org/files/filereader;1"].createInstance(Ci.nsIDOMFileReader);
   119     reader.onloadend = function onloadend() {
   120       if (reader.readyState != Ci.nsIDOMFileReader.DONE) {
   121         deferred.reject(reader.error);
   122       } else {
   123         deferred.resolve(reader.result);
   124       }
   125     };
   126     reader.readAsArrayBuffer(blob);
   127     return deferred.promise;
   128   }
   129 };
   134 /**
   135  * Singleton providing functionality for capturing web page thumbnails and for
   136  * accessing them if already cached.
   137  */
   138 this.PageThumbs = {
   139   _initialized: false,
   141   /**
   142    * The calculated width and height of the thumbnails.
   143    */
   144   _thumbnailWidth : 0,
   145   _thumbnailHeight : 0,
   147   /**
   148    * The scheme to use for thumbnail urls.
   149    */
   150   get scheme() "moz-page-thumb",
   152   /**
   153    * The static host to use for thumbnail urls.
   154    */
   155   get staticHost() "thumbnail",
   157   /**
   158    * The thumbnails' image type.
   159    */
   160   get contentType() "image/png",
   162   init: function PageThumbs_init() {
   163     if (!this._initialized) {
   164       this._initialized = true;
   165       PlacesUtils.history.addObserver(PageThumbsHistoryObserver, false);
   167       // Migrate the underlying storage, if needed.
   168       PageThumbsStorageMigrator.migrate();
   169       PageThumbsExpiration.init();
   170     }
   171   },
   173   uninit: function PageThumbs_uninit() {
   174     if (this._initialized) {
   175       this._initialized = false;
   176       PlacesUtils.history.removeObserver(PageThumbsHistoryObserver);
   177     }
   178   },
   180   /**
   181    * Gets the thumbnail image's url for a given web page's url.
   182    * @param aUrl The web page's url that is depicted in the thumbnail.
   183    * @return The thumbnail image's url.
   184    */
   185   getThumbnailURL: function PageThumbs_getThumbnailURL(aUrl) {
   186     return this.scheme + "://" + this.staticHost +
   187            "?url=" + encodeURIComponent(aUrl);
   188   },
   190    /**
   191     * Gets the path of the thumbnail file for a given web page's
   192     * url. This file may or may not exist depending on whether the
   193     * thumbnail has been captured or not.
   194     *
   195     * @param aUrl The web page's url.
   196     * @return The path of the thumbnail file.
   197     */
   198    getThumbnailPath: function PageThumbs_getThumbnailPath(aUrl) {
   199      return PageThumbsStorage.getFilePathForURL(aUrl);
   200    },
   202   /**
   203    * Captures a thumbnail for the given window.
   204    * @param aWindow The DOM window to capture a thumbnail from.
   205    * @param aCallback The function to be called when the thumbnail has been
   206    *                  captured. The first argument will be the data stream
   207    *                  containing the image data.
   208    */
   209   capture: function PageThumbs_capture(aWindow, aCallback) {
   210     if (!this._prefEnabled()) {
   211       return;
   212     }
   214     let canvas = this._createCanvas();
   215     this.captureToCanvas(aWindow, canvas);
   217     // Fetch the canvas data on the next event loop tick so that we allow
   218     // some event processing in between drawing to the canvas and encoding
   219     // its data. We want to block the UI as short as possible. See bug 744100.
   220     Services.tm.currentThread.dispatch(function () {
   221       canvas.mozFetchAsStream(aCallback, this.contentType);
   222     }.bind(this), Ci.nsIThread.DISPATCH_NORMAL);
   223   },
   226   /**
   227    * Captures a thumbnail for the given window.
   228    *
   229    * @param aWindow The DOM window to capture a thumbnail from.
   230    * @return {Promise}
   231    * @resolve {Blob} The thumbnail, as a Blob.
   232    */
   233   captureToBlob: function PageThumbs_captureToBlob(aWindow) {
   234     if (!this._prefEnabled()) {
   235       return null;
   236     }
   238     let canvas = this._createCanvas();
   239     this.captureToCanvas(aWindow, canvas);
   241     let deferred = Promise.defer();
   242     let type = this.contentType;
   243     // Fetch the canvas data on the next event loop tick so that we allow
   244     // some event processing in between drawing to the canvas and encoding
   245     // its data. We want to block the UI as short as possible. See bug 744100.
   246     canvas.toBlob(function asBlob(blob) {
   247       deferred.resolve(blob, type);
   248     });
   249     return deferred.promise;
   250   },
   252   /**
   253    * Captures a thumbnail from a given window and draws it to the given canvas.
   254    * @param aWindow The DOM window to capture a thumbnail from.
   255    * @param aCanvas The canvas to draw to.
   256    */
   257   captureToCanvas: function PageThumbs_captureToCanvas(aWindow, aCanvas) {
   258     let telemetryCaptureTime = new Date();
   259     this._captureToCanvas(aWindow, aCanvas);
   260     let telemetry = Services.telemetry;
   261     telemetry.getHistogramById("FX_THUMBNAILS_CAPTURE_TIME_MS")
   262       .add(new Date() - telemetryCaptureTime);
   263   },
   265   // The background thumbnail service captures to canvas but doesn't want to
   266   // participate in this service's telemetry, which is why this method exists.
   267   _captureToCanvas: function PageThumbs__captureToCanvas(aWindow, aCanvas) {
   268     let [sw, sh, scale] = this._determineCropSize(aWindow, aCanvas);
   269     let ctx = aCanvas.getContext("2d");
   271     // Scale the canvas accordingly.
   272     ctx.save();
   273     ctx.scale(scale, scale);
   275     try {
   276       // Draw the window contents to the canvas.
   277       ctx.drawWindow(aWindow, 0, 0, sw, sh, THUMBNAIL_BG_COLOR,
   278                      ctx.DRAWWINDOW_DO_NOT_FLUSH);
   279     } catch (e) {
   280       // We couldn't draw to the canvas for some reason.
   281     }
   283     ctx.restore();
   284   },
   286   /**
   287    * Captures a thumbnail for the given browser and stores it to the cache.
   288    * @param aBrowser The browser to capture a thumbnail for.
   289    * @param aCallback The function to be called when finished (optional).
   290    */
   291   captureAndStore: function PageThumbs_captureAndStore(aBrowser, aCallback) {
   292     if (!this._prefEnabled()) {
   293       return;
   294     }
   296     let url = aBrowser.currentURI.spec;
   297     let channel = aBrowser.docShell.currentDocumentChannel;
   298     let originalURL = channel.originalURI.spec;
   300     // see if this was an error response.
   301     let wasError = this._isChannelErrorResponse(channel);
   303     TaskUtils.spawn((function task() {
   304       let isSuccess = true;
   305       try {
   306         let blob = yield this.captureToBlob(aBrowser.contentWindow);
   307         let buffer = yield TaskUtils.readBlob(blob);
   308         yield this._store(originalURL, url, buffer, wasError);
   309       } catch (_) {
   310         isSuccess = false;
   311       }
   312       if (aCallback) {
   313         aCallback(isSuccess);
   314       }
   315     }).bind(this));
   316   },
   318   /**
   319    * Checks if an existing thumbnail for the specified URL is either missing
   320    * or stale, and if so, captures and stores it.  Once the thumbnail is stored,
   321    * an observer service notification will be sent, so consumers should observe
   322    * such notifications if they want to be notified of an updated thumbnail.
   323    *
   324    * @param aBrowser The content window of this browser will be captured.
   325    * @param aCallback The function to be called when finished (optional).
   326    */
   327   captureAndStoreIfStale: function PageThumbs_captureAndStoreIfStale(aBrowser, aCallback) {
   328     let url = aBrowser.currentURI.spec;
   329     PageThumbsStorage.isFileRecentForURL(url).then(recent => {
   330       if (!recent.ok &&
   331           // Careful, the call to PageThumbsStorage is async, so the browser may
   332           // have navigated away from the URL or even closed.
   333           aBrowser.currentURI &&
   334           aBrowser.currentURI.spec == url) {
   335         this.captureAndStore(aBrowser, aCallback);
   336       } else if (aCallback) {
   337         aCallback(true);
   338       }
   339     }, err => {
   340       if (aCallback)
   341         aCallback(false);
   342     });
   343   },
   345   /**
   346    * Stores data to disk for the given URLs.
   347    *
   348    * NB: The background thumbnail service calls this, too.
   349    *
   350    * @param aOriginalURL The URL with which the capture was initiated.
   351    * @param aFinalURL The URL to which aOriginalURL ultimately resolved.
   352    * @param aData An ArrayBuffer containing the image data.
   353    * @param aNoOverwrite If true and files for the URLs already exist, the files
   354    *                     will not be overwritten.
   355    * @return {Promise}
   356    */
   357   _store: function PageThumbs__store(aOriginalURL, aFinalURL, aData, aNoOverwrite) {
   358     return TaskUtils.spawn(function () {
   359       let telemetryStoreTime = new Date();
   360       yield PageThumbsStorage.writeData(aFinalURL, aData, aNoOverwrite);
   361       Services.telemetry.getHistogramById("FX_THUMBNAILS_STORE_TIME_MS")
   362         .add(new Date() - telemetryStoreTime);
   364       Services.obs.notifyObservers(null, "page-thumbnail:create", aFinalURL);
   365       // We've been redirected. Create a copy of the current thumbnail for
   366       // the redirect source. We need to do this because:
   367       //
   368       // 1) Users can drag any kind of links onto the newtab page. If those
   369       //    links redirect to a different URL then we want to be able to
   370       //    provide thumbnails for both of them.
   371       //
   372       // 2) The newtab page should actually display redirect targets, only.
   373       //    Because of bug 559175 this information can get lost when using
   374       //    Sync and therefore also redirect sources appear on the newtab
   375       //    page. We also want thumbnails for those.
   376       if (aFinalURL != aOriginalURL) {
   377         yield PageThumbsStorage.copy(aFinalURL, aOriginalURL, aNoOverwrite);
   378         Services.obs.notifyObservers(null, "page-thumbnail:create", aOriginalURL);
   379       }
   380     });
   381   },
   383   /**
   384    * Register an expiration filter.
   385    *
   386    * When thumbnails are going to expire, each registered filter is asked for a
   387    * list of thumbnails to keep.
   388    *
   389    * The filter (if it is a callable) or its filterForThumbnailExpiration method
   390    * (if the filter is an object) is called with a single argument.  The
   391    * argument is a callback function.  The filter must call the callback
   392    * function and pass it an array of zero or more URLs.  (It may do so
   393    * asynchronously.)  Thumbnails for those URLs will be except from expiration.
   394    *
   395    * @param aFilter callable, or object with filterForThumbnailExpiration method
   396    */
   397   addExpirationFilter: function PageThumbs_addExpirationFilter(aFilter) {
   398     PageThumbsExpiration.addFilter(aFilter);
   399   },
   401   /**
   402    * Unregister an expiration filter.
   403    * @param aFilter A filter that was previously passed to addExpirationFilter.
   404    */
   405   removeExpirationFilter: function PageThumbs_removeExpirationFilter(aFilter) {
   406     PageThumbsExpiration.removeFilter(aFilter);
   407   },
   409   /**
   410    * Determines the crop size for a given content window.
   411    * @param aWindow The content window.
   412    * @param aCanvas The target canvas.
   413    * @return An array containing width, height and scale.
   414    */
   415   _determineCropSize: function PageThumbs_determineCropSize(aWindow, aCanvas) {
   416     let utils = aWindow.QueryInterface(Ci.nsIInterfaceRequestor)
   417                        .getInterface(Ci.nsIDOMWindowUtils);
   418     let sbWidth = {}, sbHeight = {};
   420     try {
   421       utils.getScrollbarSize(false, sbWidth, sbHeight);
   422     } catch (e) {
   423       // This might fail if the window does not have a presShell.
   424       Cu.reportError("Unable to get scrollbar size in _determineCropSize.");
   425       sbWidth.value = sbHeight.value = 0;
   426     }
   428     // Even in RTL mode, scrollbars are always on the right.
   429     // So there's no need to determine a left offset.
   430     let sw = aWindow.innerWidth - sbWidth.value;
   431     let sh = aWindow.innerHeight - sbHeight.value;
   433     let {width: thumbnailWidth, height: thumbnailHeight} = aCanvas;
   434     let scale = Math.min(Math.max(thumbnailWidth / sw, thumbnailHeight / sh), 1);
   435     let scaledWidth = sw * scale;
   436     let scaledHeight = sh * scale;
   438     if (scaledHeight > thumbnailHeight)
   439       sh -= Math.floor(Math.abs(scaledHeight - thumbnailHeight) * scale);
   441     if (scaledWidth > thumbnailWidth)
   442       sw -= Math.floor(Math.abs(scaledWidth - thumbnailWidth) * scale);
   444     return [sw, sh, scale];
   445   },
   447   /**
   448    * Creates a new hidden canvas element.
   449    * @param aWindow The document of this window will be used to create the
   450    *                canvas.  If not given, the hidden window will be used.
   451    * @return The newly created canvas.
   452    */
   453   _createCanvas: function PageThumbs_createCanvas(aWindow) {
   454     let doc = (aWindow || Services.appShell.hiddenDOMWindow).document;
   455     let canvas = doc.createElementNS(HTML_NAMESPACE, "canvas");
   456     canvas.mozOpaque = true;
   457     canvas.mozImageSmoothingEnabled = true;
   458     let [thumbnailWidth, thumbnailHeight] = this._getThumbnailSize();
   459     canvas.width = thumbnailWidth;
   460     canvas.height = thumbnailHeight;
   461     return canvas;
   462   },
   464   /**
   465    * Calculates the thumbnail size based on current desktop's dimensions.
   466    * @return The calculated thumbnail size or a default if unable to calculate.
   467    */
   468   _getThumbnailSize: function PageThumbs_getThumbnailSize() {
   469     if (!this._thumbnailWidth || !this._thumbnailHeight) {
   470       let screenManager = Cc["@mozilla.org/gfx/screenmanager;1"]
   471                             .getService(Ci.nsIScreenManager);
   472       let left = {}, top = {}, width = {}, height = {};
   473       screenManager.primaryScreen.GetRectDisplayPix(left, top, width, height);
   474       this._thumbnailWidth = Math.round(width.value / 3);
   475       this._thumbnailHeight = Math.round(height.value / 3);
   476     }
   477     return [this._thumbnailWidth, this._thumbnailHeight];
   478   },
   480   /**
   481    * Given a channel, returns true if it should be considered an "error
   482    * response", false otherwise.
   483    */
   484   _isChannelErrorResponse: function(channel) {
   485     // No valid document channel sounds like an error to me!
   486     if (!channel)
   487       return true;
   488     if (!(channel instanceof Ci.nsIHttpChannel))
   489       // it might be FTP etc, so assume it's ok.
   490       return false;
   491     try {
   492       return !channel.requestSucceeded;
   493     } catch (_) {
   494       // not being able to determine success is surely failure!
   495       return true;
   496     }
   497   },
   499   _prefEnabled: function PageThumbs_prefEnabled() {
   500     try {
   501       return !Services.prefs.getBoolPref("browser.pagethumbnails.capturing_disabled");
   502     }
   503     catch (e) {
   504       return true;
   505     }
   506   },
   507 };
   509 this.PageThumbsStorage = {
   510   // The path for the storage
   511   _path: null,
   512   get path() {
   513     if (!this._path) {
   514       this._path = OS.Path.join(OS.Constants.Path.localProfileDir, THUMBNAIL_DIRECTORY);
   515     }
   516     return this._path;
   517   },
   519   ensurePath: function Storage_ensurePath() {
   520     // Create the directory (ignore any error if the directory
   521     // already exists). As all writes are done from the PageThumbsWorker
   522     // thread, which serializes its operations, this ensures that
   523     // future operations can proceed without having to check whether
   524     // the directory exists.
   525     return PageThumbsWorker.post("makeDir",
   526       [this.path, {ignoreExisting: true}]).then(
   527         null,
   528         function onError(aReason) {
   529           Components.utils.reportError("Could not create thumbnails directory" + aReason);
   530         });
   531   },
   533   getLeafNameForURL: function Storage_getLeafNameForURL(aURL) {
   534     if (typeof aURL != "string") {
   535       throw new TypeError("Expecting a string");
   536     }
   537     let hash = this._calculateMD5Hash(aURL);
   538     return hash + ".png";
   539   },
   541   getFilePathForURL: function Storage_getFilePathForURL(aURL) {
   542     return OS.Path.join(this.path, this.getLeafNameForURL(aURL));
   543   },
   545   /**
   546    * Write the contents of a thumbnail, off the main thread.
   547    *
   548    * @param {string} aURL The url for which to store a thumbnail.
   549    * @param {ArrayBuffer} aData The data to store in the thumbnail, as
   550    * an ArrayBuffer. This array buffer is neutered and cannot be
   551    * reused after the copy.
   552    * @param {boolean} aNoOverwrite If true and the thumbnail's file already
   553    * exists, the file will not be overwritten.
   554    *
   555    * @return {Promise}
   556    */
   557   writeData: function Storage_writeData(aURL, aData, aNoOverwrite) {
   558     let path = this.getFilePathForURL(aURL);
   559     this.ensurePath();
   560     aData = new Uint8Array(aData);
   561     let msg = [
   562       path,
   563       aData,
   564       {
   565         tmpPath: path + ".tmp",
   566         bytes: aData.byteLength,
   567         noOverwrite: aNoOverwrite,
   568         flush: false /*thumbnails do not require the level of guarantee provided by flush*/
   569       }];
   570     return PageThumbsWorker.post("writeAtomic", msg,
   571       msg /*we don't want that message garbage-collected,
   572            as OS.Shared.Type.void_t.in_ptr.toMsg uses C-level
   573            memory tricks to enforce zero-copy*/).
   574       then(null, this._eatNoOverwriteError(aNoOverwrite));
   575   },
   577   /**
   578    * Copy a thumbnail, off the main thread.
   579    *
   580    * @param {string} aSourceURL The url of the thumbnail to copy.
   581    * @param {string} aTargetURL The url of the target thumbnail.
   582    * @param {boolean} aNoOverwrite If true and the target file already exists,
   583    * the file will not be overwritten.
   584    *
   585    * @return {Promise}
   586    */
   587   copy: function Storage_copy(aSourceURL, aTargetURL, aNoOverwrite) {
   588     this.ensurePath();
   589     let sourceFile = this.getFilePathForURL(aSourceURL);
   590     let targetFile = this.getFilePathForURL(aTargetURL);
   591     let options = { noOverwrite: aNoOverwrite };
   592     return PageThumbsWorker.post("copy", [sourceFile, targetFile, options]).
   593       then(null, this._eatNoOverwriteError(aNoOverwrite));
   594   },
   596   /**
   597    * Remove a single thumbnail, off the main thread.
   598    *
   599    * @return {Promise}
   600    */
   601   remove: function Storage_remove(aURL) {
   602     return PageThumbsWorker.post("remove", [this.getFilePathForURL(aURL)]);
   603   },
   605   /**
   606    * Remove all thumbnails, off the main thread.
   607    *
   608    * @return {Promise}
   609    */
   610   wipe: Task.async(function* Storage_wipe() {
   611     //
   612     // This operation may be launched during shutdown, so we need to
   613     // take a few precautions to ensure that:
   614     //
   615     // 1. it is not interrupted by shutdown, in which case we
   616     //    could be leaving privacy-sensitive files on disk;
   617     // 2. it is not launched too late during shutdown, in which
   618     //    case this could cause shutdown freezes (see bug 1005487,
   619     //    which will eventually be fixed by bug 965309)
   620     //
   622     let blocker = () => promise;
   624     // The following operation will rise an error if we have already
   625     // reached profileBeforeChange, in which case it is too late
   626     // to clear the thumbnail wipe.
   627     AsyncShutdown.profileBeforeChange.addBlocker(
   628       "PageThumbs: removing all thumbnails",
   629       blocker);
   631     // Start the work only now that `profileBeforeChange` has had
   632     // a chance to throw an error.
   634     let promise = PageThumbsWorker.post("wipe", [this.path]);
   635     try {
   636       yield promise;
   637     }  finally {
   638        // Generally, we will be done much before profileBeforeChange,
   639        // so let's not hoard blockers.
   640        if ("removeBlocker" in AsyncShutdown.profileBeforeChange) {
   641          // `removeBlocker` was added with bug 985655. In the interest
   642          // of backporting, let's degrade gracefully if `removeBlocker`
   643          // doesn't exist.
   644          AsyncShutdown.profileBeforeChange.removeBlocker(blocker);
   645        }
   646     }
   647   }),
   649   fileExistsForURL: function Storage_fileExistsForURL(aURL) {
   650     return PageThumbsWorker.post("exists", [this.getFilePathForURL(aURL)]);
   651   },
   653   isFileRecentForURL: function Storage_isFileRecentForURL(aURL) {
   654     return PageThumbsWorker.post("isFileRecent",
   655                                  [this.getFilePathForURL(aURL),
   656                                   MAX_THUMBNAIL_AGE_SECS]);
   657   },
   659   _calculateMD5Hash: function Storage_calculateMD5Hash(aValue) {
   660     let hash = gCryptoHash;
   661     let value = gUnicodeConverter.convertToByteArray(aValue);
   663     hash.init(hash.MD5);
   664     hash.update(value, value.length);
   665     return this._convertToHexString(hash.finish(false));
   666   },
   668   _convertToHexString: function Storage_convertToHexString(aData) {
   669     let hex = "";
   670     for (let i = 0; i < aData.length; i++)
   671       hex += ("0" + aData.charCodeAt(i).toString(16)).slice(-2);
   672     return hex;
   673   },
   675   /**
   676    * For functions that take a noOverwrite option, OS.File throws an error if
   677    * the target file exists and noOverwrite is true.  We don't consider that an
   678    * error, and we don't want such errors propagated.
   679    *
   680    * @param {aNoOverwrite} The noOverwrite option used in the OS.File operation.
   681    *
   682    * @return {function} A function that should be passed as the second argument
   683    * to then() (the `onError` argument).
   684    */
   685   _eatNoOverwriteError: function Storage__eatNoOverwriteError(aNoOverwrite) {
   686     return function onError(err) {
   687       if (!aNoOverwrite ||
   688           !(err instanceof OS.File.Error) ||
   689           !err.becauseExists) {
   690         throw err;
   691       }
   692     };
   693   },
   695   // Deprecated, please do not use
   696   getFileForURL: function Storage_getFileForURL_DEPRECATED(aURL) {
   697     Deprecated.warning("PageThumbs.getFileForURL is deprecated. Please use PageThumbs.getFilePathForURL and OS.File",
   698                        "https://developer.mozilla.org/docs/JavaScript_OS.File");
   699     // Note: Once this method has been removed, we can get rid of the dependency towards FileUtils
   700     return new FileUtils.File(PageThumbsStorage.getFilePathForURL(aURL));
   701   }
   702 };
   704 let PageThumbsStorageMigrator = {
   705   get currentVersion() {
   706     try {
   707       return Services.prefs.getIntPref(PREF_STORAGE_VERSION);
   708     } catch (e) {
   709       // The pref doesn't exist, yet. Return version 0.
   710       return 0;
   711     }
   712   },
   714   set currentVersion(aVersion) {
   715     Services.prefs.setIntPref(PREF_STORAGE_VERSION, aVersion);
   716   },
   718   migrate: function Migrator_migrate() {
   719     let version = this.currentVersion;
   721     // Storage version 1 never made it to beta.
   722     // At the time of writing only Windows had (ProfD != ProfLD) and we
   723     // needed to move thumbnails from the roaming profile to the locale
   724     // one so that they're not needlessly included in backups and/or
   725     // written via SMB.
   727     // Storage version 2 also never made it to beta.
   728     // The thumbnail folder structure has been changed and old thumbnails
   729     // were not migrated. Instead, we just renamed the current folder to
   730     // "<name>-old" and will remove it later.
   732     if (version < 3) {
   733       this.migrateToVersion3();
   734     }
   736     this.currentVersion = LATEST_STORAGE_VERSION;
   737   },
   739   /**
   740    * Bug 239254 added support for having the disk cache and thumbnail
   741    * directories on a local path (i.e. ~/.cache/) under Linux. We'll first
   742    * try to move the old thumbnails to their new location. If that's not
   743    * possible (because ProfD might be on a different file system than
   744    * ProfLD) we'll just discard them.
   745    *
   746    * @param {string*} local The path to the local profile directory.
   747    * Used for testing. Default argument is good for all non-testing uses.
   748    * @param {string*} roaming The path to the roaming profile directory.
   749    * Used for testing. Default argument is good for all non-testing uses.
   750    */
   751   migrateToVersion3: function Migrator_migrateToVersion3(
   752     local = OS.Constants.Path.localProfileDir,
   753     roaming = OS.Constants.Path.profileDir) {
   754     PageThumbsWorker.post(
   755       "moveOrDeleteAllThumbnails",
   756       [OS.Path.join(roaming, THUMBNAIL_DIRECTORY),
   757        OS.Path.join(local, THUMBNAIL_DIRECTORY)]
   758     );
   759   }
   760 };
   762 let PageThumbsExpiration = {
   763   _filters: [],
   765   init: function Expiration_init() {
   766     gUpdateTimerManager.registerTimer("browser-cleanup-thumbnails", this,
   767                                       EXPIRATION_INTERVAL_SECS);
   768   },
   770   addFilter: function Expiration_addFilter(aFilter) {
   771     this._filters.push(aFilter);
   772   },
   774   removeFilter: function Expiration_removeFilter(aFilter) {
   775     let index = this._filters.indexOf(aFilter);
   776     if (index > -1)
   777       this._filters.splice(index, 1);
   778   },
   780   notify: function Expiration_notify(aTimer) {
   781     let urls = [];
   782     let filtersToWaitFor = this._filters.length;
   784     let expire = function expire() {
   785       this.expireThumbnails(urls);
   786     }.bind(this);
   788     // No registered filters.
   789     if (!filtersToWaitFor) {
   790       expire();
   791       return;
   792     }
   794     function filterCallback(aURLs) {
   795       urls = urls.concat(aURLs);
   796       if (--filtersToWaitFor == 0)
   797         expire();
   798     }
   800     for (let filter of this._filters) {
   801       if (typeof filter == "function")
   802         filter(filterCallback)
   803       else
   804         filter.filterForThumbnailExpiration(filterCallback);
   805     }
   806   },
   808   expireThumbnails: function Expiration_expireThumbnails(aURLsToKeep) {
   809     let path = this.path;
   810     let keep = [PageThumbsStorage.getLeafNameForURL(url) for (url of aURLsToKeep)];
   811     let msg = [
   812       PageThumbsStorage.path,
   813       keep,
   814       EXPIRATION_MIN_CHUNK_SIZE
   815     ];
   817     return PageThumbsWorker.post(
   818       "expireFilesInDirectory",
   819       msg
   820     );
   821   }
   822 };
   824 /**
   825  * Interface to a dedicated thread handling I/O
   826  */
   828 let PageThumbsWorker = (function() {
   829   let worker = new PromiseWorker("resource://gre/modules/PageThumbsWorker.js",
   830     OS.Shared.LOG.bind("PageThumbs"));
   831   return {
   832     post: function post(...args) {
   833       let promise = worker.post.apply(worker, args);
   834       return promise.then(
   835         null,
   836         function onError(error) {
   837           // Decode any serialized error
   838           if (error instanceof PromiseWorker.WorkerError) {
   839             throw OS.File.Error.fromMsg(error.data);
   840           } else {
   841             throw error;
   842           }
   843         }
   844       );
   845     }
   846   };
   847 })();
   849 let PageThumbsHistoryObserver = {
   850   onDeleteURI: function Thumbnails_onDeleteURI(aURI, aGUID) {
   851     PageThumbsStorage.remove(aURI.spec);
   852   },
   854   onClearHistory: function Thumbnails_onClearHistory() {
   855     PageThumbsStorage.wipe();
   856   },
   858   onTitleChanged: function () {},
   859   onBeginUpdateBatch: function () {},
   860   onEndUpdateBatch: function () {},
   861   onVisit: function () {},
   862   onPageChanged: function () {},
   863   onDeleteVisits: function () {},
   865   QueryInterface: XPCOMUtils.generateQI([Ci.nsINavHistoryObserver])
   866 };

mercurial