Wed, 31 Dec 2014 06:09:35 +0100
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 |
michael@0 | 3 | * file, 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 | function debug(msg) { |
michael@0 | 8 | Services.console.logStringMessage("SessionStoreContent: " + msg); |
michael@0 | 9 | } |
michael@0 | 10 | |
michael@0 | 11 | let Cu = Components.utils; |
michael@0 | 12 | let Cc = Components.classes; |
michael@0 | 13 | let Ci = Components.interfaces; |
michael@0 | 14 | let Cr = Components.results; |
michael@0 | 15 | |
michael@0 | 16 | Cu.import("resource://gre/modules/XPCOMUtils.jsm", this); |
michael@0 | 17 | Cu.import("resource://gre/modules/Timer.jsm", this); |
michael@0 | 18 | |
michael@0 | 19 | XPCOMUtils.defineLazyModuleGetter(this, "DocShellCapabilities", |
michael@0 | 20 | "resource:///modules/sessionstore/DocShellCapabilities.jsm"); |
michael@0 | 21 | XPCOMUtils.defineLazyModuleGetter(this, "FormData", |
michael@0 | 22 | "resource://gre/modules/FormData.jsm"); |
michael@0 | 23 | XPCOMUtils.defineLazyModuleGetter(this, "PageStyle", |
michael@0 | 24 | "resource:///modules/sessionstore/PageStyle.jsm"); |
michael@0 | 25 | XPCOMUtils.defineLazyModuleGetter(this, "ScrollPosition", |
michael@0 | 26 | "resource://gre/modules/ScrollPosition.jsm"); |
michael@0 | 27 | XPCOMUtils.defineLazyModuleGetter(this, "SessionHistory", |
michael@0 | 28 | "resource:///modules/sessionstore/SessionHistory.jsm"); |
michael@0 | 29 | XPCOMUtils.defineLazyModuleGetter(this, "SessionStorage", |
michael@0 | 30 | "resource:///modules/sessionstore/SessionStorage.jsm"); |
michael@0 | 31 | |
michael@0 | 32 | Cu.import("resource:///modules/sessionstore/FrameTree.jsm", this); |
michael@0 | 33 | let gFrameTree = new FrameTree(this); |
michael@0 | 34 | |
michael@0 | 35 | Cu.import("resource:///modules/sessionstore/ContentRestore.jsm", this); |
michael@0 | 36 | XPCOMUtils.defineLazyGetter(this, 'gContentRestore', |
michael@0 | 37 | () => { return new ContentRestore(this) }); |
michael@0 | 38 | |
michael@0 | 39 | /** |
michael@0 | 40 | * Returns a lazy function that will evaluate the given |
michael@0 | 41 | * function |fn| only once and cache its return value. |
michael@0 | 42 | */ |
michael@0 | 43 | function createLazy(fn) { |
michael@0 | 44 | let cached = false; |
michael@0 | 45 | let cachedValue = null; |
michael@0 | 46 | |
michael@0 | 47 | return function lazy() { |
michael@0 | 48 | if (!cached) { |
michael@0 | 49 | cachedValue = fn(); |
michael@0 | 50 | cached = true; |
michael@0 | 51 | } |
michael@0 | 52 | |
michael@0 | 53 | return cachedValue; |
michael@0 | 54 | }; |
michael@0 | 55 | } |
michael@0 | 56 | |
michael@0 | 57 | /** |
michael@0 | 58 | * Determines whether the given storage event was triggered by changes |
michael@0 | 59 | * to the sessionStorage object and not the local or globalStorage. |
michael@0 | 60 | */ |
michael@0 | 61 | function isSessionStorageEvent(event) { |
michael@0 | 62 | try { |
michael@0 | 63 | return event.storageArea == content.sessionStorage; |
michael@0 | 64 | } catch (ex if ex instanceof Ci.nsIException && ex.result == Cr.NS_ERROR_NOT_AVAILABLE) { |
michael@0 | 65 | // This page does not have a DOMSessionStorage |
michael@0 | 66 | // (this is typically the case for about: pages) |
michael@0 | 67 | return false; |
michael@0 | 68 | } |
michael@0 | 69 | } |
michael@0 | 70 | |
michael@0 | 71 | /** |
michael@0 | 72 | * Listens for and handles content events that we need for the |
michael@0 | 73 | * session store service to be notified of state changes in content. |
michael@0 | 74 | */ |
michael@0 | 75 | let EventListener = { |
michael@0 | 76 | |
michael@0 | 77 | init: function () { |
michael@0 | 78 | addEventListener("load", this, true); |
michael@0 | 79 | }, |
michael@0 | 80 | |
michael@0 | 81 | handleEvent: function (event) { |
michael@0 | 82 | // Ignore load events from subframes. |
michael@0 | 83 | if (event.target != content.document) { |
michael@0 | 84 | return; |
michael@0 | 85 | } |
michael@0 | 86 | |
michael@0 | 87 | // If we're in the process of restoring, this load may signal |
michael@0 | 88 | // the end of the restoration. |
michael@0 | 89 | let epoch = gContentRestore.getRestoreEpoch(); |
michael@0 | 90 | if (!epoch) { |
michael@0 | 91 | return; |
michael@0 | 92 | } |
michael@0 | 93 | |
michael@0 | 94 | // Restore the form data and scroll position. |
michael@0 | 95 | gContentRestore.restoreDocument(); |
michael@0 | 96 | |
michael@0 | 97 | // Ask SessionStore.jsm to trigger SSTabRestored. |
michael@0 | 98 | sendAsyncMessage("SessionStore:restoreDocumentComplete", {epoch: epoch}); |
michael@0 | 99 | } |
michael@0 | 100 | }; |
michael@0 | 101 | |
michael@0 | 102 | /** |
michael@0 | 103 | * Listens for and handles messages sent by the session store service. |
michael@0 | 104 | */ |
michael@0 | 105 | let MessageListener = { |
michael@0 | 106 | |
michael@0 | 107 | MESSAGES: [ |
michael@0 | 108 | "SessionStore:restoreHistory", |
michael@0 | 109 | "SessionStore:restoreTabContent", |
michael@0 | 110 | "SessionStore:resetRestore", |
michael@0 | 111 | ], |
michael@0 | 112 | |
michael@0 | 113 | init: function () { |
michael@0 | 114 | this.MESSAGES.forEach(m => addMessageListener(m, this)); |
michael@0 | 115 | }, |
michael@0 | 116 | |
michael@0 | 117 | receiveMessage: function ({name, data}) { |
michael@0 | 118 | switch (name) { |
michael@0 | 119 | case "SessionStore:restoreHistory": |
michael@0 | 120 | let reloadCallback = () => { |
michael@0 | 121 | // Inform SessionStore.jsm about the reload. It will send |
michael@0 | 122 | // restoreTabContent in response. |
michael@0 | 123 | sendAsyncMessage("SessionStore:reloadPendingTab", {epoch: data.epoch}); |
michael@0 | 124 | }; |
michael@0 | 125 | gContentRestore.restoreHistory(data.epoch, data.tabData, reloadCallback); |
michael@0 | 126 | |
michael@0 | 127 | // When restoreHistory finishes, we send a synchronous message to |
michael@0 | 128 | // SessionStore.jsm so that it can run SSTabRestoring. Users of |
michael@0 | 129 | // SSTabRestoring seem to get confused if chrome and content are out of |
michael@0 | 130 | // sync about the state of the restore (particularly regarding |
michael@0 | 131 | // docShell.currentURI). Using a synchronous message is the easiest way |
michael@0 | 132 | // to temporarily synchronize them. |
michael@0 | 133 | sendSyncMessage("SessionStore:restoreHistoryComplete", {epoch: data.epoch}); |
michael@0 | 134 | break; |
michael@0 | 135 | case "SessionStore:restoreTabContent": |
michael@0 | 136 | let epoch = gContentRestore.getRestoreEpoch(); |
michael@0 | 137 | let finishCallback = () => { |
michael@0 | 138 | // Tell SessionStore.jsm that it may want to restore some more tabs, |
michael@0 | 139 | // since it restores a max of MAX_CONCURRENT_TAB_RESTORES at a time. |
michael@0 | 140 | sendAsyncMessage("SessionStore:restoreTabContentComplete", {epoch: epoch}); |
michael@0 | 141 | }; |
michael@0 | 142 | |
michael@0 | 143 | // We need to pass the value of didStartLoad back to SessionStore.jsm. |
michael@0 | 144 | let didStartLoad = gContentRestore.restoreTabContent(finishCallback); |
michael@0 | 145 | |
michael@0 | 146 | sendAsyncMessage("SessionStore:restoreTabContentStarted", {epoch: epoch}); |
michael@0 | 147 | |
michael@0 | 148 | if (!didStartLoad) { |
michael@0 | 149 | // Pretend that the load succeeded so that event handlers fire correctly. |
michael@0 | 150 | sendAsyncMessage("SessionStore:restoreTabContentComplete", {epoch: epoch}); |
michael@0 | 151 | sendAsyncMessage("SessionStore:restoreDocumentComplete", {epoch: epoch}); |
michael@0 | 152 | } |
michael@0 | 153 | break; |
michael@0 | 154 | case "SessionStore:resetRestore": |
michael@0 | 155 | gContentRestore.resetRestore(); |
michael@0 | 156 | break; |
michael@0 | 157 | default: |
michael@0 | 158 | debug("received unknown message '" + name + "'"); |
michael@0 | 159 | break; |
michael@0 | 160 | } |
michael@0 | 161 | } |
michael@0 | 162 | }; |
michael@0 | 163 | |
michael@0 | 164 | /** |
michael@0 | 165 | * On initialization, this handler gets sent to the parent process as a CPOW. |
michael@0 | 166 | * The parent will use it only to flush pending data from the frame script |
michael@0 | 167 | * when needed, i.e. when closing a tab, closing a window, shutting down, etc. |
michael@0 | 168 | * |
michael@0 | 169 | * This will hopefully not be needed in the future once we have async APIs for |
michael@0 | 170 | * closing windows and tabs. |
michael@0 | 171 | */ |
michael@0 | 172 | let SyncHandler = { |
michael@0 | 173 | init: function () { |
michael@0 | 174 | // Send this object as a CPOW to chrome. In single-process mode, |
michael@0 | 175 | // the synchronous send ensures that the handler object is |
michael@0 | 176 | // available in SessionStore.jsm immediately upon loading |
michael@0 | 177 | // content-sessionStore.js. |
michael@0 | 178 | sendSyncMessage("SessionStore:setupSyncHandler", {}, {handler: this}); |
michael@0 | 179 | }, |
michael@0 | 180 | |
michael@0 | 181 | /** |
michael@0 | 182 | * This function is used to make the tab process flush all data that |
michael@0 | 183 | * hasn't been sent to the parent process, yet. |
michael@0 | 184 | * |
michael@0 | 185 | * @param id (int) |
michael@0 | 186 | * A unique id that represents the last message received by the chrome |
michael@0 | 187 | * process before flushing. We will use this to determine data that |
michael@0 | 188 | * would be lost when data has been sent asynchronously shortly |
michael@0 | 189 | * before flushing synchronously. |
michael@0 | 190 | */ |
michael@0 | 191 | flush: function (id) { |
michael@0 | 192 | MessageQueue.flush(id); |
michael@0 | 193 | }, |
michael@0 | 194 | |
michael@0 | 195 | /** |
michael@0 | 196 | * DO NOT USE - DEBUGGING / TESTING ONLY |
michael@0 | 197 | * |
michael@0 | 198 | * This function is used to simulate certain situations where race conditions |
michael@0 | 199 | * can occur by sending data shortly before flushing synchronously. |
michael@0 | 200 | */ |
michael@0 | 201 | flushAsync: function () { |
michael@0 | 202 | MessageQueue.flushAsync(); |
michael@0 | 203 | } |
michael@0 | 204 | }; |
michael@0 | 205 | |
michael@0 | 206 | /** |
michael@0 | 207 | * Listens for changes to the session history. Whenever the user navigates |
michael@0 | 208 | * we will collect URLs and everything belonging to session history. |
michael@0 | 209 | * |
michael@0 | 210 | * Causes a SessionStore:update message to be sent that contains the current |
michael@0 | 211 | * session history. |
michael@0 | 212 | * |
michael@0 | 213 | * Example: |
michael@0 | 214 | * {entries: [{url: "about:mozilla", ...}, ...], index: 1} |
michael@0 | 215 | */ |
michael@0 | 216 | let SessionHistoryListener = { |
michael@0 | 217 | init: function () { |
michael@0 | 218 | // The frame tree observer is needed to handle navigating away from |
michael@0 | 219 | // an about page. Currently nsISHistoryListener does not have |
michael@0 | 220 | // OnHistoryNewEntry() called for about pages because the history entry is |
michael@0 | 221 | // modified to point at the new page. Once Bug 981900 lands the frame tree |
michael@0 | 222 | // observer can be removed. |
michael@0 | 223 | gFrameTree.addObserver(this); |
michael@0 | 224 | |
michael@0 | 225 | // By adding the SHistoryListener immediately, we will unfortunately be |
michael@0 | 226 | // notified of every history entry as the tab is restored. We don't bother |
michael@0 | 227 | // waiting to add the listener later because these notifications are cheap. |
michael@0 | 228 | // We will likely only collect once since we are batching collection on |
michael@0 | 229 | // a delay. |
michael@0 | 230 | docShell.QueryInterface(Ci.nsIWebNavigation).sessionHistory. |
michael@0 | 231 | addSHistoryListener(this); |
michael@0 | 232 | |
michael@0 | 233 | // Collect data if we start with a non-empty shistory. |
michael@0 | 234 | if (!SessionHistory.isEmpty(docShell)) { |
michael@0 | 235 | this.collect(); |
michael@0 | 236 | } |
michael@0 | 237 | }, |
michael@0 | 238 | |
michael@0 | 239 | uninit: function () { |
michael@0 | 240 | let sessionHistory = docShell.QueryInterface(Ci.nsIWebNavigation).sessionHistory; |
michael@0 | 241 | if (sessionHistory) { |
michael@0 | 242 | sessionHistory.removeSHistoryListener(this); |
michael@0 | 243 | } |
michael@0 | 244 | }, |
michael@0 | 245 | |
michael@0 | 246 | collect: function () { |
michael@0 | 247 | if (docShell) { |
michael@0 | 248 | MessageQueue.push("history", () => SessionHistory.collect(docShell)); |
michael@0 | 249 | } |
michael@0 | 250 | }, |
michael@0 | 251 | |
michael@0 | 252 | onFrameTreeCollected: function () { |
michael@0 | 253 | this.collect(); |
michael@0 | 254 | }, |
michael@0 | 255 | |
michael@0 | 256 | onFrameTreeReset: function () { |
michael@0 | 257 | this.collect(); |
michael@0 | 258 | }, |
michael@0 | 259 | |
michael@0 | 260 | OnHistoryNewEntry: function (newURI) { |
michael@0 | 261 | this.collect(); |
michael@0 | 262 | }, |
michael@0 | 263 | |
michael@0 | 264 | OnHistoryGoBack: function (backURI) { |
michael@0 | 265 | this.collect(); |
michael@0 | 266 | return true; |
michael@0 | 267 | }, |
michael@0 | 268 | |
michael@0 | 269 | OnHistoryGoForward: function (forwardURI) { |
michael@0 | 270 | this.collect(); |
michael@0 | 271 | return true; |
michael@0 | 272 | }, |
michael@0 | 273 | |
michael@0 | 274 | OnHistoryGotoIndex: function (index, gotoURI) { |
michael@0 | 275 | this.collect(); |
michael@0 | 276 | return true; |
michael@0 | 277 | }, |
michael@0 | 278 | |
michael@0 | 279 | OnHistoryPurge: function (numEntries) { |
michael@0 | 280 | this.collect(); |
michael@0 | 281 | return true; |
michael@0 | 282 | }, |
michael@0 | 283 | |
michael@0 | 284 | OnHistoryReload: function (reloadURI, reloadFlags) { |
michael@0 | 285 | this.collect(); |
michael@0 | 286 | return true; |
michael@0 | 287 | }, |
michael@0 | 288 | |
michael@0 | 289 | OnHistoryReplaceEntry: function (index) { |
michael@0 | 290 | this.collect(); |
michael@0 | 291 | }, |
michael@0 | 292 | |
michael@0 | 293 | QueryInterface: XPCOMUtils.generateQI([ |
michael@0 | 294 | Ci.nsISHistoryListener, |
michael@0 | 295 | Ci.nsISupportsWeakReference |
michael@0 | 296 | ]) |
michael@0 | 297 | }; |
michael@0 | 298 | |
michael@0 | 299 | /** |
michael@0 | 300 | * Listens for scroll position changes. Whenever the user scrolls the top-most |
michael@0 | 301 | * frame we update the scroll position and will restore it when requested. |
michael@0 | 302 | * |
michael@0 | 303 | * Causes a SessionStore:update message to be sent that contains the current |
michael@0 | 304 | * scroll positions as a tree of strings. If no frame of the whole frame tree |
michael@0 | 305 | * is scrolled this will return null so that we don't tack a property onto |
michael@0 | 306 | * the tabData object in the parent process. |
michael@0 | 307 | * |
michael@0 | 308 | * Example: |
michael@0 | 309 | * {scroll: "100,100", children: [null, null, {scroll: "200,200"}]} |
michael@0 | 310 | */ |
michael@0 | 311 | let ScrollPositionListener = { |
michael@0 | 312 | init: function () { |
michael@0 | 313 | addEventListener("scroll", this); |
michael@0 | 314 | gFrameTree.addObserver(this); |
michael@0 | 315 | }, |
michael@0 | 316 | |
michael@0 | 317 | handleEvent: function (event) { |
michael@0 | 318 | let frame = event.target && event.target.defaultView; |
michael@0 | 319 | |
michael@0 | 320 | // Don't collect scroll data for frames created at or after the load event |
michael@0 | 321 | // as SessionStore can't restore scroll data for those. |
michael@0 | 322 | if (frame && gFrameTree.contains(frame)) { |
michael@0 | 323 | MessageQueue.push("scroll", () => this.collect()); |
michael@0 | 324 | } |
michael@0 | 325 | }, |
michael@0 | 326 | |
michael@0 | 327 | onFrameTreeCollected: function () { |
michael@0 | 328 | MessageQueue.push("scroll", () => this.collect()); |
michael@0 | 329 | }, |
michael@0 | 330 | |
michael@0 | 331 | onFrameTreeReset: function () { |
michael@0 | 332 | MessageQueue.push("scroll", () => null); |
michael@0 | 333 | }, |
michael@0 | 334 | |
michael@0 | 335 | collect: function () { |
michael@0 | 336 | return gFrameTree.map(ScrollPosition.collect); |
michael@0 | 337 | } |
michael@0 | 338 | }; |
michael@0 | 339 | |
michael@0 | 340 | /** |
michael@0 | 341 | * Listens for changes to input elements. Whenever the value of an input |
michael@0 | 342 | * element changes we will re-collect data for the current frame tree and send |
michael@0 | 343 | * a message to the parent process. |
michael@0 | 344 | * |
michael@0 | 345 | * Causes a SessionStore:update message to be sent that contains the form data |
michael@0 | 346 | * for all reachable frames. |
michael@0 | 347 | * |
michael@0 | 348 | * Example: |
michael@0 | 349 | * { |
michael@0 | 350 | * formdata: {url: "http://mozilla.org/", id: {input_id: "input value"}}, |
michael@0 | 351 | * children: [ |
michael@0 | 352 | * null, |
michael@0 | 353 | * {url: "http://sub.mozilla.org/", id: {input_id: "input value 2"}} |
michael@0 | 354 | * ] |
michael@0 | 355 | * } |
michael@0 | 356 | */ |
michael@0 | 357 | let FormDataListener = { |
michael@0 | 358 | init: function () { |
michael@0 | 359 | addEventListener("input", this, true); |
michael@0 | 360 | addEventListener("change", this, true); |
michael@0 | 361 | gFrameTree.addObserver(this); |
michael@0 | 362 | }, |
michael@0 | 363 | |
michael@0 | 364 | handleEvent: function (event) { |
michael@0 | 365 | let frame = event.target && |
michael@0 | 366 | event.target.ownerDocument && |
michael@0 | 367 | event.target.ownerDocument.defaultView; |
michael@0 | 368 | |
michael@0 | 369 | // Don't collect form data for frames created at or after the load event |
michael@0 | 370 | // as SessionStore can't restore form data for those. |
michael@0 | 371 | if (frame && gFrameTree.contains(frame)) { |
michael@0 | 372 | MessageQueue.push("formdata", () => this.collect()); |
michael@0 | 373 | } |
michael@0 | 374 | }, |
michael@0 | 375 | |
michael@0 | 376 | onFrameTreeReset: function () { |
michael@0 | 377 | MessageQueue.push("formdata", () => null); |
michael@0 | 378 | }, |
michael@0 | 379 | |
michael@0 | 380 | collect: function () { |
michael@0 | 381 | return gFrameTree.map(FormData.collect); |
michael@0 | 382 | } |
michael@0 | 383 | }; |
michael@0 | 384 | |
michael@0 | 385 | /** |
michael@0 | 386 | * Listens for changes to the page style. Whenever a different page style is |
michael@0 | 387 | * selected or author styles are enabled/disabled we send a message with the |
michael@0 | 388 | * currently applied style to the chrome process. |
michael@0 | 389 | * |
michael@0 | 390 | * Causes a SessionStore:update message to be sent that contains the currently |
michael@0 | 391 | * selected pageStyle for all reachable frames. |
michael@0 | 392 | * |
michael@0 | 393 | * Example: |
michael@0 | 394 | * {pageStyle: "Dusk", children: [null, {pageStyle: "Mozilla"}]} |
michael@0 | 395 | */ |
michael@0 | 396 | let PageStyleListener = { |
michael@0 | 397 | init: function () { |
michael@0 | 398 | Services.obs.addObserver(this, "author-style-disabled-changed", false); |
michael@0 | 399 | Services.obs.addObserver(this, "style-sheet-applicable-state-changed", false); |
michael@0 | 400 | gFrameTree.addObserver(this); |
michael@0 | 401 | }, |
michael@0 | 402 | |
michael@0 | 403 | uninit: function () { |
michael@0 | 404 | Services.obs.removeObserver(this, "author-style-disabled-changed"); |
michael@0 | 405 | Services.obs.removeObserver(this, "style-sheet-applicable-state-changed"); |
michael@0 | 406 | }, |
michael@0 | 407 | |
michael@0 | 408 | observe: function (subject, topic) { |
michael@0 | 409 | let frame = subject.defaultView; |
michael@0 | 410 | |
michael@0 | 411 | if (frame && gFrameTree.contains(frame)) { |
michael@0 | 412 | MessageQueue.push("pageStyle", () => this.collect()); |
michael@0 | 413 | } |
michael@0 | 414 | }, |
michael@0 | 415 | |
michael@0 | 416 | collect: function () { |
michael@0 | 417 | return PageStyle.collect(docShell, gFrameTree); |
michael@0 | 418 | }, |
michael@0 | 419 | |
michael@0 | 420 | onFrameTreeCollected: function () { |
michael@0 | 421 | MessageQueue.push("pageStyle", () => this.collect()); |
michael@0 | 422 | }, |
michael@0 | 423 | |
michael@0 | 424 | onFrameTreeReset: function () { |
michael@0 | 425 | MessageQueue.push("pageStyle", () => null); |
michael@0 | 426 | } |
michael@0 | 427 | }; |
michael@0 | 428 | |
michael@0 | 429 | /** |
michael@0 | 430 | * Listens for changes to docShell capabilities. Whenever a new load is started |
michael@0 | 431 | * we need to re-check the list of capabilities and send message when it has |
michael@0 | 432 | * changed. |
michael@0 | 433 | * |
michael@0 | 434 | * Causes a SessionStore:update message to be sent that contains the currently |
michael@0 | 435 | * disabled docShell capabilities (all nsIDocShell.allow* properties set to |
michael@0 | 436 | * false) as a string - i.e. capability names separate by commas. |
michael@0 | 437 | */ |
michael@0 | 438 | let DocShellCapabilitiesListener = { |
michael@0 | 439 | /** |
michael@0 | 440 | * This field is used to compare the last docShell capabilities to the ones |
michael@0 | 441 | * that have just been collected. If nothing changed we won't send a message. |
michael@0 | 442 | */ |
michael@0 | 443 | _latestCapabilities: "", |
michael@0 | 444 | |
michael@0 | 445 | init: function () { |
michael@0 | 446 | gFrameTree.addObserver(this); |
michael@0 | 447 | }, |
michael@0 | 448 | |
michael@0 | 449 | /** |
michael@0 | 450 | * onFrameTreeReset() is called as soon as we start loading a page. |
michael@0 | 451 | */ |
michael@0 | 452 | onFrameTreeReset: function() { |
michael@0 | 453 | // The order of docShell capabilities cannot change while we're running |
michael@0 | 454 | // so calling join() without sorting before is totally sufficient. |
michael@0 | 455 | let caps = DocShellCapabilities.collect(docShell).join(","); |
michael@0 | 456 | |
michael@0 | 457 | // Send new data only when the capability list changes. |
michael@0 | 458 | if (caps != this._latestCapabilities) { |
michael@0 | 459 | this._latestCapabilities = caps; |
michael@0 | 460 | MessageQueue.push("disallow", () => caps || null); |
michael@0 | 461 | } |
michael@0 | 462 | } |
michael@0 | 463 | }; |
michael@0 | 464 | |
michael@0 | 465 | /** |
michael@0 | 466 | * Listens for changes to the DOMSessionStorage. Whenever new keys are added, |
michael@0 | 467 | * existing ones removed or changed, or the storage is cleared we will send a |
michael@0 | 468 | * message to the parent process containing up-to-date sessionStorage data. |
michael@0 | 469 | * |
michael@0 | 470 | * Causes a SessionStore:update message to be sent that contains the current |
michael@0 | 471 | * DOMSessionStorage contents. The data is a nested object using host names |
michael@0 | 472 | * as keys and per-host DOMSessionStorage data as values. |
michael@0 | 473 | */ |
michael@0 | 474 | let SessionStorageListener = { |
michael@0 | 475 | init: function () { |
michael@0 | 476 | addEventListener("MozStorageChanged", this); |
michael@0 | 477 | Services.obs.addObserver(this, "browser:purge-domain-data", false); |
michael@0 | 478 | gFrameTree.addObserver(this); |
michael@0 | 479 | }, |
michael@0 | 480 | |
michael@0 | 481 | uninit: function () { |
michael@0 | 482 | Services.obs.removeObserver(this, "browser:purge-domain-data"); |
michael@0 | 483 | }, |
michael@0 | 484 | |
michael@0 | 485 | handleEvent: function (event) { |
michael@0 | 486 | // Ignore events triggered by localStorage or globalStorage changes. |
michael@0 | 487 | if (gFrameTree.contains(event.target) && isSessionStorageEvent(event)) { |
michael@0 | 488 | this.collect(); |
michael@0 | 489 | } |
michael@0 | 490 | }, |
michael@0 | 491 | |
michael@0 | 492 | observe: function () { |
michael@0 | 493 | // Collect data on the next tick so that any other observer |
michael@0 | 494 | // that needs to purge data can do its work first. |
michael@0 | 495 | setTimeout(() => this.collect(), 0); |
michael@0 | 496 | }, |
michael@0 | 497 | |
michael@0 | 498 | collect: function () { |
michael@0 | 499 | if (docShell) { |
michael@0 | 500 | MessageQueue.push("storage", () => SessionStorage.collect(docShell, gFrameTree)); |
michael@0 | 501 | } |
michael@0 | 502 | }, |
michael@0 | 503 | |
michael@0 | 504 | onFrameTreeCollected: function () { |
michael@0 | 505 | this.collect(); |
michael@0 | 506 | }, |
michael@0 | 507 | |
michael@0 | 508 | onFrameTreeReset: function () { |
michael@0 | 509 | this.collect(); |
michael@0 | 510 | } |
michael@0 | 511 | }; |
michael@0 | 512 | |
michael@0 | 513 | /** |
michael@0 | 514 | * Listen for changes to the privacy status of the tab. |
michael@0 | 515 | * By definition, tabs start in non-private mode. |
michael@0 | 516 | * |
michael@0 | 517 | * Causes a SessionStore:update message to be sent for |
michael@0 | 518 | * field "isPrivate". This message contains |
michael@0 | 519 | * |true| if the tab is now private |
michael@0 | 520 | * |null| if the tab is now public - the field is therefore |
michael@0 | 521 | * not saved. |
michael@0 | 522 | */ |
michael@0 | 523 | let PrivacyListener = { |
michael@0 | 524 | init: function() { |
michael@0 | 525 | docShell.addWeakPrivacyTransitionObserver(this); |
michael@0 | 526 | |
michael@0 | 527 | // Check that value at startup as it might have |
michael@0 | 528 | // been set before the frame script was loaded. |
michael@0 | 529 | if (docShell.QueryInterface(Ci.nsILoadContext).usePrivateBrowsing) { |
michael@0 | 530 | MessageQueue.push("isPrivate", () => true); |
michael@0 | 531 | } |
michael@0 | 532 | }, |
michael@0 | 533 | |
michael@0 | 534 | // Ci.nsIPrivacyTransitionObserver |
michael@0 | 535 | privateModeChanged: function(enabled) { |
michael@0 | 536 | MessageQueue.push("isPrivate", () => enabled || null); |
michael@0 | 537 | }, |
michael@0 | 538 | |
michael@0 | 539 | QueryInterface: XPCOMUtils.generateQI([Ci.nsIPrivacyTransitionObserver, |
michael@0 | 540 | Ci.nsISupportsWeakReference]) |
michael@0 | 541 | }; |
michael@0 | 542 | |
michael@0 | 543 | /** |
michael@0 | 544 | * A message queue that takes collected data and will take care of sending it |
michael@0 | 545 | * to the chrome process. It allows flushing using synchronous messages and |
michael@0 | 546 | * takes care of any race conditions that might occur because of that. Changes |
michael@0 | 547 | * will be batched if they're pushed in quick succession to avoid a message |
michael@0 | 548 | * flood. |
michael@0 | 549 | */ |
michael@0 | 550 | let MessageQueue = { |
michael@0 | 551 | /** |
michael@0 | 552 | * A unique, monotonically increasing ID used for outgoing messages. This is |
michael@0 | 553 | * important to make it possible to reuse tabs and allow sync flushes before |
michael@0 | 554 | * data could be destroyed. |
michael@0 | 555 | */ |
michael@0 | 556 | _id: 1, |
michael@0 | 557 | |
michael@0 | 558 | /** |
michael@0 | 559 | * A map (string -> lazy fn) holding lazy closures of all queued data |
michael@0 | 560 | * collection routines. These functions will return data collected from the |
michael@0 | 561 | * docShell. |
michael@0 | 562 | */ |
michael@0 | 563 | _data: new Map(), |
michael@0 | 564 | |
michael@0 | 565 | /** |
michael@0 | 566 | * A map holding the |this._id| value for every type of data back when it |
michael@0 | 567 | * was pushed onto the queue. We will use those IDs to find the data to send |
michael@0 | 568 | * and flush. |
michael@0 | 569 | */ |
michael@0 | 570 | _lastUpdated: new Map(), |
michael@0 | 571 | |
michael@0 | 572 | /** |
michael@0 | 573 | * The delay (in ms) used to delay sending changes after data has been |
michael@0 | 574 | * invalidated. |
michael@0 | 575 | */ |
michael@0 | 576 | BATCH_DELAY_MS: 1000, |
michael@0 | 577 | |
michael@0 | 578 | /** |
michael@0 | 579 | * The current timeout ID, null if there is no queue data. We use timeouts |
michael@0 | 580 | * to damp a flood of data changes and send lots of changes as one batch. |
michael@0 | 581 | */ |
michael@0 | 582 | _timeout: null, |
michael@0 | 583 | |
michael@0 | 584 | /** |
michael@0 | 585 | * Pushes a given |value| onto the queue. The given |key| represents the type |
michael@0 | 586 | * of data that is stored and can override data that has been queued before |
michael@0 | 587 | * but has not been sent to the parent process, yet. |
michael@0 | 588 | * |
michael@0 | 589 | * @param key (string) |
michael@0 | 590 | * A unique identifier specific to the type of data this is passed. |
michael@0 | 591 | * @param fn (function) |
michael@0 | 592 | * A function that returns the value that will be sent to the parent |
michael@0 | 593 | * process. |
michael@0 | 594 | */ |
michael@0 | 595 | push: function (key, fn) { |
michael@0 | 596 | this._data.set(key, createLazy(fn)); |
michael@0 | 597 | this._lastUpdated.set(key, this._id); |
michael@0 | 598 | |
michael@0 | 599 | if (!this._timeout) { |
michael@0 | 600 | // Wait a little before sending the message to batch multiple changes. |
michael@0 | 601 | this._timeout = setTimeout(() => this.send(), this.BATCH_DELAY_MS); |
michael@0 | 602 | } |
michael@0 | 603 | }, |
michael@0 | 604 | |
michael@0 | 605 | /** |
michael@0 | 606 | * Sends queued data to the chrome process. |
michael@0 | 607 | * |
michael@0 | 608 | * @param options (object) |
michael@0 | 609 | * {id: 123} to override the update ID used to accumulate data to send. |
michael@0 | 610 | * {sync: true} to send data to the parent process synchronously. |
michael@0 | 611 | */ |
michael@0 | 612 | send: function (options = {}) { |
michael@0 | 613 | // Looks like we have been called off a timeout after the tab has been |
michael@0 | 614 | // closed. The docShell is gone now and we can just return here as there |
michael@0 | 615 | // is nothing to do. |
michael@0 | 616 | if (!docShell) { |
michael@0 | 617 | return; |
michael@0 | 618 | } |
michael@0 | 619 | |
michael@0 | 620 | if (this._timeout) { |
michael@0 | 621 | clearTimeout(this._timeout); |
michael@0 | 622 | this._timeout = null; |
michael@0 | 623 | } |
michael@0 | 624 | |
michael@0 | 625 | let sync = options && options.sync; |
michael@0 | 626 | let startID = (options && options.id) || this._id; |
michael@0 | 627 | |
michael@0 | 628 | // We use sendRpcMessage in the sync case because we may have been called |
michael@0 | 629 | // through a CPOW. RPC messages are the only synchronous messages that the |
michael@0 | 630 | // child is allowed to send to the parent while it is handling a CPOW |
michael@0 | 631 | // request. |
michael@0 | 632 | let sendMessage = sync ? sendRpcMessage : sendAsyncMessage; |
michael@0 | 633 | |
michael@0 | 634 | let durationMs = Date.now(); |
michael@0 | 635 | |
michael@0 | 636 | let data = {}; |
michael@0 | 637 | for (let [key, id] of this._lastUpdated) { |
michael@0 | 638 | // There is no data for the given key anymore because |
michael@0 | 639 | // the parent process already marked it as received. |
michael@0 | 640 | if (!this._data.has(key)) { |
michael@0 | 641 | continue; |
michael@0 | 642 | } |
michael@0 | 643 | |
michael@0 | 644 | if (startID > id) { |
michael@0 | 645 | // If the |id| passed by the parent process is higher than the one |
michael@0 | 646 | // stored in |_lastUpdated| for the given key we know that the parent |
michael@0 | 647 | // received all necessary data and we can remove it from the map. |
michael@0 | 648 | this._data.delete(key); |
michael@0 | 649 | continue; |
michael@0 | 650 | } |
michael@0 | 651 | |
michael@0 | 652 | data[key] = this._data.get(key)(); |
michael@0 | 653 | } |
michael@0 | 654 | |
michael@0 | 655 | durationMs = Date.now() - durationMs; |
michael@0 | 656 | let telemetry = { |
michael@0 | 657 | FX_SESSION_RESTORE_CONTENT_COLLECT_DATA_LONGEST_OP_MS: durationMs |
michael@0 | 658 | } |
michael@0 | 659 | |
michael@0 | 660 | // Send all data to the parent process. |
michael@0 | 661 | sendMessage("SessionStore:update", { |
michael@0 | 662 | id: this._id, |
michael@0 | 663 | data: data, |
michael@0 | 664 | telemetry: telemetry |
michael@0 | 665 | }); |
michael@0 | 666 | |
michael@0 | 667 | // Increase our unique message ID. |
michael@0 | 668 | this._id++; |
michael@0 | 669 | }, |
michael@0 | 670 | |
michael@0 | 671 | /** |
michael@0 | 672 | * This function is used to make the message queue flush all queue data that |
michael@0 | 673 | * hasn't been sent to the parent process, yet. |
michael@0 | 674 | * |
michael@0 | 675 | * @param id (int) |
michael@0 | 676 | * A unique id that represents the latest message received by the |
michael@0 | 677 | * chrome process. We can use this to determine which messages have not |
michael@0 | 678 | * yet been received because they are still stuck in the event queue. |
michael@0 | 679 | */ |
michael@0 | 680 | flush: function (id) { |
michael@0 | 681 | // It's important to always send data, even if there is nothing to flush. |
michael@0 | 682 | // The update message will be received by the parent process that can then |
michael@0 | 683 | // update its last received update ID to ignore stale messages. |
michael@0 | 684 | this.send({id: id + 1, sync: true}); |
michael@0 | 685 | |
michael@0 | 686 | this._data.clear(); |
michael@0 | 687 | this._lastUpdated.clear(); |
michael@0 | 688 | }, |
michael@0 | 689 | |
michael@0 | 690 | /** |
michael@0 | 691 | * DO NOT USE - DEBUGGING / TESTING ONLY |
michael@0 | 692 | * |
michael@0 | 693 | * This function is used to simulate certain situations where race conditions |
michael@0 | 694 | * can occur by sending data shortly before flushing synchronously. |
michael@0 | 695 | */ |
michael@0 | 696 | flushAsync: function () { |
michael@0 | 697 | if (!Services.prefs.getBoolPref("browser.sessionstore.debug")) { |
michael@0 | 698 | throw new Error("flushAsync() must be used for testing, only."); |
michael@0 | 699 | } |
michael@0 | 700 | |
michael@0 | 701 | this.send(); |
michael@0 | 702 | } |
michael@0 | 703 | }; |
michael@0 | 704 | |
michael@0 | 705 | EventListener.init(); |
michael@0 | 706 | MessageListener.init(); |
michael@0 | 707 | FormDataListener.init(); |
michael@0 | 708 | SyncHandler.init(); |
michael@0 | 709 | PageStyleListener.init(); |
michael@0 | 710 | SessionHistoryListener.init(); |
michael@0 | 711 | SessionStorageListener.init(); |
michael@0 | 712 | ScrollPositionListener.init(); |
michael@0 | 713 | DocShellCapabilitiesListener.init(); |
michael@0 | 714 | PrivacyListener.init(); |
michael@0 | 715 | |
michael@0 | 716 | addEventListener("unload", () => { |
michael@0 | 717 | // Remove all registered nsIObservers. |
michael@0 | 718 | PageStyleListener.uninit(); |
michael@0 | 719 | SessionStorageListener.uninit(); |
michael@0 | 720 | SessionHistoryListener.uninit(); |
michael@0 | 721 | |
michael@0 | 722 | // We don't need to take care of any gFrameTree observers as the gFrameTree |
michael@0 | 723 | // will die with the content script. The same goes for the privacy transition |
michael@0 | 724 | // observer that will die with the docShell when the tab is closed. |
michael@0 | 725 | }); |