browser/components/sessionstore/content/content-sessionStore.js

Wed, 31 Dec 2014 07:53:36 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 07:53:36 +0100
branch
TOR_BUG_3246
changeset 5
4ab42b5ab56c
permissions
-rw-r--r--

Correct small whitespace inconsistency, lost while renaming variables.

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

mercurial