browser/components/sessionstore/src/ContentRestore.jsm

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/browser/components/sessionstore/src/ContentRestore.jsm	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,419 @@
     1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public
     1.5 +* License, v. 2.0. If a copy of the MPL was not distributed with this file,
     1.6 +* You can obtain one at http://mozilla.org/MPL/2.0/. */
     1.7 +
     1.8 +"use strict";
     1.9 +
    1.10 +this.EXPORTED_SYMBOLS = ["ContentRestore"];
    1.11 +
    1.12 +const Cu = Components.utils;
    1.13 +const Ci = Components.interfaces;
    1.14 +
    1.15 +Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
    1.16 +
    1.17 +XPCOMUtils.defineLazyModuleGetter(this, "DocShellCapabilities",
    1.18 +  "resource:///modules/sessionstore/DocShellCapabilities.jsm");
    1.19 +XPCOMUtils.defineLazyModuleGetter(this, "FormData",
    1.20 +  "resource://gre/modules/FormData.jsm");
    1.21 +XPCOMUtils.defineLazyModuleGetter(this, "PageStyle",
    1.22 +  "resource:///modules/sessionstore/PageStyle.jsm");
    1.23 +XPCOMUtils.defineLazyModuleGetter(this, "ScrollPosition",
    1.24 +  "resource://gre/modules/ScrollPosition.jsm");
    1.25 +XPCOMUtils.defineLazyModuleGetter(this, "SessionHistory",
    1.26 +  "resource:///modules/sessionstore/SessionHistory.jsm");
    1.27 +XPCOMUtils.defineLazyModuleGetter(this, "SessionStorage",
    1.28 +  "resource:///modules/sessionstore/SessionStorage.jsm");
    1.29 +XPCOMUtils.defineLazyModuleGetter(this, "Utils",
    1.30 +  "resource:///modules/sessionstore/Utils.jsm");
    1.31 +
    1.32 +/**
    1.33 + * This module implements the content side of session restoration. The chrome
    1.34 + * side is handled by SessionStore.jsm. The functions in this module are called
    1.35 + * by content-sessionStore.js based on messages received from SessionStore.jsm
    1.36 + * (or, in one case, based on a "load" event). Each tab has its own
    1.37 + * ContentRestore instance, constructed by content-sessionStore.js.
    1.38 + *
    1.39 + * In a typical restore, content-sessionStore.js will call the following based
    1.40 + * on messages and events it receives:
    1.41 + *
    1.42 + *   restoreHistory(epoch, tabData, reloadCallback)
    1.43 + *     Restores the tab's history and session cookies.
    1.44 + *   restoreTabContent(finishCallback)
    1.45 + *     Starts loading the data for the current page to restore.
    1.46 + *   restoreDocument()
    1.47 + *     Restore form and scroll data.
    1.48 + *
    1.49 + * When the page has been loaded from the network, we call finishCallback. It
    1.50 + * should send a message to SessionStore.jsm, which may cause other tabs to be
    1.51 + * restored.
    1.52 + *
    1.53 + * When the page has finished loading, a "load" event will trigger in
    1.54 + * content-sessionStore.js, which will call restoreDocument. At that point,
    1.55 + * form data is restored and the restore is complete.
    1.56 + *
    1.57 + * At any time, SessionStore.jsm can cancel the ongoing restore by sending a
    1.58 + * reset message, which causes resetRestore to be called. At that point it's
    1.59 + * legal to begin another restore.
    1.60 + *
    1.61 + * The epoch that is passed into restoreHistory is merely a token. All messages
    1.62 + * sent back to SessionStore.jsm include the epoch. This way, SessionStore.jsm
    1.63 + * can discard messages that relate to restores that it has canceled (by
    1.64 + * starting a new restore, say).
    1.65 + */
    1.66 +function ContentRestore(chromeGlobal) {
    1.67 +  let internal = new ContentRestoreInternal(chromeGlobal);
    1.68 +  let external = {};
    1.69 +
    1.70 +  let EXPORTED_METHODS = ["restoreHistory",
    1.71 +                          "restoreTabContent",
    1.72 +                          "restoreDocument",
    1.73 +                          "resetRestore",
    1.74 +                          "getRestoreEpoch",
    1.75 +                         ];
    1.76 +
    1.77 +  for (let method of EXPORTED_METHODS) {
    1.78 +    external[method] = internal[method].bind(internal);
    1.79 +  }
    1.80 +
    1.81 +  return Object.freeze(external);
    1.82 +}
    1.83 +
    1.84 +function ContentRestoreInternal(chromeGlobal) {
    1.85 +  this.chromeGlobal = chromeGlobal;
    1.86 +
    1.87 +  // The following fields are only valid during certain phases of the restore
    1.88 +  // process.
    1.89 +
    1.90 +  // The epoch that was passed into restoreHistory. Removed in restoreDocument.
    1.91 +  this._epoch = 0;
    1.92 +
    1.93 +  // The tabData for the restore. Set in restoreHistory and removed in
    1.94 +  // restoreTabContent.
    1.95 +  this._tabData = null;
    1.96 +
    1.97 +  // Contains {entry, pageStyle, scrollPositions, formdata}, where entry is a
    1.98 +  // single entry from the tabData.entries array. Set in
    1.99 +  // restoreTabContent and removed in restoreDocument.
   1.100 +  this._restoringDocument = null;
   1.101 +
   1.102 +  // This listener is used to detect reloads on restoring tabs. Set in
   1.103 +  // restoreHistory and removed in restoreTabContent.
   1.104 +  this._historyListener = null;
   1.105 +
   1.106 +  // This listener detects when a restoring tab has finished loading data from
   1.107 +  // the network. Set in restoreTabContent and removed in resetRestore.
   1.108 +  this._progressListener = null;
   1.109 +}
   1.110 +
   1.111 +/**
   1.112 + * The API for the ContentRestore module. Methods listed in EXPORTED_METHODS are
   1.113 + * public.
   1.114 + */
   1.115 +ContentRestoreInternal.prototype = {
   1.116 +
   1.117 +  get docShell() {
   1.118 +    return this.chromeGlobal.docShell;
   1.119 +  },
   1.120 +
   1.121 +  /**
   1.122 +   * Starts the process of restoring a tab. The tabData to be restored is passed
   1.123 +   * in here and used throughout the restoration. The epoch (which must be
   1.124 +   * non-zero) is passed through to all the callbacks. If the tab is ever
   1.125 +   * reloaded during the restore process, reloadCallback is called.
   1.126 +   */
   1.127 +  restoreHistory: function (epoch, tabData, reloadCallback) {
   1.128 +    this._tabData = tabData;
   1.129 +    this._epoch = epoch;
   1.130 +
   1.131 +    // In case about:blank isn't done yet.
   1.132 +    let webNavigation = this.docShell.QueryInterface(Ci.nsIWebNavigation);
   1.133 +    webNavigation.stop(Ci.nsIWebNavigation.STOP_ALL);
   1.134 +
   1.135 +    // Make sure currentURI is set so that switch-to-tab works before the tab is
   1.136 +    // restored. We'll reset this to about:blank when we try to restore the tab
   1.137 +    // to ensure that docshell doeesn't get confused.
   1.138 +    let activeIndex = tabData.index - 1;
   1.139 +    let activePageData = tabData.entries[activeIndex] || {};
   1.140 +    let uri = activePageData.url || null;
   1.141 +    if (uri) {
   1.142 +      webNavigation.setCurrentURI(Utils.makeURI(uri));
   1.143 +    }
   1.144 +
   1.145 +    SessionHistory.restore(this.docShell, tabData);
   1.146 +
   1.147 +    // Add a listener to watch for reloads.
   1.148 +    let listener = new HistoryListener(this.docShell, reloadCallback);
   1.149 +    webNavigation.sessionHistory.addSHistoryListener(listener);
   1.150 +    this._historyListener = listener;
   1.151 +
   1.152 +    // Make sure to reset the capabilities and attributes in case this tab gets
   1.153 +    // reused.
   1.154 +    let disallow = new Set(tabData.disallow && tabData.disallow.split(","));
   1.155 +    DocShellCapabilities.restore(this.docShell, disallow);
   1.156 +
   1.157 +    if (tabData.storage && this.docShell instanceof Ci.nsIDocShell)
   1.158 +      SessionStorage.restore(this.docShell, tabData.storage);
   1.159 +  },
   1.160 +
   1.161 +  /**
   1.162 +   * Start loading the current page. When the data has finished loading from the
   1.163 +   * network, finishCallback is called. Returns true if the load was successful.
   1.164 +   */
   1.165 +  restoreTabContent: function (finishCallback) {
   1.166 +    let tabData = this._tabData;
   1.167 +    this._tabData = null;
   1.168 +
   1.169 +    let webNavigation = this.docShell.QueryInterface(Ci.nsIWebNavigation);
   1.170 +    let history = webNavigation.sessionHistory;
   1.171 +
   1.172 +    // The reload listener is no longer needed.
   1.173 +    this._historyListener.uninstall();
   1.174 +    this._historyListener = null;
   1.175 +
   1.176 +    // We're about to start a load. This listener will be called when the load
   1.177 +    // has finished getting everything from the network.
   1.178 +    let progressListener = new ProgressListener(this.docShell, () => {
   1.179 +      // Call resetRestore to reset the state back to normal. The data needed
   1.180 +      // for restoreDocument (which hasn't happened yet) will remain in
   1.181 +      // _restoringDocument.
   1.182 +      this.resetRestore(this.docShell);
   1.183 +
   1.184 +      finishCallback();
   1.185 +    });
   1.186 +    this._progressListener = progressListener;
   1.187 +
   1.188 +    // Reset the current URI to about:blank. We changed it above for
   1.189 +    // switch-to-tab, but now it must go back to the correct value before the
   1.190 +    // load happens.
   1.191 +    webNavigation.setCurrentURI(Utils.makeURI("about:blank"));
   1.192 +
   1.193 +    try {
   1.194 +      if (tabData.userTypedValue && tabData.userTypedClear) {
   1.195 +        // If the user typed a URL into the URL bar and hit enter right before
   1.196 +        // we crashed, we want to start loading that page again. A non-zero
   1.197 +        // userTypedClear value means that the load had started.
   1.198 +        let activeIndex = tabData.index - 1;
   1.199 +        if (activeIndex > 0) {
   1.200 +          // Go to the right history entry, but don't load anything yet.
   1.201 +          history.getEntryAtIndex(activeIndex, true);
   1.202 +        }
   1.203 +
   1.204 +        // Load userTypedValue and fix up the URL if it's partial/broken.
   1.205 +        webNavigation.loadURI(tabData.userTypedValue,
   1.206 +                              Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP,
   1.207 +                              null, null, null);
   1.208 +      } else if (tabData.entries.length) {
   1.209 +        // Stash away the data we need for restoreDocument.
   1.210 +        let activeIndex = tabData.index - 1;
   1.211 +        this._restoringDocument = {entry: tabData.entries[activeIndex] || {},
   1.212 +                                   formdata: tabData.formdata || {},
   1.213 +                                   pageStyle: tabData.pageStyle || {},
   1.214 +                                   scrollPositions: tabData.scroll || {}};
   1.215 +
   1.216 +        // In order to work around certain issues in session history, we need to
   1.217 +        // force session history to update its internal index and call reload
   1.218 +        // instead of gotoIndex. See bug 597315.
   1.219 +        history.getEntryAtIndex(activeIndex, true);
   1.220 +        history.reloadCurrentEntry();
   1.221 +      } else {
   1.222 +        // If there's nothing to restore, we should still blank the page.
   1.223 +        webNavigation.loadURI("about:blank",
   1.224 +                              Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_HISTORY,
   1.225 +                              null, null, null);
   1.226 +      }
   1.227 +
   1.228 +      return true;
   1.229 +    } catch (ex if ex instanceof Ci.nsIException) {
   1.230 +      // Ignore page load errors, but return false to signal that the load never
   1.231 +      // happened.
   1.232 +      return false;
   1.233 +    }
   1.234 +  },
   1.235 +
   1.236 +  /**
   1.237 +   * Accumulates a list of frames that need to be restored for the given browser
   1.238 +   * element. A frame is only restored if its current URL matches the one saved
   1.239 +   * in the session data. Each frame to be restored is returned along with its
   1.240 +   * associated session data.
   1.241 +   *
   1.242 +   * @param browser the browser being restored
   1.243 +   * @return an array of [frame, data] pairs
   1.244 +   */
   1.245 +  getFramesToRestore: function (content, data) {
   1.246 +    function hasExpectedURL(aDocument, aURL) {
   1.247 +      return !aURL || aURL.replace(/#.*/, "") == aDocument.location.href.replace(/#.*/, "");
   1.248 +    }
   1.249 +
   1.250 +    let frameList = [];
   1.251 +
   1.252 +    function enumerateFrame(content, data) {
   1.253 +      // Skip the frame if the user has navigated away before loading finished.
   1.254 +      if (!hasExpectedURL(content.document, data.url)) {
   1.255 +        return;
   1.256 +      }
   1.257 +
   1.258 +      frameList.push([content, data]);
   1.259 +
   1.260 +      for (let i = 0; i < content.frames.length; i++) {
   1.261 +        if (data.children && data.children[i]) {
   1.262 +          enumerateFrame(content.frames[i], data.children[i]);
   1.263 +        }
   1.264 +      }
   1.265 +    }
   1.266 +
   1.267 +    enumerateFrame(content, data);
   1.268 +
   1.269 +    return frameList;
   1.270 +  },
   1.271 +
   1.272 +  /**
   1.273 +   * Finish restoring the tab by filling in form data and setting the scroll
   1.274 +   * position. The restore is complete when this function exits. It should be
   1.275 +   * called when the "load" event fires for the restoring tab.
   1.276 +   */
   1.277 +  restoreDocument: function () {
   1.278 +    this._epoch = 0;
   1.279 +
   1.280 +    if (!this._restoringDocument) {
   1.281 +      return;
   1.282 +    }
   1.283 +    let {entry, pageStyle, formdata, scrollPositions} = this._restoringDocument;
   1.284 +    this._restoringDocument = null;
   1.285 +
   1.286 +    let window = this.docShell.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindow);
   1.287 +    let frameList = this.getFramesToRestore(window, entry);
   1.288 +
   1.289 +    // Support the old pageStyle format.
   1.290 +    if (typeof(pageStyle) === "string") {
   1.291 +      PageStyle.restore(this.docShell, frameList, pageStyle);
   1.292 +    } else {
   1.293 +      PageStyle.restoreTree(this.docShell, pageStyle);
   1.294 +    }
   1.295 +
   1.296 +    FormData.restoreTree(window, formdata);
   1.297 +    ScrollPosition.restoreTree(window, scrollPositions);
   1.298 +
   1.299 +    // We need to support the old form and scroll data for a while at least.
   1.300 +    for (let [frame, data] of frameList) {
   1.301 +      if (data.hasOwnProperty("formdata") || data.hasOwnProperty("innerHTML")) {
   1.302 +        let formdata = data.formdata || {};
   1.303 +        formdata.url = data.url;
   1.304 +
   1.305 +        if (data.hasOwnProperty("innerHTML")) {
   1.306 +          formdata.innerHTML = data.innerHTML;
   1.307 +        }
   1.308 +
   1.309 +        FormData.restore(frame, formdata);
   1.310 +      }
   1.311 +
   1.312 +      ScrollPosition.restore(frame, data.scroll || "");
   1.313 +    }
   1.314 +  },
   1.315 +
   1.316 +  /**
   1.317 +   * Cancel an ongoing restore. This function can be called any time between
   1.318 +   * restoreHistory and restoreDocument.
   1.319 +   *
   1.320 +   * This function is called externally (if a restore is canceled) and
   1.321 +   * internally (when the loads for a restore have finished). In the latter
   1.322 +   * case, it's called before restoreDocument, so it cannot clear
   1.323 +   * _restoringDocument.
   1.324 +   */
   1.325 +  resetRestore: function () {
   1.326 +    this._tabData = null;
   1.327 +
   1.328 +    if (this._historyListener) {
   1.329 +      this._historyListener.uninstall();
   1.330 +    }
   1.331 +    this._historyListener = null;
   1.332 +
   1.333 +    if (this._progressListener) {
   1.334 +      this._progressListener.uninstall();
   1.335 +    }
   1.336 +    this._progressListener = null;
   1.337 +  },
   1.338 +
   1.339 +  /**
   1.340 +   * If a restore is ongoing, this function returns the value of |epoch| that
   1.341 +   * was passed to restoreHistory. If no restore is ongoing, it returns 0.
   1.342 +   */
   1.343 +  getRestoreEpoch: function () {
   1.344 +    return this._epoch;
   1.345 +  },
   1.346 +};
   1.347 +
   1.348 +/*
   1.349 + * This listener detects when a page being restored is reloaded. It triggers a
   1.350 + * callback and cancels the reload. The callback will send a message to
   1.351 + * SessionStore.jsm so that it can restore the content immediately.
   1.352 + */
   1.353 +function HistoryListener(docShell, callback) {
   1.354 +  let webNavigation = docShell.QueryInterface(Ci.nsIWebNavigation);
   1.355 +  webNavigation.sessionHistory.addSHistoryListener(this);
   1.356 +
   1.357 +  this.webNavigation = webNavigation;
   1.358 +  this.callback = callback;
   1.359 +}
   1.360 +HistoryListener.prototype = {
   1.361 +  QueryInterface: XPCOMUtils.generateQI([
   1.362 +    Ci.nsISHistoryListener,
   1.363 +    Ci.nsISupportsWeakReference
   1.364 +  ]),
   1.365 +
   1.366 +  uninstall: function () {
   1.367 +    this.webNavigation.sessionHistory.removeSHistoryListener(this);
   1.368 +  },
   1.369 +
   1.370 +  OnHistoryNewEntry: function(newURI) {},
   1.371 +  OnHistoryGoBack: function(backURI) { return true; },
   1.372 +  OnHistoryGoForward: function(forwardURI) { return true; },
   1.373 +  OnHistoryGotoIndex: function(index, gotoURI) { return true; },
   1.374 +  OnHistoryPurge: function(numEntries) { return true; },
   1.375 +  OnHistoryReplaceEntry: function(index) {},
   1.376 +
   1.377 +  OnHistoryReload: function(reloadURI, reloadFlags) {
   1.378 +    this.callback();
   1.379 +
   1.380 +    // Cancel the load.
   1.381 +    return false;
   1.382 +  },
   1.383 +}
   1.384 +
   1.385 +/**
   1.386 + * This class informs SessionStore.jsm whenever the network requests for a
   1.387 + * restoring page have completely finished. We only restore three tabs
   1.388 + * simultaneously, so this is the signal for SessionStore.jsm to kick off
   1.389 + * another restore (if there are more to do).
   1.390 + */
   1.391 +function ProgressListener(docShell, callback)
   1.392 +{
   1.393 +  let webProgress = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
   1.394 +                            .getInterface(Ci.nsIWebProgress);
   1.395 +  webProgress.addProgressListener(this, Ci.nsIWebProgress.NOTIFY_STATE_WINDOW);
   1.396 +
   1.397 +  this.webProgress = webProgress;
   1.398 +  this.callback = callback;
   1.399 +}
   1.400 +ProgressListener.prototype = {
   1.401 +  QueryInterface: XPCOMUtils.generateQI([
   1.402 +    Ci.nsIWebProgressListener,
   1.403 +    Ci.nsISupportsWeakReference
   1.404 +  ]),
   1.405 +
   1.406 +  uninstall: function() {
   1.407 +    this.webProgress.removeProgressListener(this);
   1.408 +  },
   1.409 +
   1.410 +  onStateChange: function(webProgress, request, stateFlags, status) {
   1.411 +    if (stateFlags & Ci.nsIWebProgressListener.STATE_STOP &&
   1.412 +        stateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK &&
   1.413 +        stateFlags & Ci.nsIWebProgressListener.STATE_IS_WINDOW) {
   1.414 +      this.callback();
   1.415 +    }
   1.416 +  },
   1.417 +
   1.418 +  onLocationChange: function() {},
   1.419 +  onProgressChange: function() {},
   1.420 +  onStatusChange: function() {},
   1.421 +  onSecurityChange: function() {},
   1.422 +};

mercurial