1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/mobile/android/components/SessionStore.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,937 @@ 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 1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +const Cc = Components.classes; 1.9 +const Ci = Components.interfaces; 1.10 +const Cu = Components.utils; 1.11 +const Cr = Components.results; 1.12 + 1.13 +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); 1.14 +Cu.import("resource://gre/modules/Services.jsm"); 1.15 + 1.16 +#ifdef MOZ_CRASHREPORTER 1.17 +XPCOMUtils.defineLazyServiceGetter(this, "CrashReporter", 1.18 + "@mozilla.org/xre/app-info;1", "nsICrashReporter"); 1.19 +#endif 1.20 + 1.21 +XPCOMUtils.defineLazyModuleGetter(this, "Task", "resource://gre/modules/Task.jsm"); 1.22 +XPCOMUtils.defineLazyModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm"); 1.23 +XPCOMUtils.defineLazyModuleGetter(this, "sendMessageToJava", "resource://gre/modules/Messaging.jsm"); 1.24 + 1.25 +function dump(a) { 1.26 + Services.console.logStringMessage(a); 1.27 +} 1.28 + 1.29 +// ----------------------------------------------------------------------- 1.30 +// Session Store 1.31 +// ----------------------------------------------------------------------- 1.32 + 1.33 +const STATE_STOPPED = 0; 1.34 +const STATE_RUNNING = 1; 1.35 + 1.36 +function SessionStore() { } 1.37 + 1.38 +SessionStore.prototype = { 1.39 + classID: Components.ID("{8c1f07d6-cba3-4226-a315-8bd43d67d032}"), 1.40 + 1.41 + QueryInterface: XPCOMUtils.generateQI([Ci.nsISessionStore, 1.42 + Ci.nsIDOMEventListener, 1.43 + Ci.nsIObserver, 1.44 + Ci.nsISupportsWeakReference]), 1.45 + 1.46 + _windows: {}, 1.47 + _lastSaveTime: 0, 1.48 + _interval: 10000, 1.49 + _maxTabsUndo: 1, 1.50 + _pendingWrite: 0, 1.51 + 1.52 + init: function ss_init() { 1.53 + // Get file references 1.54 + this._sessionFile = Services.dirsvc.get("ProfD", Ci.nsILocalFile); 1.55 + this._sessionFileBackup = this._sessionFile.clone(); 1.56 + this._sessionFile.append("sessionstore.js"); 1.57 + this._sessionFileBackup.append("sessionstore.bak"); 1.58 + 1.59 + this._loadState = STATE_STOPPED; 1.60 + 1.61 + this._interval = Services.prefs.getIntPref("browser.sessionstore.interval"); 1.62 + this._maxTabsUndo = Services.prefs.getIntPref("browser.sessionstore.max_tabs_undo"); 1.63 + }, 1.64 + 1.65 + _clearDisk: function ss_clearDisk() { 1.66 + OS.File.remove(this._sessionFile.path); 1.67 + OS.File.remove(this._sessionFileBackup.path); 1.68 + }, 1.69 + 1.70 + observe: function ss_observe(aSubject, aTopic, aData) { 1.71 + let self = this; 1.72 + let observerService = Services.obs; 1.73 + switch (aTopic) { 1.74 + case "app-startup": 1.75 + observerService.addObserver(this, "final-ui-startup", true); 1.76 + observerService.addObserver(this, "domwindowopened", true); 1.77 + observerService.addObserver(this, "domwindowclosed", true); 1.78 + observerService.addObserver(this, "browser:purge-session-history", true); 1.79 + observerService.addObserver(this, "Session:Restore", true); 1.80 + observerService.addObserver(this, "application-background", true); 1.81 + break; 1.82 + case "final-ui-startup": 1.83 + observerService.removeObserver(this, "final-ui-startup"); 1.84 + this.init(); 1.85 + break; 1.86 + case "domwindowopened": { 1.87 + let window = aSubject; 1.88 + window.addEventListener("load", function() { 1.89 + self.onWindowOpen(window); 1.90 + window.removeEventListener("load", arguments.callee, false); 1.91 + }, false); 1.92 + break; 1.93 + } 1.94 + case "domwindowclosed": // catch closed windows 1.95 + this.onWindowClose(aSubject); 1.96 + break; 1.97 + case "browser:purge-session-history": // catch sanitization 1.98 + this._clearDisk(); 1.99 + 1.100 + // Clear all data about closed tabs 1.101 + for (let [ssid, win] in Iterator(this._windows)) 1.102 + win.closedTabs = []; 1.103 + 1.104 + if (this._loadState == STATE_RUNNING) { 1.105 + // Save the purged state immediately 1.106 + this.saveState(); 1.107 + } 1.108 + 1.109 + Services.obs.notifyObservers(null, "sessionstore-state-purge-complete", ""); 1.110 + break; 1.111 + case "timer-callback": 1.112 + // Timer call back for delayed saving 1.113 + this._saveTimer = null; 1.114 + if (this._pendingWrite) { 1.115 + this.saveState(); 1.116 + } 1.117 + break; 1.118 + case "Session:Restore": { 1.119 + Services.obs.removeObserver(this, "Session:Restore"); 1.120 + if (aData) { 1.121 + // Be ready to handle any restore failures by making sure we have a valid tab opened 1.122 + let window = Services.wm.getMostRecentWindow("navigator:browser"); 1.123 + let restoreCleanup = { 1.124 + observe: function (aSubject, aTopic, aData) { 1.125 + Services.obs.removeObserver(restoreCleanup, "sessionstore-windows-restored"); 1.126 + 1.127 + if (window.BrowserApp.tabs.length == 0) { 1.128 + window.BrowserApp.addTab("about:home", { 1.129 + selected: true 1.130 + }); 1.131 + } 1.132 + 1.133 + // Let Java know we're done restoring tabs so tabs added after this can be animated 1.134 + sendMessageToJava({ 1.135 + type: "Session:RestoreEnd" 1.136 + }); 1.137 + }.bind(this) 1.138 + }; 1.139 + Services.obs.addObserver(restoreCleanup, "sessionstore-windows-restored", false); 1.140 + 1.141 + // Do a restore, triggered by Java 1.142 + let data = JSON.parse(aData); 1.143 + this.restoreLastSession(data.sessionString); 1.144 + } else { 1.145 + // Not doing a restore; just send restore message 1.146 + Services.obs.notifyObservers(null, "sessionstore-windows-restored", ""); 1.147 + } 1.148 + break; 1.149 + } 1.150 + case "application-background": 1.151 + // We receive this notification when Android's onPause callback is 1.152 + // executed. After onPause, the application may be terminated at any 1.153 + // point without notice; therefore, we must synchronously write out any 1.154 + // pending save state to ensure that this data does not get lost. 1.155 + this.flushPendingState(); 1.156 + break; 1.157 + } 1.158 + }, 1.159 + 1.160 + handleEvent: function ss_handleEvent(aEvent) { 1.161 + let window = aEvent.currentTarget.ownerDocument.defaultView; 1.162 + switch (aEvent.type) { 1.163 + case "TabOpen": { 1.164 + let browser = aEvent.target; 1.165 + this.onTabAdd(window, browser); 1.166 + break; 1.167 + } 1.168 + case "TabClose": { 1.169 + let browser = aEvent.target; 1.170 + this.onTabClose(window, browser); 1.171 + this.onTabRemove(window, browser); 1.172 + break; 1.173 + } 1.174 + case "TabSelect": { 1.175 + let browser = aEvent.target; 1.176 + this.onTabSelect(window, browser); 1.177 + break; 1.178 + } 1.179 + case "DOMTitleChanged": { 1.180 + let browser = aEvent.currentTarget; 1.181 + 1.182 + // Handle only top-level DOMTitleChanged event 1.183 + if (browser.contentDocument !== aEvent.originalTarget) 1.184 + return; 1.185 + 1.186 + // Use DOMTitleChanged to detect page loads over alternatives. 1.187 + // onLocationChange happens too early, so we don't have the page title 1.188 + // yet; pageshow happens too late, so we could lose session data if the 1.189 + // browser were killed. 1.190 + this.onTabLoad(window, browser); 1.191 + break; 1.192 + } 1.193 + } 1.194 + }, 1.195 + 1.196 + onWindowOpen: function ss_onWindowOpen(aWindow) { 1.197 + // Return if window has already been initialized 1.198 + if (aWindow && aWindow.__SSID && this._windows[aWindow.__SSID]) 1.199 + return; 1.200 + 1.201 + // Ignore non-browser windows and windows opened while shutting down 1.202 + if (aWindow.document.documentElement.getAttribute("windowtype") != "navigator:browser") 1.203 + return; 1.204 + 1.205 + // Assign it a unique identifier (timestamp) and create its data object 1.206 + aWindow.__SSID = "window" + Date.now(); 1.207 + this._windows[aWindow.__SSID] = { tabs: [], selected: 0, closedTabs: [] }; 1.208 + 1.209 + // Perform additional initialization when the first window is loading 1.210 + if (this._loadState == STATE_STOPPED) { 1.211 + this._loadState = STATE_RUNNING; 1.212 + this._lastSaveTime = Date.now(); 1.213 + } 1.214 + 1.215 + // Add tab change listeners to all already existing tabs 1.216 + let tabs = aWindow.BrowserApp.tabs; 1.217 + for (let i = 0; i < tabs.length; i++) 1.218 + this.onTabAdd(aWindow, tabs[i].browser, true); 1.219 + 1.220 + // Notification of tab add/remove/selection 1.221 + let browsers = aWindow.document.getElementById("browsers"); 1.222 + browsers.addEventListener("TabOpen", this, true); 1.223 + browsers.addEventListener("TabClose", this, true); 1.224 + browsers.addEventListener("TabSelect", this, true); 1.225 + }, 1.226 + 1.227 + onWindowClose: function ss_onWindowClose(aWindow) { 1.228 + // Ignore windows not tracked by SessionStore 1.229 + if (!aWindow.__SSID || !this._windows[aWindow.__SSID]) 1.230 + return; 1.231 + 1.232 + let browsers = aWindow.document.getElementById("browsers"); 1.233 + browsers.removeEventListener("TabOpen", this, true); 1.234 + browsers.removeEventListener("TabClose", this, true); 1.235 + browsers.removeEventListener("TabSelect", this, true); 1.236 + 1.237 + if (this._loadState == STATE_RUNNING) { 1.238 + // Update all window data for a last time 1.239 + this._collectWindowData(aWindow); 1.240 + 1.241 + // Clear this window from the list 1.242 + delete this._windows[aWindow.__SSID]; 1.243 + 1.244 + // Save the state without this window to disk 1.245 + this.saveStateDelayed(); 1.246 + } 1.247 + 1.248 + let tabs = aWindow.BrowserApp.tabs; 1.249 + for (let i = 0; i < tabs.length; i++) 1.250 + this.onTabRemove(aWindow, tabs[i].browser, true); 1.251 + 1.252 + delete aWindow.__SSID; 1.253 + }, 1.254 + 1.255 + onTabAdd: function ss_onTabAdd(aWindow, aBrowser, aNoNotification) { 1.256 + aBrowser.addEventListener("DOMTitleChanged", this, true); 1.257 + if (!aNoNotification) 1.258 + this.saveStateDelayed(); 1.259 + this._updateCrashReportURL(aWindow); 1.260 + }, 1.261 + 1.262 + onTabRemove: function ss_onTabRemove(aWindow, aBrowser, aNoNotification) { 1.263 + aBrowser.removeEventListener("DOMTitleChanged", this, true); 1.264 + 1.265 + // If this browser is being restored, skip any session save activity 1.266 + if (aBrowser.__SS_restore) 1.267 + return; 1.268 + 1.269 + delete aBrowser.__SS_data; 1.270 + 1.271 + if (!aNoNotification) 1.272 + this.saveStateDelayed(); 1.273 + }, 1.274 + 1.275 + onTabClose: function ss_onTabClose(aWindow, aBrowser) { 1.276 + if (this._maxTabsUndo == 0) 1.277 + return; 1.278 + 1.279 + if (aWindow.BrowserApp.tabs.length > 0) { 1.280 + // Bundle this browser's data and extra data and save in the closedTabs 1.281 + // window property 1.282 + let data = aBrowser.__SS_data; 1.283 + data.extData = aBrowser.__SS_extdata; 1.284 + 1.285 + this._windows[aWindow.__SSID].closedTabs.unshift(data); 1.286 + let length = this._windows[aWindow.__SSID].closedTabs.length; 1.287 + if (length > this._maxTabsUndo) 1.288 + this._windows[aWindow.__SSID].closedTabs.splice(this._maxTabsUndo, length - this._maxTabsUndo); 1.289 + } 1.290 + }, 1.291 + 1.292 + onTabLoad: function ss_onTabLoad(aWindow, aBrowser) { 1.293 + // If this browser is being restored, skip any session save activity 1.294 + if (aBrowser.__SS_restore) 1.295 + return; 1.296 + 1.297 + // Ignore a transient "about:blank" 1.298 + if (!aBrowser.canGoBack && aBrowser.currentURI.spec == "about:blank") 1.299 + return; 1.300 + 1.301 + let history = aBrowser.sessionHistory; 1.302 + 1.303 + // Serialize the tab data 1.304 + let entries = []; 1.305 + let index = history.index + 1; 1.306 + for (let i = 0; i < history.count; i++) { 1.307 + let historyEntry = history.getEntryAtIndex(i, false); 1.308 + // Don't try to restore wyciwyg URLs 1.309 + if (historyEntry.URI.schemeIs("wyciwyg")) { 1.310 + // Adjust the index to account for skipped history entries 1.311 + if (i <= history.index) 1.312 + index--; 1.313 + continue; 1.314 + } 1.315 + let entry = this._serializeHistoryEntry(historyEntry); 1.316 + entries.push(entry); 1.317 + } 1.318 + let data = { entries: entries, index: index }; 1.319 + 1.320 + delete aBrowser.__SS_data; 1.321 + this._collectTabData(aWindow, aBrowser, data); 1.322 + this.saveStateDelayed(); 1.323 + 1.324 + this._updateCrashReportURL(aWindow); 1.325 + }, 1.326 + 1.327 + onTabSelect: function ss_onTabSelect(aWindow, aBrowser) { 1.328 + if (this._loadState != STATE_RUNNING) 1.329 + return; 1.330 + 1.331 + let browsers = aWindow.document.getElementById("browsers"); 1.332 + let index = browsers.selectedIndex; 1.333 + this._windows[aWindow.__SSID].selected = parseInt(index) + 1; // 1-based 1.334 + 1.335 + // Restore the resurrected browser 1.336 + if (aBrowser.__SS_restore) { 1.337 + let data = aBrowser.__SS_data; 1.338 + if (data.entries.length > 0) 1.339 + this._restoreHistory(data, aBrowser.sessionHistory); 1.340 + 1.341 + delete aBrowser.__SS_restore; 1.342 + aBrowser.removeAttribute("pending"); 1.343 + } 1.344 + 1.345 + this.saveStateDelayed(); 1.346 + this._updateCrashReportURL(aWindow); 1.347 + }, 1.348 + 1.349 + saveStateDelayed: function ss_saveStateDelayed() { 1.350 + if (!this._saveTimer) { 1.351 + // Interval until the next disk operation is allowed 1.352 + let minimalDelay = this._lastSaveTime + this._interval - Date.now(); 1.353 + 1.354 + // If we have to wait, set a timer, otherwise saveState directly 1.355 + let delay = Math.max(minimalDelay, 2000); 1.356 + if (delay > 0) { 1.357 + this._pendingWrite++; 1.358 + this._saveTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); 1.359 + this._saveTimer.init(this, delay, Ci.nsITimer.TYPE_ONE_SHOT); 1.360 + } else { 1.361 + this.saveState(); 1.362 + } 1.363 + } 1.364 + }, 1.365 + 1.366 + saveState: function ss_saveState() { 1.367 + this._pendingWrite++; 1.368 + this._saveState(true); 1.369 + }, 1.370 + 1.371 + // Immediately and synchronously writes any pending state to disk. 1.372 + flushPendingState: function ss_flushPendingState() { 1.373 + if (this._pendingWrite) { 1.374 + this._saveState(false); 1.375 + } 1.376 + }, 1.377 + 1.378 + _saveState: function ss_saveState(aAsync) { 1.379 + // Kill any queued timer and save immediately 1.380 + if (this._saveTimer) { 1.381 + this._saveTimer.cancel(); 1.382 + this._saveTimer = null; 1.383 + } 1.384 + 1.385 + let data = this._getCurrentState(); 1.386 + let normalData = { windows: [] }; 1.387 + let privateData = { windows: [] }; 1.388 + 1.389 + for (let winIndex = 0; winIndex < data.windows.length; ++winIndex) { 1.390 + let win = data.windows[winIndex]; 1.391 + let normalWin = {}; 1.392 + for (let prop in win) { 1.393 + normalWin[prop] = data[prop]; 1.394 + } 1.395 + normalWin.tabs = []; 1.396 + normalData.windows.push(normalWin); 1.397 + privateData.windows.push({ tabs: [] }); 1.398 + 1.399 + // Split the session data into private and non-private data objects. 1.400 + // Non-private session data will be saved to disk, and private session 1.401 + // data will be sent to Java for Android to hold it in memory. 1.402 + for (let i = 0; i < win.tabs.length; ++i) { 1.403 + let tab = win.tabs[i]; 1.404 + let savedWin = tab.isPrivate ? privateData.windows[winIndex] : normalData.windows[winIndex]; 1.405 + savedWin.tabs.push(tab); 1.406 + if (win.selected == i + 1) { 1.407 + savedWin.selected = savedWin.tabs.length; 1.408 + } 1.409 + } 1.410 + } 1.411 + 1.412 + // Write only non-private data to disk 1.413 + this._writeFile(this._sessionFile, JSON.stringify(normalData), aAsync); 1.414 + 1.415 + // If we have private data, send it to Java; otherwise, send null to 1.416 + // indicate that there is no private data 1.417 + sendMessageToJava({ 1.418 + type: "PrivateBrowsing:Data", 1.419 + session: (privateData.windows[0].tabs.length > 0) ? JSON.stringify(privateData) : null 1.420 + }); 1.421 + 1.422 + this._lastSaveTime = Date.now(); 1.423 + }, 1.424 + 1.425 + _getCurrentState: function ss_getCurrentState() { 1.426 + let self = this; 1.427 + this._forEachBrowserWindow(function(aWindow) { 1.428 + self._collectWindowData(aWindow); 1.429 + }); 1.430 + 1.431 + let data = { windows: [] }; 1.432 + for (let index in this._windows) { 1.433 + data.windows.push(this._windows[index]); 1.434 + } 1.435 + 1.436 + return data; 1.437 + }, 1.438 + 1.439 + _collectTabData: function ss__collectTabData(aWindow, aBrowser, aHistory) { 1.440 + // If this browser is being restored, skip any session save activity 1.441 + if (aBrowser.__SS_restore) 1.442 + return; 1.443 + 1.444 + aHistory = aHistory || { entries: [{ url: aBrowser.currentURI.spec, title: aBrowser.contentTitle }], index: 1 }; 1.445 + 1.446 + let tabData = {}; 1.447 + tabData.entries = aHistory.entries; 1.448 + tabData.index = aHistory.index; 1.449 + tabData.attributes = { image: aBrowser.mIconURL }; 1.450 + tabData.desktopMode = aWindow.BrowserApp.getTabForBrowser(aBrowser).desktopMode; 1.451 + tabData.isPrivate = aBrowser.docShell.QueryInterface(Ci.nsILoadContext).usePrivateBrowsing; 1.452 + 1.453 + aBrowser.__SS_data = tabData; 1.454 + }, 1.455 + 1.456 + _collectWindowData: function ss__collectWindowData(aWindow) { 1.457 + // Ignore windows not tracked by SessionStore 1.458 + if (!aWindow.__SSID || !this._windows[aWindow.__SSID]) 1.459 + return; 1.460 + 1.461 + let winData = this._windows[aWindow.__SSID]; 1.462 + winData.tabs = []; 1.463 + 1.464 + let browsers = aWindow.document.getElementById("browsers"); 1.465 + let index = browsers.selectedIndex; 1.466 + winData.selected = parseInt(index) + 1; // 1-based 1.467 + 1.468 + let tabs = aWindow.BrowserApp.tabs; 1.469 + for (let i = 0; i < tabs.length; i++) { 1.470 + let browser = tabs[i].browser; 1.471 + if (browser.__SS_data) { 1.472 + let tabData = browser.__SS_data; 1.473 + if (browser.__SS_extdata) 1.474 + tabData.extData = browser.__SS_extdata; 1.475 + winData.tabs.push(tabData); 1.476 + } 1.477 + } 1.478 + }, 1.479 + 1.480 + _forEachBrowserWindow: function ss_forEachBrowserWindow(aFunc) { 1.481 + let windowsEnum = Services.wm.getEnumerator("navigator:browser"); 1.482 + while (windowsEnum.hasMoreElements()) { 1.483 + let window = windowsEnum.getNext(); 1.484 + if (window.__SSID && !window.closed) 1.485 + aFunc.call(this, window); 1.486 + } 1.487 + }, 1.488 + 1.489 + _writeFile: function ss_writeFile(aFile, aData, aAsync) { 1.490 + let stateString = Cc["@mozilla.org/supports-string;1"].createInstance(Ci.nsISupportsString); 1.491 + stateString.data = aData; 1.492 + Services.obs.notifyObservers(stateString, "sessionstore-state-write", ""); 1.493 + 1.494 + // Don't touch the file if an observer has deleted all state data 1.495 + if (!stateString.data) 1.496 + return; 1.497 + 1.498 + if (aAsync) { 1.499 + let array = new TextEncoder().encode(aData); 1.500 + let pendingWrite = this._pendingWrite; 1.501 + OS.File.writeAtomic(aFile.path, array, { tmpPath: aFile.path + ".tmp" }).then(function onSuccess() { 1.502 + // Make sure this._pendingWrite is the same value it was before we 1.503 + // fired off the async write. If the count is different, another write 1.504 + // is pending, so we shouldn't reset this._pendingWrite yet. 1.505 + if (pendingWrite === this._pendingWrite) 1.506 + this._pendingWrite = 0; 1.507 + Services.obs.notifyObservers(null, "sessionstore-state-write-complete", ""); 1.508 + }.bind(this)); 1.509 + } else { 1.510 + this._pendingWrite = 0; 1.511 + let foStream = Cc["@mozilla.org/network/file-output-stream;1"]. 1.512 + createInstance(Ci.nsIFileOutputStream); 1.513 + foStream.init(aFile, 0x02 | 0x08 | 0x20, 0666, 0); 1.514 + let converter = Cc["@mozilla.org/intl/converter-output-stream;1"]. 1.515 + createInstance(Ci.nsIConverterOutputStream); 1.516 + converter.init(foStream, "UTF-8", 0, 0); 1.517 + converter.writeString(aData); 1.518 + converter.close(); 1.519 + } 1.520 + }, 1.521 + 1.522 + _updateCrashReportURL: function ss_updateCrashReportURL(aWindow) { 1.523 +#ifdef MOZ_CRASHREPORTER 1.524 + if (!aWindow.BrowserApp.selectedBrowser) 1.525 + return; 1.526 + 1.527 + try { 1.528 + let currentURI = aWindow.BrowserApp.selectedBrowser.currentURI.clone(); 1.529 + // if the current URI contains a username/password, remove it 1.530 + try { 1.531 + currentURI.userPass = ""; 1.532 + } 1.533 + catch (ex) { } // ignore failures on about: URIs 1.534 + 1.535 + CrashReporter.annotateCrashReport("URL", currentURI.spec); 1.536 + } 1.537 + catch (ex) { 1.538 + // don't make noise when crashreporter is built but not enabled 1.539 + if (ex.result != Components.results.NS_ERROR_NOT_INITIALIZED) 1.540 + Components.utils.reportError("SessionStore:" + ex); 1.541 + } 1.542 +#endif 1.543 + }, 1.544 + 1.545 + _serializeHistoryEntry: function _serializeHistoryEntry(aEntry) { 1.546 + let entry = { url: aEntry.URI.spec }; 1.547 + 1.548 + if (aEntry.title && aEntry.title != entry.url) 1.549 + entry.title = aEntry.title; 1.550 + 1.551 + if (!(aEntry instanceof Ci.nsISHEntry)) 1.552 + return entry; 1.553 + 1.554 + let cacheKey = aEntry.cacheKey; 1.555 + if (cacheKey && cacheKey instanceof Ci.nsISupportsPRUint32 && cacheKey.data != 0) 1.556 + entry.cacheKey = cacheKey.data; 1.557 + 1.558 + entry.ID = aEntry.ID; 1.559 + entry.docshellID = aEntry.docshellID; 1.560 + 1.561 + if (aEntry.referrerURI) 1.562 + entry.referrer = aEntry.referrerURI.spec; 1.563 + 1.564 + if (aEntry.contentType) 1.565 + entry.contentType = aEntry.contentType; 1.566 + 1.567 + let x = {}, y = {}; 1.568 + aEntry.getScrollPosition(x, y); 1.569 + if (x.value != 0 || y.value != 0) 1.570 + entry.scroll = x.value + "," + y.value; 1.571 + 1.572 + if (aEntry.owner) { 1.573 + try { 1.574 + let binaryStream = Cc["@mozilla.org/binaryoutputstream;1"].createInstance(Ci.nsIObjectOutputStream); 1.575 + let pipe = Cc["@mozilla.org/pipe;1"].createInstance(Ci.nsIPipe); 1.576 + pipe.init(false, false, 0, 0xffffffff, null); 1.577 + binaryStream.setOutputStream(pipe.outputStream); 1.578 + binaryStream.writeCompoundObject(aEntry.owner, Ci.nsISupports, true); 1.579 + binaryStream.close(); 1.580 + 1.581 + // Now we want to read the data from the pipe's input end and encode it. 1.582 + let scriptableStream = Cc["@mozilla.org/binaryinputstream;1"].createInstance(Ci.nsIBinaryInputStream); 1.583 + scriptableStream.setInputStream(pipe.inputStream); 1.584 + let ownerBytes = scriptableStream.readByteArray(scriptableStream.available()); 1.585 + // We can stop doing base64 encoding once our serialization into JSON 1.586 + // is guaranteed to handle all chars in strings, including embedded 1.587 + // nulls. 1.588 + entry.owner_b64 = btoa(String.fromCharCode.apply(null, ownerBytes)); 1.589 + } catch (e) { dump(e); } 1.590 + } 1.591 + 1.592 + entry.docIdentifier = aEntry.BFCacheEntry.ID; 1.593 + 1.594 + if (aEntry.stateData != null) { 1.595 + entry.structuredCloneState = aEntry.stateData.getDataAsBase64(); 1.596 + entry.structuredCloneVersion = aEntry.stateData.formatVersion; 1.597 + } 1.598 + 1.599 + if (!(aEntry instanceof Ci.nsISHContainer)) 1.600 + return entry; 1.601 + 1.602 + if (aEntry.childCount > 0) { 1.603 + let children = []; 1.604 + for (let i = 0; i < aEntry.childCount; i++) { 1.605 + let child = aEntry.GetChildAt(i); 1.606 + 1.607 + if (child) { 1.608 + // don't try to restore framesets containing wyciwyg URLs (cf. bug 424689 and bug 450595) 1.609 + if (child.URI.schemeIs("wyciwyg")) { 1.610 + children = []; 1.611 + break; 1.612 + } 1.613 + children.push(this._serializeHistoryEntry(child)); 1.614 + } 1.615 + 1.616 + if (children.length) 1.617 + entry.children = children; 1.618 + } 1.619 + } 1.620 + 1.621 + return entry; 1.622 + }, 1.623 + 1.624 + _deserializeHistoryEntry: function _deserializeHistoryEntry(aEntry, aIdMap, aDocIdentMap) { 1.625 + let shEntry = Cc["@mozilla.org/browser/session-history-entry;1"].createInstance(Ci.nsISHEntry); 1.626 + 1.627 + shEntry.setURI(Services.io.newURI(aEntry.url, null, null)); 1.628 + shEntry.setTitle(aEntry.title || aEntry.url); 1.629 + if (aEntry.subframe) 1.630 + shEntry.setIsSubFrame(aEntry.subframe || false); 1.631 + shEntry.loadType = Ci.nsIDocShellLoadInfo.loadHistory; 1.632 + if (aEntry.contentType) 1.633 + shEntry.contentType = aEntry.contentType; 1.634 + if (aEntry.referrer) 1.635 + shEntry.referrerURI = Services.io.newURI(aEntry.referrer, null, null); 1.636 + 1.637 + if (aEntry.cacheKey) { 1.638 + let cacheKey = Cc["@mozilla.org/supports-PRUint32;1"].createInstance(Ci.nsISupportsPRUint32); 1.639 + cacheKey.data = aEntry.cacheKey; 1.640 + shEntry.cacheKey = cacheKey; 1.641 + } 1.642 + 1.643 + if (aEntry.ID) { 1.644 + // get a new unique ID for this frame (since the one from the last 1.645 + // start might already be in use) 1.646 + let id = aIdMap[aEntry.ID] || 0; 1.647 + if (!id) { 1.648 + for (id = Date.now(); id in aIdMap.used; id++); 1.649 + aIdMap[aEntry.ID] = id; 1.650 + aIdMap.used[id] = true; 1.651 + } 1.652 + shEntry.ID = id; 1.653 + } 1.654 + 1.655 + if (aEntry.docshellID) 1.656 + shEntry.docshellID = aEntry.docshellID; 1.657 + 1.658 + if (aEntry.structuredCloneState && aEntry.structuredCloneVersion) { 1.659 + shEntry.stateData = 1.660 + Cc["@mozilla.org/docshell/structured-clone-container;1"]. 1.661 + createInstance(Ci.nsIStructuredCloneContainer); 1.662 + 1.663 + shEntry.stateData.initFromBase64(aEntry.structuredCloneState, aEntry.structuredCloneVersion); 1.664 + } 1.665 + 1.666 + if (aEntry.scroll) { 1.667 + let scrollPos = aEntry.scroll.split(","); 1.668 + scrollPos = [parseInt(scrollPos[0]) || 0, parseInt(scrollPos[1]) || 0]; 1.669 + shEntry.setScrollPosition(scrollPos[0], scrollPos[1]); 1.670 + } 1.671 + 1.672 + let childDocIdents = {}; 1.673 + if (aEntry.docIdentifier) { 1.674 + // If we have a serialized document identifier, try to find an SHEntry 1.675 + // which matches that doc identifier and adopt that SHEntry's 1.676 + // BFCacheEntry. If we don't find a match, insert shEntry as the match 1.677 + // for the document identifier. 1.678 + let matchingEntry = aDocIdentMap[aEntry.docIdentifier]; 1.679 + if (!matchingEntry) { 1.680 + matchingEntry = {shEntry: shEntry, childDocIdents: childDocIdents}; 1.681 + aDocIdentMap[aEntry.docIdentifier] = matchingEntry; 1.682 + } else { 1.683 + shEntry.adoptBFCacheEntry(matchingEntry.shEntry); 1.684 + childDocIdents = matchingEntry.childDocIdents; 1.685 + } 1.686 + } 1.687 + 1.688 + if (aEntry.owner_b64) { 1.689 + let ownerInput = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(Ci.nsIStringInputStream); 1.690 + let binaryData = atob(aEntry.owner_b64); 1.691 + ownerInput.setData(binaryData, binaryData.length); 1.692 + let binaryStream = Cc["@mozilla.org/binaryinputstream;1"].createInstance(Ci.nsIObjectInputStream); 1.693 + binaryStream.setInputStream(ownerInput); 1.694 + try { // Catch possible deserialization exceptions 1.695 + shEntry.owner = binaryStream.readObject(true); 1.696 + } catch (ex) { dump(ex); } 1.697 + } 1.698 + 1.699 + if (aEntry.children && shEntry instanceof Ci.nsISHContainer) { 1.700 + for (let i = 0; i < aEntry.children.length; i++) { 1.701 + if (!aEntry.children[i].url) 1.702 + continue; 1.703 + 1.704 + // We're getting sessionrestore.js files with a cycle in the 1.705 + // doc-identifier graph, likely due to bug 698656. (That is, we have 1.706 + // an entry where doc identifier A is an ancestor of doc identifier B, 1.707 + // and another entry where doc identifier B is an ancestor of A.) 1.708 + // 1.709 + // If we were to respect these doc identifiers, we'd create a cycle in 1.710 + // the SHEntries themselves, which causes the docshell to loop forever 1.711 + // when it looks for the root SHEntry. 1.712 + // 1.713 + // So as a hack to fix this, we restrict the scope of a doc identifier 1.714 + // to be a node's siblings and cousins, and pass childDocIdents, not 1.715 + // aDocIdents, to _deserializeHistoryEntry. That is, we say that two 1.716 + // SHEntries with the same doc identifier have the same document iff 1.717 + // they have the same parent or their parents have the same document. 1.718 + 1.719 + shEntry.AddChild(this._deserializeHistoryEntry(aEntry.children[i], aIdMap, childDocIdents), i); 1.720 + } 1.721 + } 1.722 + 1.723 + return shEntry; 1.724 + }, 1.725 + 1.726 + _restoreHistory: function _restoreHistory(aTabData, aHistory) { 1.727 + if (aHistory.count > 0) 1.728 + aHistory.PurgeHistory(aHistory.count); 1.729 + aHistory.QueryInterface(Ci.nsISHistoryInternal); 1.730 + 1.731 + // helper hashes for ensuring unique frame IDs and unique document 1.732 + // identifiers. 1.733 + let idMap = { used: {} }; 1.734 + let docIdentMap = {}; 1.735 + 1.736 + for (let i = 0; i < aTabData.entries.length; i++) { 1.737 + if (!aTabData.entries[i].url) 1.738 + continue; 1.739 + aHistory.addEntry(this._deserializeHistoryEntry(aTabData.entries[i], idMap, docIdentMap), true); 1.740 + } 1.741 + 1.742 + // We need to force set the active history item and cause it to reload since 1.743 + // we stop the load above 1.744 + let activeIndex = (aTabData.index || aTabData.entries.length) - 1; 1.745 + aHistory.getEntryAtIndex(activeIndex, true); 1.746 + aHistory.QueryInterface(Ci.nsISHistory).reloadCurrentEntry(); 1.747 + }, 1.748 + 1.749 + getBrowserState: function ss_getBrowserState() { 1.750 + return this._getCurrentState(); 1.751 + }, 1.752 + 1.753 + _restoreWindow: function ss_restoreWindow(aData) { 1.754 + let state; 1.755 + try { 1.756 + state = JSON.parse(aData); 1.757 + } catch (e) { 1.758 + Cu.reportError("SessionStore: invalid session JSON"); 1.759 + return false; 1.760 + } 1.761 + 1.762 + // To do a restore, we must have at least one window with one tab 1.763 + if (!state || state.windows.length == 0 || !state.windows[0].tabs || state.windows[0].tabs.length == 0) { 1.764 + Cu.reportError("SessionStore: no tabs to restore"); 1.765 + return false; 1.766 + } 1.767 + 1.768 + let window = Services.wm.getMostRecentWindow("navigator:browser"); 1.769 + 1.770 + let tabs = state.windows[0].tabs; 1.771 + let selected = state.windows[0].selected; 1.772 + if (selected == null || selected > tabs.length) // Clamp the selected index if it's bogus 1.773 + selected = 1; 1.774 + 1.775 + for (let i = 0; i < tabs.length; i++) { 1.776 + let tabData = tabs[i]; 1.777 + let entry = tabData.entries[tabData.index - 1]; 1.778 + 1.779 + // Use stubbed tab if we've already created it; otherwise, make a new tab 1.780 + let tab; 1.781 + if (tabData.tabId == null) { 1.782 + let params = { 1.783 + selected: (selected == i+1), 1.784 + delayLoad: true, 1.785 + title: entry.title, 1.786 + desktopMode: (tabData.desktopMode == true), 1.787 + isPrivate: (tabData.isPrivate == true) 1.788 + }; 1.789 + tab = window.BrowserApp.addTab(entry.url, params); 1.790 + } else { 1.791 + tab = window.BrowserApp.getTabForId(tabData.tabId); 1.792 + delete tabData.tabId; 1.793 + 1.794 + // Don't restore tab if user has closed it 1.795 + if (tab == null) { 1.796 + continue; 1.797 + } 1.798 + } 1.799 + 1.800 + if (window.BrowserApp.selectedTab == tab) { 1.801 + this._restoreHistory(tabData, tab.browser.sessionHistory); 1.802 + delete tab.browser.__SS_restore; 1.803 + tab.browser.removeAttribute("pending"); 1.804 + } else { 1.805 + // Make sure the browser has its session data for the delay reload 1.806 + tab.browser.__SS_data = tabData; 1.807 + tab.browser.__SS_restore = true; 1.808 + tab.browser.setAttribute("pending", "true"); 1.809 + } 1.810 + 1.811 + tab.browser.__SS_extdata = tabData.extData; 1.812 + } 1.813 + 1.814 + return true; 1.815 + }, 1.816 + 1.817 + getClosedTabCount: function ss_getClosedTabCount(aWindow) { 1.818 + if (!aWindow || !aWindow.__SSID || !this._windows[aWindow.__SSID]) 1.819 + return 0; // not a browser window, or not otherwise tracked by SS. 1.820 + 1.821 + return this._windows[aWindow.__SSID].closedTabs.length; 1.822 + }, 1.823 + 1.824 + getClosedTabData: function ss_getClosedTabData(aWindow) { 1.825 + if (!aWindow.__SSID) 1.826 + throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG); 1.827 + 1.828 + return JSON.stringify(this._windows[aWindow.__SSID].closedTabs); 1.829 + }, 1.830 + 1.831 + undoCloseTab: function ss_undoCloseTab(aWindow, aIndex) { 1.832 + if (!aWindow.__SSID) 1.833 + throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG); 1.834 + 1.835 + let closedTabs = this._windows[aWindow.__SSID].closedTabs; 1.836 + if (!closedTabs) 1.837 + return null; 1.838 + 1.839 + // default to the most-recently closed tab 1.840 + aIndex = aIndex || 0; 1.841 + if (!(aIndex in closedTabs)) 1.842 + throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG); 1.843 + 1.844 + // fetch the data of closed tab, while removing it from the array 1.845 + let closedTab = closedTabs.splice(aIndex, 1).shift(); 1.846 + 1.847 + // create a new tab and bring to front 1.848 + let params = { selected: true }; 1.849 + let tab = aWindow.BrowserApp.addTab(closedTab.entries[closedTab.index - 1].url, params); 1.850 + this._restoreHistory(closedTab, tab.browser.sessionHistory); 1.851 + 1.852 + // Put back the extra data 1.853 + tab.browser.__SS_extdata = closedTab.extData; 1.854 + 1.855 + return tab.browser; 1.856 + }, 1.857 + 1.858 + forgetClosedTab: function ss_forgetClosedTab(aWindow, aIndex) { 1.859 + if (!aWindow.__SSID) 1.860 + throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG); 1.861 + 1.862 + let closedTabs = this._windows[aWindow.__SSID].closedTabs; 1.863 + 1.864 + // default to the most-recently closed tab 1.865 + aIndex = aIndex || 0; 1.866 + if (!(aIndex in closedTabs)) 1.867 + throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG); 1.868 + 1.869 + // remove closed tab from the array 1.870 + closedTabs.splice(aIndex, 1); 1.871 + }, 1.872 + 1.873 + getTabValue: function ss_getTabValue(aTab, aKey) { 1.874 + let browser = aTab.browser; 1.875 + let data = browser.__SS_extdata || {}; 1.876 + return data[aKey] || ""; 1.877 + }, 1.878 + 1.879 + setTabValue: function ss_setTabValue(aTab, aKey, aStringValue) { 1.880 + let browser = aTab.browser; 1.881 + 1.882 + if (!browser.__SS_extdata) 1.883 + browser.__SS_extdata = {}; 1.884 + browser.__SS_extdata[aKey] = aStringValue; 1.885 + this.saveStateDelayed(); 1.886 + }, 1.887 + 1.888 + deleteTabValue: function ss_deleteTabValue(aTab, aKey) { 1.889 + let browser = aTab.browser; 1.890 + if (browser.__SS_extdata && browser.__SS_extdata[aKey]) 1.891 + delete browser.__SS_extdata[aKey]; 1.892 + else 1.893 + throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG); 1.894 + }, 1.895 + 1.896 + restoreLastSession: function ss_restoreLastSession(aSessionString) { 1.897 + let self = this; 1.898 + 1.899 + function restoreWindow(data) { 1.900 + if (!self._restoreWindow(data)) { 1.901 + throw "Could not restore window"; 1.902 + } 1.903 + 1.904 + notifyObservers(); 1.905 + } 1.906 + 1.907 + function notifyObservers(aMessage) { 1.908 + Services.obs.notifyObservers(null, "sessionstore-windows-restored", aMessage || ""); 1.909 + } 1.910 + 1.911 + try { 1.912 + // Normally, we'll receive the session string from Java, but there are 1.913 + // cases where we may want to restore that Java cannot detect (e.g., if 1.914 + // browser.sessionstore.resume_session_once is true). In these cases, the 1.915 + // session will be read from sessionstore.bak (which is also used for 1.916 + // "tabs from last time"). 1.917 + if (aSessionString == null) { 1.918 + Task.spawn(function() { 1.919 + let bytes = yield OS.File.read(this._sessionFileBackup.path); 1.920 + let data = JSON.parse(new TextDecoder().decode(bytes) || ""); 1.921 + restoreWindow(data); 1.922 + }.bind(this)).then(null, function onError(reason) { 1.923 + if (reason instanceof OS.File.Error && reason.becauseNoSuchFile) { 1.924 + Cu.reportError("Session file doesn't exist"); 1.925 + } else { 1.926 + Cu.reportError("SessionStore: " + reason.message); 1.927 + } 1.928 + notifyObservers("fail"); 1.929 + }); 1.930 + } else { 1.931 + restoreWindow(aSessionString); 1.932 + } 1.933 + } catch (e) { 1.934 + Cu.reportError("SessionStore: " + e); 1.935 + notifyObservers("fail"); 1.936 + } 1.937 + } 1.938 +}; 1.939 + 1.940 +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([SessionStore]);