toolkit/components/thumbnails/PageThumbs.jsm

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/toolkit/components/thumbnails/PageThumbs.jsm	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,866 @@
     1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public
     1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this file,
     1.6 + * You can obtain one at http://mozilla.org/MPL/2.0/. */
     1.7 +
     1.8 +"use strict";
     1.9 +
    1.10 +this.EXPORTED_SYMBOLS = ["PageThumbs", "PageThumbsStorage"];
    1.11 +
    1.12 +const Cu = Components.utils;
    1.13 +const Cc = Components.classes;
    1.14 +const Ci = Components.interfaces;
    1.15 +
    1.16 +const HTML_NAMESPACE = "http://www.w3.org/1999/xhtml";
    1.17 +const PREF_STORAGE_VERSION = "browser.pagethumbnails.storage_version";
    1.18 +const LATEST_STORAGE_VERSION = 3;
    1.19 +
    1.20 +const EXPIRATION_MIN_CHUNK_SIZE = 50;
    1.21 +const EXPIRATION_INTERVAL_SECS = 3600;
    1.22 +
    1.23 +// If a request for a thumbnail comes in and we find one that is "stale"
    1.24 +// (or don't find one at all) we automatically queue a request to generate a
    1.25 +// new one.
    1.26 +const MAX_THUMBNAIL_AGE_SECS = 172800; // 2 days == 60*60*24*2 == 172800 secs.
    1.27 +
    1.28 +/**
    1.29 + * Name of the directory in the profile that contains the thumbnails.
    1.30 + */
    1.31 +const THUMBNAIL_DIRECTORY = "thumbnails";
    1.32 +
    1.33 +/**
    1.34 + * The default background color for page thumbnails.
    1.35 + */
    1.36 +const THUMBNAIL_BG_COLOR = "#fff";
    1.37 +
    1.38 +Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
    1.39 +Cu.import("resource://gre/modules/osfile/_PromiseWorker.jsm", this);
    1.40 +Cu.import("resource://gre/modules/Promise.jsm", this);
    1.41 +Cu.import("resource://gre/modules/osfile.jsm", this);
    1.42 +
    1.43 +XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
    1.44 +  "resource://gre/modules/NetUtil.jsm");
    1.45 +
    1.46 +XPCOMUtils.defineLazyModuleGetter(this, "Services",
    1.47 +  "resource://gre/modules/Services.jsm");
    1.48 +
    1.49 +XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
    1.50 +  "resource://gre/modules/FileUtils.jsm");
    1.51 +
    1.52 +XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
    1.53 +  "resource://gre/modules/PlacesUtils.jsm");
    1.54 +
    1.55 +XPCOMUtils.defineLazyServiceGetter(this, "gUpdateTimerManager",
    1.56 +  "@mozilla.org/updates/timer-manager;1", "nsIUpdateTimerManager");
    1.57 +
    1.58 +XPCOMUtils.defineLazyGetter(this, "gCryptoHash", function () {
    1.59 +  return Cc["@mozilla.org/security/hash;1"].createInstance(Ci.nsICryptoHash);
    1.60 +});
    1.61 +
    1.62 +XPCOMUtils.defineLazyGetter(this, "gUnicodeConverter", function () {
    1.63 +  let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
    1.64 +                    .createInstance(Ci.nsIScriptableUnicodeConverter);
    1.65 +  converter.charset = 'utf8';
    1.66 +  return converter;
    1.67 +});
    1.68 +
    1.69 +XPCOMUtils.defineLazyModuleGetter(this, "Task",
    1.70 +  "resource://gre/modules/Task.jsm");
    1.71 +XPCOMUtils.defineLazyModuleGetter(this, "Deprecated",
    1.72 +  "resource://gre/modules/Deprecated.jsm");
    1.73 +XPCOMUtils.defineLazyModuleGetter(this, "AsyncShutdown",
    1.74 +  "resource://gre/modules/AsyncShutdown.jsm");
    1.75 +
    1.76 +/**
    1.77 + * Utilities for dealing with promises and Task.jsm
    1.78 + */
    1.79 +const TaskUtils = {
    1.80 +  /**
    1.81 +   * Add logging to a promise.
    1.82 +   *
    1.83 +   * @param {Promise} promise
    1.84 +   * @return {Promise} A promise behaving as |promise|, but with additional
    1.85 +   * logging in case of uncaught error.
    1.86 +   */
    1.87 +  captureErrors: function captureErrors(promise) {
    1.88 +    return promise.then(
    1.89 +      null,
    1.90 +      function onError(reason) {
    1.91 +        Cu.reportError("Uncaught asynchronous error: " + reason + " at\n"
    1.92 +          + reason.stack + "\n");
    1.93 +        throw reason;
    1.94 +      }
    1.95 +    );
    1.96 +  },
    1.97 +
    1.98 +  /**
    1.99 +   * Spawn a new Task from a generator.
   1.100 +   *
   1.101 +   * This function behaves as |Task.spawn|, with the exception that it
   1.102 +   * adds logging in case of uncaught error. For more information, see
   1.103 +   * the documentation of |Task.jsm|.
   1.104 +   *
   1.105 +   * @param {generator} gen Some generator.
   1.106 +   * @return {Promise} A promise built from |gen|, with the same semantics
   1.107 +   * as |Task.spawn(gen)|.
   1.108 +   */
   1.109 +  spawn: function spawn(gen) {
   1.110 +    return this.captureErrors(Task.spawn(gen));
   1.111 +  },
   1.112 +  /**
   1.113 +   * Read the bytes from a blob, asynchronously.
   1.114 +   *
   1.115 +   * @return {Promise}
   1.116 +   * @resolve {ArrayBuffer} In case of success, the bytes contained in the blob.
   1.117 +   * @reject {DOMError} In case of error, the underlying DOMError.
   1.118 +   */
   1.119 +  readBlob: function readBlob(blob) {
   1.120 +    let deferred = Promise.defer();
   1.121 +    let reader = Cc["@mozilla.org/files/filereader;1"].createInstance(Ci.nsIDOMFileReader);
   1.122 +    reader.onloadend = function onloadend() {
   1.123 +      if (reader.readyState != Ci.nsIDOMFileReader.DONE) {
   1.124 +        deferred.reject(reader.error);
   1.125 +      } else {
   1.126 +        deferred.resolve(reader.result);
   1.127 +      }
   1.128 +    };
   1.129 +    reader.readAsArrayBuffer(blob);
   1.130 +    return deferred.promise;
   1.131 +  }
   1.132 +};
   1.133 +
   1.134 +
   1.135 +
   1.136 +
   1.137 +/**
   1.138 + * Singleton providing functionality for capturing web page thumbnails and for
   1.139 + * accessing them if already cached.
   1.140 + */
   1.141 +this.PageThumbs = {
   1.142 +  _initialized: false,
   1.143 +
   1.144 +  /**
   1.145 +   * The calculated width and height of the thumbnails.
   1.146 +   */
   1.147 +  _thumbnailWidth : 0,
   1.148 +  _thumbnailHeight : 0,
   1.149 +
   1.150 +  /**
   1.151 +   * The scheme to use for thumbnail urls.
   1.152 +   */
   1.153 +  get scheme() "moz-page-thumb",
   1.154 +
   1.155 +  /**
   1.156 +   * The static host to use for thumbnail urls.
   1.157 +   */
   1.158 +  get staticHost() "thumbnail",
   1.159 +
   1.160 +  /**
   1.161 +   * The thumbnails' image type.
   1.162 +   */
   1.163 +  get contentType() "image/png",
   1.164 +
   1.165 +  init: function PageThumbs_init() {
   1.166 +    if (!this._initialized) {
   1.167 +      this._initialized = true;
   1.168 +      PlacesUtils.history.addObserver(PageThumbsHistoryObserver, false);
   1.169 +
   1.170 +      // Migrate the underlying storage, if needed.
   1.171 +      PageThumbsStorageMigrator.migrate();
   1.172 +      PageThumbsExpiration.init();
   1.173 +    }
   1.174 +  },
   1.175 +
   1.176 +  uninit: function PageThumbs_uninit() {
   1.177 +    if (this._initialized) {
   1.178 +      this._initialized = false;
   1.179 +      PlacesUtils.history.removeObserver(PageThumbsHistoryObserver);
   1.180 +    }
   1.181 +  },
   1.182 +
   1.183 +  /**
   1.184 +   * Gets the thumbnail image's url for a given web page's url.
   1.185 +   * @param aUrl The web page's url that is depicted in the thumbnail.
   1.186 +   * @return The thumbnail image's url.
   1.187 +   */
   1.188 +  getThumbnailURL: function PageThumbs_getThumbnailURL(aUrl) {
   1.189 +    return this.scheme + "://" + this.staticHost +
   1.190 +           "?url=" + encodeURIComponent(aUrl);
   1.191 +  },
   1.192 +
   1.193 +   /**
   1.194 +    * Gets the path of the thumbnail file for a given web page's
   1.195 +    * url. This file may or may not exist depending on whether the
   1.196 +    * thumbnail has been captured or not.
   1.197 +    *
   1.198 +    * @param aUrl The web page's url.
   1.199 +    * @return The path of the thumbnail file.
   1.200 +    */
   1.201 +   getThumbnailPath: function PageThumbs_getThumbnailPath(aUrl) {
   1.202 +     return PageThumbsStorage.getFilePathForURL(aUrl);
   1.203 +   },
   1.204 +
   1.205 +  /**
   1.206 +   * Captures a thumbnail for the given window.
   1.207 +   * @param aWindow The DOM window to capture a thumbnail from.
   1.208 +   * @param aCallback The function to be called when the thumbnail has been
   1.209 +   *                  captured. The first argument will be the data stream
   1.210 +   *                  containing the image data.
   1.211 +   */
   1.212 +  capture: function PageThumbs_capture(aWindow, aCallback) {
   1.213 +    if (!this._prefEnabled()) {
   1.214 +      return;
   1.215 +    }
   1.216 +
   1.217 +    let canvas = this._createCanvas();
   1.218 +    this.captureToCanvas(aWindow, canvas);
   1.219 +
   1.220 +    // Fetch the canvas data on the next event loop tick so that we allow
   1.221 +    // some event processing in between drawing to the canvas and encoding
   1.222 +    // its data. We want to block the UI as short as possible. See bug 744100.
   1.223 +    Services.tm.currentThread.dispatch(function () {
   1.224 +      canvas.mozFetchAsStream(aCallback, this.contentType);
   1.225 +    }.bind(this), Ci.nsIThread.DISPATCH_NORMAL);
   1.226 +  },
   1.227 +
   1.228 +
   1.229 +  /**
   1.230 +   * Captures a thumbnail for the given window.
   1.231 +   *
   1.232 +   * @param aWindow The DOM window to capture a thumbnail from.
   1.233 +   * @return {Promise}
   1.234 +   * @resolve {Blob} The thumbnail, as a Blob.
   1.235 +   */
   1.236 +  captureToBlob: function PageThumbs_captureToBlob(aWindow) {
   1.237 +    if (!this._prefEnabled()) {
   1.238 +      return null;
   1.239 +    }
   1.240 +
   1.241 +    let canvas = this._createCanvas();
   1.242 +    this.captureToCanvas(aWindow, canvas);
   1.243 +
   1.244 +    let deferred = Promise.defer();
   1.245 +    let type = this.contentType;
   1.246 +    // Fetch the canvas data on the next event loop tick so that we allow
   1.247 +    // some event processing in between drawing to the canvas and encoding
   1.248 +    // its data. We want to block the UI as short as possible. See bug 744100.
   1.249 +    canvas.toBlob(function asBlob(blob) {
   1.250 +      deferred.resolve(blob, type);
   1.251 +    });
   1.252 +    return deferred.promise;
   1.253 +  },
   1.254 +
   1.255 +  /**
   1.256 +   * Captures a thumbnail from a given window and draws it to the given canvas.
   1.257 +   * @param aWindow The DOM window to capture a thumbnail from.
   1.258 +   * @param aCanvas The canvas to draw to.
   1.259 +   */
   1.260 +  captureToCanvas: function PageThumbs_captureToCanvas(aWindow, aCanvas) {
   1.261 +    let telemetryCaptureTime = new Date();
   1.262 +    this._captureToCanvas(aWindow, aCanvas);
   1.263 +    let telemetry = Services.telemetry;
   1.264 +    telemetry.getHistogramById("FX_THUMBNAILS_CAPTURE_TIME_MS")
   1.265 +      .add(new Date() - telemetryCaptureTime);
   1.266 +  },
   1.267 +
   1.268 +  // The background thumbnail service captures to canvas but doesn't want to
   1.269 +  // participate in this service's telemetry, which is why this method exists.
   1.270 +  _captureToCanvas: function PageThumbs__captureToCanvas(aWindow, aCanvas) {
   1.271 +    let [sw, sh, scale] = this._determineCropSize(aWindow, aCanvas);
   1.272 +    let ctx = aCanvas.getContext("2d");
   1.273 +
   1.274 +    // Scale the canvas accordingly.
   1.275 +    ctx.save();
   1.276 +    ctx.scale(scale, scale);
   1.277 +
   1.278 +    try {
   1.279 +      // Draw the window contents to the canvas.
   1.280 +      ctx.drawWindow(aWindow, 0, 0, sw, sh, THUMBNAIL_BG_COLOR,
   1.281 +                     ctx.DRAWWINDOW_DO_NOT_FLUSH);
   1.282 +    } catch (e) {
   1.283 +      // We couldn't draw to the canvas for some reason.
   1.284 +    }
   1.285 +
   1.286 +    ctx.restore();
   1.287 +  },
   1.288 +
   1.289 +  /**
   1.290 +   * Captures a thumbnail for the given browser and stores it to the cache.
   1.291 +   * @param aBrowser The browser to capture a thumbnail for.
   1.292 +   * @param aCallback The function to be called when finished (optional).
   1.293 +   */
   1.294 +  captureAndStore: function PageThumbs_captureAndStore(aBrowser, aCallback) {
   1.295 +    if (!this._prefEnabled()) {
   1.296 +      return;
   1.297 +    }
   1.298 +
   1.299 +    let url = aBrowser.currentURI.spec;
   1.300 +    let channel = aBrowser.docShell.currentDocumentChannel;
   1.301 +    let originalURL = channel.originalURI.spec;
   1.302 +
   1.303 +    // see if this was an error response.
   1.304 +    let wasError = this._isChannelErrorResponse(channel);
   1.305 +
   1.306 +    TaskUtils.spawn((function task() {
   1.307 +      let isSuccess = true;
   1.308 +      try {
   1.309 +        let blob = yield this.captureToBlob(aBrowser.contentWindow);
   1.310 +        let buffer = yield TaskUtils.readBlob(blob);
   1.311 +        yield this._store(originalURL, url, buffer, wasError);
   1.312 +      } catch (_) {
   1.313 +        isSuccess = false;
   1.314 +      }
   1.315 +      if (aCallback) {
   1.316 +        aCallback(isSuccess);
   1.317 +      }
   1.318 +    }).bind(this));
   1.319 +  },
   1.320 +
   1.321 +  /**
   1.322 +   * Checks if an existing thumbnail for the specified URL is either missing
   1.323 +   * or stale, and if so, captures and stores it.  Once the thumbnail is stored,
   1.324 +   * an observer service notification will be sent, so consumers should observe
   1.325 +   * such notifications if they want to be notified of an updated thumbnail.
   1.326 +   *
   1.327 +   * @param aBrowser The content window of this browser will be captured.
   1.328 +   * @param aCallback The function to be called when finished (optional).
   1.329 +   */
   1.330 +  captureAndStoreIfStale: function PageThumbs_captureAndStoreIfStale(aBrowser, aCallback) {
   1.331 +    let url = aBrowser.currentURI.spec;
   1.332 +    PageThumbsStorage.isFileRecentForURL(url).then(recent => {
   1.333 +      if (!recent.ok &&
   1.334 +          // Careful, the call to PageThumbsStorage is async, so the browser may
   1.335 +          // have navigated away from the URL or even closed.
   1.336 +          aBrowser.currentURI &&
   1.337 +          aBrowser.currentURI.spec == url) {
   1.338 +        this.captureAndStore(aBrowser, aCallback);
   1.339 +      } else if (aCallback) {
   1.340 +        aCallback(true);
   1.341 +      }
   1.342 +    }, err => {
   1.343 +      if (aCallback)
   1.344 +        aCallback(false);
   1.345 +    });
   1.346 +  },
   1.347 +
   1.348 +  /**
   1.349 +   * Stores data to disk for the given URLs.
   1.350 +   *
   1.351 +   * NB: The background thumbnail service calls this, too.
   1.352 +   *
   1.353 +   * @param aOriginalURL The URL with which the capture was initiated.
   1.354 +   * @param aFinalURL The URL to which aOriginalURL ultimately resolved.
   1.355 +   * @param aData An ArrayBuffer containing the image data.
   1.356 +   * @param aNoOverwrite If true and files for the URLs already exist, the files
   1.357 +   *                     will not be overwritten.
   1.358 +   * @return {Promise}
   1.359 +   */
   1.360 +  _store: function PageThumbs__store(aOriginalURL, aFinalURL, aData, aNoOverwrite) {
   1.361 +    return TaskUtils.spawn(function () {
   1.362 +      let telemetryStoreTime = new Date();
   1.363 +      yield PageThumbsStorage.writeData(aFinalURL, aData, aNoOverwrite);
   1.364 +      Services.telemetry.getHistogramById("FX_THUMBNAILS_STORE_TIME_MS")
   1.365 +        .add(new Date() - telemetryStoreTime);
   1.366 +
   1.367 +      Services.obs.notifyObservers(null, "page-thumbnail:create", aFinalURL);
   1.368 +      // We've been redirected. Create a copy of the current thumbnail for
   1.369 +      // the redirect source. We need to do this because:
   1.370 +      //
   1.371 +      // 1) Users can drag any kind of links onto the newtab page. If those
   1.372 +      //    links redirect to a different URL then we want to be able to
   1.373 +      //    provide thumbnails for both of them.
   1.374 +      //
   1.375 +      // 2) The newtab page should actually display redirect targets, only.
   1.376 +      //    Because of bug 559175 this information can get lost when using
   1.377 +      //    Sync and therefore also redirect sources appear on the newtab
   1.378 +      //    page. We also want thumbnails for those.
   1.379 +      if (aFinalURL != aOriginalURL) {
   1.380 +        yield PageThumbsStorage.copy(aFinalURL, aOriginalURL, aNoOverwrite);
   1.381 +        Services.obs.notifyObservers(null, "page-thumbnail:create", aOriginalURL);
   1.382 +      }
   1.383 +    });
   1.384 +  },
   1.385 +
   1.386 +  /**
   1.387 +   * Register an expiration filter.
   1.388 +   *
   1.389 +   * When thumbnails are going to expire, each registered filter is asked for a
   1.390 +   * list of thumbnails to keep.
   1.391 +   *
   1.392 +   * The filter (if it is a callable) or its filterForThumbnailExpiration method
   1.393 +   * (if the filter is an object) is called with a single argument.  The
   1.394 +   * argument is a callback function.  The filter must call the callback
   1.395 +   * function and pass it an array of zero or more URLs.  (It may do so
   1.396 +   * asynchronously.)  Thumbnails for those URLs will be except from expiration.
   1.397 +   *
   1.398 +   * @param aFilter callable, or object with filterForThumbnailExpiration method
   1.399 +   */
   1.400 +  addExpirationFilter: function PageThumbs_addExpirationFilter(aFilter) {
   1.401 +    PageThumbsExpiration.addFilter(aFilter);
   1.402 +  },
   1.403 +
   1.404 +  /**
   1.405 +   * Unregister an expiration filter.
   1.406 +   * @param aFilter A filter that was previously passed to addExpirationFilter.
   1.407 +   */
   1.408 +  removeExpirationFilter: function PageThumbs_removeExpirationFilter(aFilter) {
   1.409 +    PageThumbsExpiration.removeFilter(aFilter);
   1.410 +  },
   1.411 +
   1.412 +  /**
   1.413 +   * Determines the crop size for a given content window.
   1.414 +   * @param aWindow The content window.
   1.415 +   * @param aCanvas The target canvas.
   1.416 +   * @return An array containing width, height and scale.
   1.417 +   */
   1.418 +  _determineCropSize: function PageThumbs_determineCropSize(aWindow, aCanvas) {
   1.419 +    let utils = aWindow.QueryInterface(Ci.nsIInterfaceRequestor)
   1.420 +                       .getInterface(Ci.nsIDOMWindowUtils);
   1.421 +    let sbWidth = {}, sbHeight = {};
   1.422 +
   1.423 +    try {
   1.424 +      utils.getScrollbarSize(false, sbWidth, sbHeight);
   1.425 +    } catch (e) {
   1.426 +      // This might fail if the window does not have a presShell.
   1.427 +      Cu.reportError("Unable to get scrollbar size in _determineCropSize.");
   1.428 +      sbWidth.value = sbHeight.value = 0;
   1.429 +    }
   1.430 +
   1.431 +    // Even in RTL mode, scrollbars are always on the right.
   1.432 +    // So there's no need to determine a left offset.
   1.433 +    let sw = aWindow.innerWidth - sbWidth.value;
   1.434 +    let sh = aWindow.innerHeight - sbHeight.value;
   1.435 +
   1.436 +    let {width: thumbnailWidth, height: thumbnailHeight} = aCanvas;
   1.437 +    let scale = Math.min(Math.max(thumbnailWidth / sw, thumbnailHeight / sh), 1);
   1.438 +    let scaledWidth = sw * scale;
   1.439 +    let scaledHeight = sh * scale;
   1.440 +
   1.441 +    if (scaledHeight > thumbnailHeight)
   1.442 +      sh -= Math.floor(Math.abs(scaledHeight - thumbnailHeight) * scale);
   1.443 +
   1.444 +    if (scaledWidth > thumbnailWidth)
   1.445 +      sw -= Math.floor(Math.abs(scaledWidth - thumbnailWidth) * scale);
   1.446 +
   1.447 +    return [sw, sh, scale];
   1.448 +  },
   1.449 +
   1.450 +  /**
   1.451 +   * Creates a new hidden canvas element.
   1.452 +   * @param aWindow The document of this window will be used to create the
   1.453 +   *                canvas.  If not given, the hidden window will be used.
   1.454 +   * @return The newly created canvas.
   1.455 +   */
   1.456 +  _createCanvas: function PageThumbs_createCanvas(aWindow) {
   1.457 +    let doc = (aWindow || Services.appShell.hiddenDOMWindow).document;
   1.458 +    let canvas = doc.createElementNS(HTML_NAMESPACE, "canvas");
   1.459 +    canvas.mozOpaque = true;
   1.460 +    canvas.mozImageSmoothingEnabled = true;
   1.461 +    let [thumbnailWidth, thumbnailHeight] = this._getThumbnailSize();
   1.462 +    canvas.width = thumbnailWidth;
   1.463 +    canvas.height = thumbnailHeight;
   1.464 +    return canvas;
   1.465 +  },
   1.466 +
   1.467 +  /**
   1.468 +   * Calculates the thumbnail size based on current desktop's dimensions.
   1.469 +   * @return The calculated thumbnail size or a default if unable to calculate.
   1.470 +   */
   1.471 +  _getThumbnailSize: function PageThumbs_getThumbnailSize() {
   1.472 +    if (!this._thumbnailWidth || !this._thumbnailHeight) {
   1.473 +      let screenManager = Cc["@mozilla.org/gfx/screenmanager;1"]
   1.474 +                            .getService(Ci.nsIScreenManager);
   1.475 +      let left = {}, top = {}, width = {}, height = {};
   1.476 +      screenManager.primaryScreen.GetRectDisplayPix(left, top, width, height);
   1.477 +      this._thumbnailWidth = Math.round(width.value / 3);
   1.478 +      this._thumbnailHeight = Math.round(height.value / 3);
   1.479 +    }
   1.480 +    return [this._thumbnailWidth, this._thumbnailHeight];
   1.481 +  },
   1.482 +
   1.483 +  /**
   1.484 +   * Given a channel, returns true if it should be considered an "error
   1.485 +   * response", false otherwise.
   1.486 +   */
   1.487 +  _isChannelErrorResponse: function(channel) {
   1.488 +    // No valid document channel sounds like an error to me!
   1.489 +    if (!channel)
   1.490 +      return true;
   1.491 +    if (!(channel instanceof Ci.nsIHttpChannel))
   1.492 +      // it might be FTP etc, so assume it's ok.
   1.493 +      return false;
   1.494 +    try {
   1.495 +      return !channel.requestSucceeded;
   1.496 +    } catch (_) {
   1.497 +      // not being able to determine success is surely failure!
   1.498 +      return true;
   1.499 +    }
   1.500 +  },
   1.501 +
   1.502 +  _prefEnabled: function PageThumbs_prefEnabled() {
   1.503 +    try {
   1.504 +      return !Services.prefs.getBoolPref("browser.pagethumbnails.capturing_disabled");
   1.505 +    }
   1.506 +    catch (e) {
   1.507 +      return true;
   1.508 +    }
   1.509 +  },
   1.510 +};
   1.511 +
   1.512 +this.PageThumbsStorage = {
   1.513 +  // The path for the storage
   1.514 +  _path: null,
   1.515 +  get path() {
   1.516 +    if (!this._path) {
   1.517 +      this._path = OS.Path.join(OS.Constants.Path.localProfileDir, THUMBNAIL_DIRECTORY);
   1.518 +    }
   1.519 +    return this._path;
   1.520 +  },
   1.521 +
   1.522 +  ensurePath: function Storage_ensurePath() {
   1.523 +    // Create the directory (ignore any error if the directory
   1.524 +    // already exists). As all writes are done from the PageThumbsWorker
   1.525 +    // thread, which serializes its operations, this ensures that
   1.526 +    // future operations can proceed without having to check whether
   1.527 +    // the directory exists.
   1.528 +    return PageThumbsWorker.post("makeDir",
   1.529 +      [this.path, {ignoreExisting: true}]).then(
   1.530 +        null,
   1.531 +        function onError(aReason) {
   1.532 +          Components.utils.reportError("Could not create thumbnails directory" + aReason);
   1.533 +        });
   1.534 +  },
   1.535 +
   1.536 +  getLeafNameForURL: function Storage_getLeafNameForURL(aURL) {
   1.537 +    if (typeof aURL != "string") {
   1.538 +      throw new TypeError("Expecting a string");
   1.539 +    }
   1.540 +    let hash = this._calculateMD5Hash(aURL);
   1.541 +    return hash + ".png";
   1.542 +  },
   1.543 +
   1.544 +  getFilePathForURL: function Storage_getFilePathForURL(aURL) {
   1.545 +    return OS.Path.join(this.path, this.getLeafNameForURL(aURL));
   1.546 +  },
   1.547 +
   1.548 +  /**
   1.549 +   * Write the contents of a thumbnail, off the main thread.
   1.550 +   *
   1.551 +   * @param {string} aURL The url for which to store a thumbnail.
   1.552 +   * @param {ArrayBuffer} aData The data to store in the thumbnail, as
   1.553 +   * an ArrayBuffer. This array buffer is neutered and cannot be
   1.554 +   * reused after the copy.
   1.555 +   * @param {boolean} aNoOverwrite If true and the thumbnail's file already
   1.556 +   * exists, the file will not be overwritten.
   1.557 +   *
   1.558 +   * @return {Promise}
   1.559 +   */
   1.560 +  writeData: function Storage_writeData(aURL, aData, aNoOverwrite) {
   1.561 +    let path = this.getFilePathForURL(aURL);
   1.562 +    this.ensurePath();
   1.563 +    aData = new Uint8Array(aData);
   1.564 +    let msg = [
   1.565 +      path,
   1.566 +      aData,
   1.567 +      {
   1.568 +        tmpPath: path + ".tmp",
   1.569 +        bytes: aData.byteLength,
   1.570 +        noOverwrite: aNoOverwrite,
   1.571 +        flush: false /*thumbnails do not require the level of guarantee provided by flush*/
   1.572 +      }];
   1.573 +    return PageThumbsWorker.post("writeAtomic", msg,
   1.574 +      msg /*we don't want that message garbage-collected,
   1.575 +           as OS.Shared.Type.void_t.in_ptr.toMsg uses C-level
   1.576 +           memory tricks to enforce zero-copy*/).
   1.577 +      then(null, this._eatNoOverwriteError(aNoOverwrite));
   1.578 +  },
   1.579 +
   1.580 +  /**
   1.581 +   * Copy a thumbnail, off the main thread.
   1.582 +   *
   1.583 +   * @param {string} aSourceURL The url of the thumbnail to copy.
   1.584 +   * @param {string} aTargetURL The url of the target thumbnail.
   1.585 +   * @param {boolean} aNoOverwrite If true and the target file already exists,
   1.586 +   * the file will not be overwritten.
   1.587 +   *
   1.588 +   * @return {Promise}
   1.589 +   */
   1.590 +  copy: function Storage_copy(aSourceURL, aTargetURL, aNoOverwrite) {
   1.591 +    this.ensurePath();
   1.592 +    let sourceFile = this.getFilePathForURL(aSourceURL);
   1.593 +    let targetFile = this.getFilePathForURL(aTargetURL);
   1.594 +    let options = { noOverwrite: aNoOverwrite };
   1.595 +    return PageThumbsWorker.post("copy", [sourceFile, targetFile, options]).
   1.596 +      then(null, this._eatNoOverwriteError(aNoOverwrite));
   1.597 +  },
   1.598 +
   1.599 +  /**
   1.600 +   * Remove a single thumbnail, off the main thread.
   1.601 +   *
   1.602 +   * @return {Promise}
   1.603 +   */
   1.604 +  remove: function Storage_remove(aURL) {
   1.605 +    return PageThumbsWorker.post("remove", [this.getFilePathForURL(aURL)]);
   1.606 +  },
   1.607 +
   1.608 +  /**
   1.609 +   * Remove all thumbnails, off the main thread.
   1.610 +   *
   1.611 +   * @return {Promise}
   1.612 +   */
   1.613 +  wipe: Task.async(function* Storage_wipe() {
   1.614 +    //
   1.615 +    // This operation may be launched during shutdown, so we need to
   1.616 +    // take a few precautions to ensure that:
   1.617 +    //
   1.618 +    // 1. it is not interrupted by shutdown, in which case we
   1.619 +    //    could be leaving privacy-sensitive files on disk;
   1.620 +    // 2. it is not launched too late during shutdown, in which
   1.621 +    //    case this could cause shutdown freezes (see bug 1005487,
   1.622 +    //    which will eventually be fixed by bug 965309)
   1.623 +    //
   1.624 +
   1.625 +    let blocker = () => promise;
   1.626 +
   1.627 +    // The following operation will rise an error if we have already
   1.628 +    // reached profileBeforeChange, in which case it is too late
   1.629 +    // to clear the thumbnail wipe.
   1.630 +    AsyncShutdown.profileBeforeChange.addBlocker(
   1.631 +      "PageThumbs: removing all thumbnails",
   1.632 +      blocker);
   1.633 +
   1.634 +    // Start the work only now that `profileBeforeChange` has had
   1.635 +    // a chance to throw an error.
   1.636 +
   1.637 +    let promise = PageThumbsWorker.post("wipe", [this.path]);
   1.638 +    try {
   1.639 +      yield promise;
   1.640 +    }  finally {
   1.641 +       // Generally, we will be done much before profileBeforeChange,
   1.642 +       // so let's not hoard blockers.
   1.643 +       if ("removeBlocker" in AsyncShutdown.profileBeforeChange) {
   1.644 +         // `removeBlocker` was added with bug 985655. In the interest
   1.645 +         // of backporting, let's degrade gracefully if `removeBlocker`
   1.646 +         // doesn't exist.
   1.647 +         AsyncShutdown.profileBeforeChange.removeBlocker(blocker);
   1.648 +       }
   1.649 +    }
   1.650 +  }),
   1.651 +
   1.652 +  fileExistsForURL: function Storage_fileExistsForURL(aURL) {
   1.653 +    return PageThumbsWorker.post("exists", [this.getFilePathForURL(aURL)]);
   1.654 +  },
   1.655 +
   1.656 +  isFileRecentForURL: function Storage_isFileRecentForURL(aURL) {
   1.657 +    return PageThumbsWorker.post("isFileRecent",
   1.658 +                                 [this.getFilePathForURL(aURL),
   1.659 +                                  MAX_THUMBNAIL_AGE_SECS]);
   1.660 +  },
   1.661 +
   1.662 +  _calculateMD5Hash: function Storage_calculateMD5Hash(aValue) {
   1.663 +    let hash = gCryptoHash;
   1.664 +    let value = gUnicodeConverter.convertToByteArray(aValue);
   1.665 +
   1.666 +    hash.init(hash.MD5);
   1.667 +    hash.update(value, value.length);
   1.668 +    return this._convertToHexString(hash.finish(false));
   1.669 +  },
   1.670 +
   1.671 +  _convertToHexString: function Storage_convertToHexString(aData) {
   1.672 +    let hex = "";
   1.673 +    for (let i = 0; i < aData.length; i++)
   1.674 +      hex += ("0" + aData.charCodeAt(i).toString(16)).slice(-2);
   1.675 +    return hex;
   1.676 +  },
   1.677 +
   1.678 +  /**
   1.679 +   * For functions that take a noOverwrite option, OS.File throws an error if
   1.680 +   * the target file exists and noOverwrite is true.  We don't consider that an
   1.681 +   * error, and we don't want such errors propagated.
   1.682 +   *
   1.683 +   * @param {aNoOverwrite} The noOverwrite option used in the OS.File operation.
   1.684 +   *
   1.685 +   * @return {function} A function that should be passed as the second argument
   1.686 +   * to then() (the `onError` argument).
   1.687 +   */
   1.688 +  _eatNoOverwriteError: function Storage__eatNoOverwriteError(aNoOverwrite) {
   1.689 +    return function onError(err) {
   1.690 +      if (!aNoOverwrite ||
   1.691 +          !(err instanceof OS.File.Error) ||
   1.692 +          !err.becauseExists) {
   1.693 +        throw err;
   1.694 +      }
   1.695 +    };
   1.696 +  },
   1.697 +
   1.698 +  // Deprecated, please do not use
   1.699 +  getFileForURL: function Storage_getFileForURL_DEPRECATED(aURL) {
   1.700 +    Deprecated.warning("PageThumbs.getFileForURL is deprecated. Please use PageThumbs.getFilePathForURL and OS.File",
   1.701 +                       "https://developer.mozilla.org/docs/JavaScript_OS.File");
   1.702 +    // Note: Once this method has been removed, we can get rid of the dependency towards FileUtils
   1.703 +    return new FileUtils.File(PageThumbsStorage.getFilePathForURL(aURL));
   1.704 +  }
   1.705 +};
   1.706 +
   1.707 +let PageThumbsStorageMigrator = {
   1.708 +  get currentVersion() {
   1.709 +    try {
   1.710 +      return Services.prefs.getIntPref(PREF_STORAGE_VERSION);
   1.711 +    } catch (e) {
   1.712 +      // The pref doesn't exist, yet. Return version 0.
   1.713 +      return 0;
   1.714 +    }
   1.715 +  },
   1.716 +
   1.717 +  set currentVersion(aVersion) {
   1.718 +    Services.prefs.setIntPref(PREF_STORAGE_VERSION, aVersion);
   1.719 +  },
   1.720 +
   1.721 +  migrate: function Migrator_migrate() {
   1.722 +    let version = this.currentVersion;
   1.723 +
   1.724 +    // Storage version 1 never made it to beta.
   1.725 +    // At the time of writing only Windows had (ProfD != ProfLD) and we
   1.726 +    // needed to move thumbnails from the roaming profile to the locale
   1.727 +    // one so that they're not needlessly included in backups and/or
   1.728 +    // written via SMB.
   1.729 +
   1.730 +    // Storage version 2 also never made it to beta.
   1.731 +    // The thumbnail folder structure has been changed and old thumbnails
   1.732 +    // were not migrated. Instead, we just renamed the current folder to
   1.733 +    // "<name>-old" and will remove it later.
   1.734 +
   1.735 +    if (version < 3) {
   1.736 +      this.migrateToVersion3();
   1.737 +    }
   1.738 +
   1.739 +    this.currentVersion = LATEST_STORAGE_VERSION;
   1.740 +  },
   1.741 +
   1.742 +  /**
   1.743 +   * Bug 239254 added support for having the disk cache and thumbnail
   1.744 +   * directories on a local path (i.e. ~/.cache/) under Linux. We'll first
   1.745 +   * try to move the old thumbnails to their new location. If that's not
   1.746 +   * possible (because ProfD might be on a different file system than
   1.747 +   * ProfLD) we'll just discard them.
   1.748 +   *
   1.749 +   * @param {string*} local The path to the local profile directory.
   1.750 +   * Used for testing. Default argument is good for all non-testing uses.
   1.751 +   * @param {string*} roaming The path to the roaming profile directory.
   1.752 +   * Used for testing. Default argument is good for all non-testing uses.
   1.753 +   */
   1.754 +  migrateToVersion3: function Migrator_migrateToVersion3(
   1.755 +    local = OS.Constants.Path.localProfileDir,
   1.756 +    roaming = OS.Constants.Path.profileDir) {
   1.757 +    PageThumbsWorker.post(
   1.758 +      "moveOrDeleteAllThumbnails",
   1.759 +      [OS.Path.join(roaming, THUMBNAIL_DIRECTORY),
   1.760 +       OS.Path.join(local, THUMBNAIL_DIRECTORY)]
   1.761 +    );
   1.762 +  }
   1.763 +};
   1.764 +
   1.765 +let PageThumbsExpiration = {
   1.766 +  _filters: [],
   1.767 +
   1.768 +  init: function Expiration_init() {
   1.769 +    gUpdateTimerManager.registerTimer("browser-cleanup-thumbnails", this,
   1.770 +                                      EXPIRATION_INTERVAL_SECS);
   1.771 +  },
   1.772 +
   1.773 +  addFilter: function Expiration_addFilter(aFilter) {
   1.774 +    this._filters.push(aFilter);
   1.775 +  },
   1.776 +
   1.777 +  removeFilter: function Expiration_removeFilter(aFilter) {
   1.778 +    let index = this._filters.indexOf(aFilter);
   1.779 +    if (index > -1)
   1.780 +      this._filters.splice(index, 1);
   1.781 +  },
   1.782 +
   1.783 +  notify: function Expiration_notify(aTimer) {
   1.784 +    let urls = [];
   1.785 +    let filtersToWaitFor = this._filters.length;
   1.786 +
   1.787 +    let expire = function expire() {
   1.788 +      this.expireThumbnails(urls);
   1.789 +    }.bind(this);
   1.790 +
   1.791 +    // No registered filters.
   1.792 +    if (!filtersToWaitFor) {
   1.793 +      expire();
   1.794 +      return;
   1.795 +    }
   1.796 +
   1.797 +    function filterCallback(aURLs) {
   1.798 +      urls = urls.concat(aURLs);
   1.799 +      if (--filtersToWaitFor == 0)
   1.800 +        expire();
   1.801 +    }
   1.802 +
   1.803 +    for (let filter of this._filters) {
   1.804 +      if (typeof filter == "function")
   1.805 +        filter(filterCallback)
   1.806 +      else
   1.807 +        filter.filterForThumbnailExpiration(filterCallback);
   1.808 +    }
   1.809 +  },
   1.810 +
   1.811 +  expireThumbnails: function Expiration_expireThumbnails(aURLsToKeep) {
   1.812 +    let path = this.path;
   1.813 +    let keep = [PageThumbsStorage.getLeafNameForURL(url) for (url of aURLsToKeep)];
   1.814 +    let msg = [
   1.815 +      PageThumbsStorage.path,
   1.816 +      keep,
   1.817 +      EXPIRATION_MIN_CHUNK_SIZE
   1.818 +    ];
   1.819 +
   1.820 +    return PageThumbsWorker.post(
   1.821 +      "expireFilesInDirectory",
   1.822 +      msg
   1.823 +    );
   1.824 +  }
   1.825 +};
   1.826 +
   1.827 +/**
   1.828 + * Interface to a dedicated thread handling I/O
   1.829 + */
   1.830 +
   1.831 +let PageThumbsWorker = (function() {
   1.832 +  let worker = new PromiseWorker("resource://gre/modules/PageThumbsWorker.js",
   1.833 +    OS.Shared.LOG.bind("PageThumbs"));
   1.834 +  return {
   1.835 +    post: function post(...args) {
   1.836 +      let promise = worker.post.apply(worker, args);
   1.837 +      return promise.then(
   1.838 +        null,
   1.839 +        function onError(error) {
   1.840 +          // Decode any serialized error
   1.841 +          if (error instanceof PromiseWorker.WorkerError) {
   1.842 +            throw OS.File.Error.fromMsg(error.data);
   1.843 +          } else {
   1.844 +            throw error;
   1.845 +          }
   1.846 +        }
   1.847 +      );
   1.848 +    }
   1.849 +  };
   1.850 +})();
   1.851 +
   1.852 +let PageThumbsHistoryObserver = {
   1.853 +  onDeleteURI: function Thumbnails_onDeleteURI(aURI, aGUID) {
   1.854 +    PageThumbsStorage.remove(aURI.spec);
   1.855 +  },
   1.856 +
   1.857 +  onClearHistory: function Thumbnails_onClearHistory() {
   1.858 +    PageThumbsStorage.wipe();
   1.859 +  },
   1.860 +
   1.861 +  onTitleChanged: function () {},
   1.862 +  onBeginUpdateBatch: function () {},
   1.863 +  onEndUpdateBatch: function () {},
   1.864 +  onVisit: function () {},
   1.865 +  onPageChanged: function () {},
   1.866 +  onDeleteVisits: function () {},
   1.867 +
   1.868 +  QueryInterface: XPCOMUtils.generateQI([Ci.nsINavHistoryObserver])
   1.869 +};

mercurial