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