toolkit/components/thumbnails/BackgroundPageThumbs.jsm

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

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

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

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
michael@0 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 4
michael@0 5 const EXPORTED_SYMBOLS = [
michael@0 6 "BackgroundPageThumbs",
michael@0 7 ];
michael@0 8
michael@0 9 const DEFAULT_CAPTURE_TIMEOUT = 30000; // ms
michael@0 10 const DESTROY_BROWSER_TIMEOUT = 60000; // ms
michael@0 11 const FRAME_SCRIPT_URL = "chrome://global/content/backgroundPageThumbsContent.js";
michael@0 12
michael@0 13 const TELEMETRY_HISTOGRAM_ID_PREFIX = "FX_THUMBNAILS_BG_";
michael@0 14
michael@0 15 // possible FX_THUMBNAILS_BG_CAPTURE_DONE_REASON_2 telemetry values
michael@0 16 const TEL_CAPTURE_DONE_OK = 0;
michael@0 17 const TEL_CAPTURE_DONE_TIMEOUT = 1;
michael@0 18 // 2 and 3 were used when we had special handling for private-browsing.
michael@0 19 const TEL_CAPTURE_DONE_CRASHED = 4;
michael@0 20
michael@0 21 const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
michael@0 22 const HTML_NS = "http://www.w3.org/1999/xhtml";
michael@0 23
michael@0 24 const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
michael@0 25
michael@0 26 Cu.import("resource://gre/modules/PageThumbs.jsm");
michael@0 27 Cu.import("resource://gre/modules/Services.jsm");
michael@0 28
michael@0 29 const BackgroundPageThumbs = {
michael@0 30
michael@0 31 /**
michael@0 32 * Asynchronously captures a thumbnail of the given URL.
michael@0 33 *
michael@0 34 * The page is loaded anonymously, and plug-ins are disabled.
michael@0 35 *
michael@0 36 * @param url The URL to capture.
michael@0 37 * @param options An optional object that configures the capture. Its
michael@0 38 * properties are the following, and all are optional:
michael@0 39 * @opt onDone A function that will be asynchronously called when the
michael@0 40 * capture is complete or times out. It's called as
michael@0 41 * onDone(url),
michael@0 42 * where `url` is the captured URL.
michael@0 43 * @opt timeout The capture will time out after this many milliseconds have
michael@0 44 * elapsed after the capture has progressed to the head of
michael@0 45 * the queue and started. Defaults to 30000 (30 seconds).
michael@0 46 */
michael@0 47 capture: function (url, options={}) {
michael@0 48 if (!PageThumbs._prefEnabled()) {
michael@0 49 if (options.onDone)
michael@0 50 schedule(() => options.onDone(url));
michael@0 51 return;
michael@0 52 }
michael@0 53 this._captureQueue = this._captureQueue || [];
michael@0 54 this._capturesByURL = this._capturesByURL || new Map();
michael@0 55
michael@0 56 tel("QUEUE_SIZE_ON_CAPTURE", this._captureQueue.length);
michael@0 57
michael@0 58 // We want to avoid duplicate captures for the same URL. If there is an
michael@0 59 // existing one, we just add the callback to that one and we are done.
michael@0 60 let existing = this._capturesByURL.get(url);
michael@0 61 if (existing) {
michael@0 62 if (options.onDone)
michael@0 63 existing.doneCallbacks.push(options.onDone);
michael@0 64 // The queue is already being processed, so nothing else to do...
michael@0 65 return;
michael@0 66 }
michael@0 67 let cap = new Capture(url, this._onCaptureOrTimeout.bind(this), options);
michael@0 68 this._captureQueue.push(cap);
michael@0 69 this._capturesByURL.set(url, cap);
michael@0 70 this._processCaptureQueue();
michael@0 71 },
michael@0 72
michael@0 73 /**
michael@0 74 * Asynchronously captures a thumbnail of the given URL if one does not
michael@0 75 * already exist. Otherwise does nothing.
michael@0 76 *
michael@0 77 * @param url The URL to capture.
michael@0 78 * @param options An optional object that configures the capture. See
michael@0 79 * capture() for description.
michael@0 80 */
michael@0 81 captureIfMissing: function (url, options={}) {
michael@0 82 // The fileExistsForURL call is an optimization, potentially but unlikely
michael@0 83 // incorrect, and no big deal when it is. After the capture is done, we
michael@0 84 // atomically test whether the file exists before writing it.
michael@0 85 PageThumbsStorage.fileExistsForURL(url).then(exists => {
michael@0 86 if (exists.ok) {
michael@0 87 if (options.onDone)
michael@0 88 options.onDone(url);
michael@0 89 return;
michael@0 90 }
michael@0 91 this.capture(url, options);
michael@0 92 }, err => {
michael@0 93 if (options.onDone)
michael@0 94 options.onDone(url);
michael@0 95 });
michael@0 96 },
michael@0 97
michael@0 98 /**
michael@0 99 * Ensures that initialization of the thumbnail browser's parent window has
michael@0 100 * begun.
michael@0 101 *
michael@0 102 * @return True if the parent window is completely initialized and can be
michael@0 103 * used, and false if initialization has started but not completed.
michael@0 104 */
michael@0 105 _ensureParentWindowReady: function () {
michael@0 106 if (this._parentWin)
michael@0 107 // Already fully initialized.
michael@0 108 return true;
michael@0 109 if (this._startedParentWinInit)
michael@0 110 // Already started initializing.
michael@0 111 return false;
michael@0 112
michael@0 113 this._startedParentWinInit = true;
michael@0 114
michael@0 115 // Create an html:iframe, stick it in the parent document, and
michael@0 116 // use it to host the browser. about:blank will not have the system
michael@0 117 // principal, so it can't host, but a document with a chrome URI will.
michael@0 118 let hostWindow = Services.appShell.hiddenDOMWindow;
michael@0 119 let iframe = hostWindow.document.createElementNS(HTML_NS, "iframe");
michael@0 120 iframe.setAttribute("src", "chrome://global/content/mozilla.xhtml");
michael@0 121 let onLoad = function onLoadFn() {
michael@0 122 iframe.removeEventListener("load", onLoad, true);
michael@0 123 this._parentWin = iframe.contentWindow;
michael@0 124 this._processCaptureQueue();
michael@0 125 }.bind(this);
michael@0 126 iframe.addEventListener("load", onLoad, true);
michael@0 127 hostWindow.document.documentElement.appendChild(iframe);
michael@0 128 this._hostIframe = iframe;
michael@0 129
michael@0 130 return false;
michael@0 131 },
michael@0 132
michael@0 133 /**
michael@0 134 * Destroys the service. Queued and pending captures will never complete, and
michael@0 135 * their consumer callbacks will never be called.
michael@0 136 */
michael@0 137 _destroy: function () {
michael@0 138 if (this._captureQueue)
michael@0 139 this._captureQueue.forEach(cap => cap.destroy());
michael@0 140 this._destroyBrowser();
michael@0 141 if (this._hostIframe)
michael@0 142 this._hostIframe.remove();
michael@0 143 delete this._captureQueue;
michael@0 144 delete this._hostIframe;
michael@0 145 delete this._startedParentWinInit;
michael@0 146 delete this._parentWin;
michael@0 147 },
michael@0 148
michael@0 149 /**
michael@0 150 * Creates the thumbnail browser if it doesn't already exist.
michael@0 151 */
michael@0 152 _ensureBrowser: function () {
michael@0 153 if (this._thumbBrowser)
michael@0 154 return;
michael@0 155
michael@0 156 let browser = this._parentWin.document.createElementNS(XUL_NS, "browser");
michael@0 157 browser.setAttribute("type", "content");
michael@0 158 browser.setAttribute("remote", "true");
michael@0 159
michael@0 160 // Size the browser. Make its aspect ratio the same as the canvases' that
michael@0 161 // the thumbnails are drawn into; the canvases' aspect ratio is the same as
michael@0 162 // the screen's, so use that. Aim for a size in the ballpark of 1024x768.
michael@0 163 let [swidth, sheight] = [{}, {}];
michael@0 164 Cc["@mozilla.org/gfx/screenmanager;1"].
michael@0 165 getService(Ci.nsIScreenManager).
michael@0 166 primaryScreen.
michael@0 167 GetRectDisplayPix({}, {}, swidth, sheight);
michael@0 168 let bwidth = Math.min(1024, swidth.value);
michael@0 169 // Setting the width and height attributes doesn't work -- the resulting
michael@0 170 // thumbnails are blank and transparent -- but setting the style does.
michael@0 171 browser.style.width = bwidth + "px";
michael@0 172 browser.style.height = (bwidth * sheight.value / swidth.value) + "px";
michael@0 173
michael@0 174 this._parentWin.document.documentElement.appendChild(browser);
michael@0 175
michael@0 176 // an event that is sent if the remote process crashes - no need to remove
michael@0 177 // it as we want it to be there as long as the browser itself lives.
michael@0 178 browser.addEventListener("oop-browser-crashed", () => {
michael@0 179 Cu.reportError("BackgroundThumbnails remote process crashed - recovering");
michael@0 180 this._destroyBrowser();
michael@0 181 let curCapture = this._captureQueue.length ? this._captureQueue[0] : null;
michael@0 182 // we could retry the pending capture, but it's possible the crash
michael@0 183 // was due directly to it, so trying again might just crash again.
michael@0 184 // We could keep a flag to indicate if it previously crashed, but
michael@0 185 // "resetting" the capture requires more work - so for now, we just
michael@0 186 // discard it.
michael@0 187 if (curCapture && curCapture.pending) {
michael@0 188 curCapture._done(null, TEL_CAPTURE_DONE_CRASHED);
michael@0 189 // _done automatically continues queue processing.
michael@0 190 }
michael@0 191 // else: we must have been idle and not currently doing a capture (eg,
michael@0 192 // maybe a GC or similar crashed) - so there's no need to attempt a
michael@0 193 // queue restart - the next capture request will set everything up.
michael@0 194 });
michael@0 195
michael@0 196 browser.messageManager.loadFrameScript(FRAME_SCRIPT_URL, false);
michael@0 197 this._thumbBrowser = browser;
michael@0 198 },
michael@0 199
michael@0 200 _destroyBrowser: function () {
michael@0 201 if (!this._thumbBrowser)
michael@0 202 return;
michael@0 203 this._thumbBrowser.remove();
michael@0 204 delete this._thumbBrowser;
michael@0 205 },
michael@0 206
michael@0 207 /**
michael@0 208 * Starts the next capture if the queue is not empty and the service is fully
michael@0 209 * initialized.
michael@0 210 */
michael@0 211 _processCaptureQueue: function () {
michael@0 212 if (!this._captureQueue.length ||
michael@0 213 this._captureQueue[0].pending ||
michael@0 214 !this._ensureParentWindowReady())
michael@0 215 return;
michael@0 216
michael@0 217 // Ready to start the first capture in the queue.
michael@0 218 this._ensureBrowser();
michael@0 219 this._captureQueue[0].start(this._thumbBrowser.messageManager);
michael@0 220 if (this._destroyBrowserTimer) {
michael@0 221 this._destroyBrowserTimer.cancel();
michael@0 222 delete this._destroyBrowserTimer;
michael@0 223 }
michael@0 224 },
michael@0 225
michael@0 226 /**
michael@0 227 * Called when the current capture completes or fails (eg, times out, remote
michael@0 228 * process crashes.)
michael@0 229 */
michael@0 230 _onCaptureOrTimeout: function (capture) {
michael@0 231 // Since timeouts start as an item is being processed, only the first
michael@0 232 // item in the queue can be passed to this method.
michael@0 233 if (capture !== this._captureQueue[0])
michael@0 234 throw new Error("The capture should be at the head of the queue.");
michael@0 235 this._captureQueue.shift();
michael@0 236 this._capturesByURL.delete(capture.url);
michael@0 237
michael@0 238 // Start the destroy-browser timer *before* processing the capture queue.
michael@0 239 let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
michael@0 240 timer.initWithCallback(this._destroyBrowser.bind(this),
michael@0 241 this._destroyBrowserTimeout,
michael@0 242 Ci.nsITimer.TYPE_ONE_SHOT);
michael@0 243 this._destroyBrowserTimer = timer;
michael@0 244
michael@0 245 this._processCaptureQueue();
michael@0 246 },
michael@0 247
michael@0 248 _destroyBrowserTimeout: DESTROY_BROWSER_TIMEOUT,
michael@0 249 };
michael@0 250
michael@0 251 /**
michael@0 252 * Represents a single capture request in the capture queue.
michael@0 253 *
michael@0 254 * @param url The URL to capture.
michael@0 255 * @param captureCallback A function you want called when the capture
michael@0 256 * completes.
michael@0 257 * @param options The capture options.
michael@0 258 */
michael@0 259 function Capture(url, captureCallback, options) {
michael@0 260 this.url = url;
michael@0 261 this.captureCallback = captureCallback;
michael@0 262 this.options = options;
michael@0 263 this.id = Capture.nextID++;
michael@0 264 this.creationDate = new Date();
michael@0 265 this.doneCallbacks = [];
michael@0 266 if (options.onDone)
michael@0 267 this.doneCallbacks.push(options.onDone);
michael@0 268 }
michael@0 269
michael@0 270 Capture.prototype = {
michael@0 271
michael@0 272 get pending() {
michael@0 273 return !!this._msgMan;
michael@0 274 },
michael@0 275
michael@0 276 /**
michael@0 277 * Sends a message to the content script to start the capture.
michael@0 278 *
michael@0 279 * @param messageManager The nsIMessageSender of the thumbnail browser.
michael@0 280 */
michael@0 281 start: function (messageManager) {
michael@0 282 this.startDate = new Date();
michael@0 283 tel("CAPTURE_QUEUE_TIME_MS", this.startDate - this.creationDate);
michael@0 284
michael@0 285 // timeout timer
michael@0 286 let timeout = typeof(this.options.timeout) == "number" ?
michael@0 287 this.options.timeout :
michael@0 288 DEFAULT_CAPTURE_TIMEOUT;
michael@0 289 this._timeoutTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
michael@0 290 this._timeoutTimer.initWithCallback(this, timeout,
michael@0 291 Ci.nsITimer.TYPE_ONE_SHOT);
michael@0 292
michael@0 293 // didCapture registration
michael@0 294 this._msgMan = messageManager;
michael@0 295 this._msgMan.sendAsyncMessage("BackgroundPageThumbs:capture",
michael@0 296 { id: this.id, url: this.url });
michael@0 297 this._msgMan.addMessageListener("BackgroundPageThumbs:didCapture", this);
michael@0 298 },
michael@0 299
michael@0 300 /**
michael@0 301 * The only intended external use of this method is by the service when it's
michael@0 302 * uninitializing and doing things like destroying the thumbnail browser. In
michael@0 303 * that case the consumer's completion callback will never be called.
michael@0 304 */
michael@0 305 destroy: function () {
michael@0 306 // This method may be called for captures that haven't started yet, so
michael@0 307 // guard against not yet having _timeoutTimer, _msgMan etc properties...
michael@0 308 if (this._timeoutTimer) {
michael@0 309 this._timeoutTimer.cancel();
michael@0 310 delete this._timeoutTimer;
michael@0 311 }
michael@0 312 if (this._msgMan) {
michael@0 313 this._msgMan.removeMessageListener("BackgroundPageThumbs:didCapture",
michael@0 314 this);
michael@0 315 delete this._msgMan;
michael@0 316 }
michael@0 317 delete this.captureCallback;
michael@0 318 delete this.doneCallbacks;
michael@0 319 delete this.options;
michael@0 320 },
michael@0 321
michael@0 322 // Called when the didCapture message is received.
michael@0 323 receiveMessage: function (msg) {
michael@0 324 tel("CAPTURE_SERVICE_TIME_MS", new Date() - this.startDate);
michael@0 325
michael@0 326 // A different timed-out capture may have finally successfully completed, so
michael@0 327 // discard messages that aren't meant for this capture.
michael@0 328 if (msg.json.id == this.id)
michael@0 329 this._done(msg.json, TEL_CAPTURE_DONE_OK);
michael@0 330 },
michael@0 331
michael@0 332 // Called when the timeout timer fires.
michael@0 333 notify: function () {
michael@0 334 this._done(null, TEL_CAPTURE_DONE_TIMEOUT);
michael@0 335 },
michael@0 336
michael@0 337 _done: function (data, reason) {
michael@0 338 // Note that _done will be called only once, by either receiveMessage or
michael@0 339 // notify, since it calls destroy here, which cancels the timeout timer and
michael@0 340 // removes the didCapture message listener.
michael@0 341 let { captureCallback, doneCallbacks, options } = this;
michael@0 342 this.destroy();
michael@0 343
michael@0 344 if (typeof(reason) != "number")
michael@0 345 throw new Error("A done reason must be given.");
michael@0 346 tel("CAPTURE_DONE_REASON_2", reason);
michael@0 347 if (data && data.telemetry) {
michael@0 348 // Telemetry is currently disabled in the content process (bug 680508).
michael@0 349 for (let id in data.telemetry) {
michael@0 350 tel(id, data.telemetry[id]);
michael@0 351 }
michael@0 352 }
michael@0 353
michael@0 354 let done = () => {
michael@0 355 captureCallback(this);
michael@0 356 for (let callback of doneCallbacks) {
michael@0 357 try {
michael@0 358 callback.call(options, this.url);
michael@0 359 }
michael@0 360 catch (err) {
michael@0 361 Cu.reportError(err);
michael@0 362 }
michael@0 363 }
michael@0 364 };
michael@0 365
michael@0 366 if (!data) {
michael@0 367 done();
michael@0 368 return;
michael@0 369 }
michael@0 370
michael@0 371 PageThumbs._store(this.url, data.finalURL, data.imageData, true)
michael@0 372 .then(done, done);
michael@0 373 },
michael@0 374 };
michael@0 375
michael@0 376 Capture.nextID = 0;
michael@0 377
michael@0 378 /**
michael@0 379 * Adds a value to one of this module's telemetry histograms.
michael@0 380 *
michael@0 381 * @param histogramID This is prefixed with this module's ID.
michael@0 382 * @param value The value to add.
michael@0 383 */
michael@0 384 function tel(histogramID, value) {
michael@0 385 let id = TELEMETRY_HISTOGRAM_ID_PREFIX + histogramID;
michael@0 386 Services.telemetry.getHistogramById(id).add(value);
michael@0 387 }
michael@0 388
michael@0 389 function schedule(callback) {
michael@0 390 Services.tm.mainThread.dispatch(callback, Ci.nsIThread.DISPATCH_NORMAL);
michael@0 391 }

mercurial