toolkit/components/thumbnails/PageThumbs.jsm

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

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

mercurial