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 +};