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 file, |
michael@0 | 3 | * 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 | this.EXPORTED_SYMBOLS = ["SessionHistory"]; |
michael@0 | 8 | |
michael@0 | 9 | const Cu = Components.utils; |
michael@0 | 10 | const Cc = Components.classes; |
michael@0 | 11 | const Ci = Components.interfaces; |
michael@0 | 12 | |
michael@0 | 13 | Cu.import("resource://gre/modules/Services.jsm"); |
michael@0 | 14 | Cu.import("resource://gre/modules/XPCOMUtils.jsm"); |
michael@0 | 15 | |
michael@0 | 16 | XPCOMUtils.defineLazyModuleGetter(this, "Utils", |
michael@0 | 17 | "resource:///modules/sessionstore/Utils.jsm"); |
michael@0 | 18 | |
michael@0 | 19 | function debug(msg) { |
michael@0 | 20 | Services.console.logStringMessage("SessionHistory: " + msg); |
michael@0 | 21 | } |
michael@0 | 22 | |
michael@0 | 23 | /** |
michael@0 | 24 | * The external API exported by this module. |
michael@0 | 25 | */ |
michael@0 | 26 | this.SessionHistory = Object.freeze({ |
michael@0 | 27 | isEmpty: function (docShell) { |
michael@0 | 28 | return SessionHistoryInternal.isEmpty(docShell); |
michael@0 | 29 | }, |
michael@0 | 30 | |
michael@0 | 31 | collect: function (docShell) { |
michael@0 | 32 | return SessionHistoryInternal.collect(docShell); |
michael@0 | 33 | }, |
michael@0 | 34 | |
michael@0 | 35 | restore: function (docShell, tabData) { |
michael@0 | 36 | SessionHistoryInternal.restore(docShell, tabData); |
michael@0 | 37 | } |
michael@0 | 38 | }); |
michael@0 | 39 | |
michael@0 | 40 | /** |
michael@0 | 41 | * The internal API for the SessionHistory module. |
michael@0 | 42 | */ |
michael@0 | 43 | let SessionHistoryInternal = { |
michael@0 | 44 | /** |
michael@0 | 45 | * Returns whether the given docShell's session history is empty. |
michael@0 | 46 | * |
michael@0 | 47 | * @param docShell |
michael@0 | 48 | * The docShell that owns the session history. |
michael@0 | 49 | */ |
michael@0 | 50 | isEmpty: function (docShell) { |
michael@0 | 51 | let webNavigation = docShell.QueryInterface(Ci.nsIWebNavigation); |
michael@0 | 52 | let history = webNavigation.sessionHistory; |
michael@0 | 53 | if (!webNavigation.currentURI) { |
michael@0 | 54 | return true; |
michael@0 | 55 | } |
michael@0 | 56 | let uri = webNavigation.currentURI.spec; |
michael@0 | 57 | return uri == "about:blank" && history.count == 0; |
michael@0 | 58 | }, |
michael@0 | 59 | |
michael@0 | 60 | /** |
michael@0 | 61 | * Collects session history data for a given docShell. |
michael@0 | 62 | * |
michael@0 | 63 | * @param docShell |
michael@0 | 64 | * The docShell that owns the session history. |
michael@0 | 65 | */ |
michael@0 | 66 | collect: function (docShell) { |
michael@0 | 67 | let data = {entries: []}; |
michael@0 | 68 | let isPinned = docShell.isAppTab; |
michael@0 | 69 | let webNavigation = docShell.QueryInterface(Ci.nsIWebNavigation); |
michael@0 | 70 | let history = webNavigation.sessionHistory; |
michael@0 | 71 | |
michael@0 | 72 | if (history && history.count > 0) { |
michael@0 | 73 | let oldest; |
michael@0 | 74 | let maxSerializeBack = |
michael@0 | 75 | Services.prefs.getIntPref("browser.sessionstore.max_serialize_back"); |
michael@0 | 76 | if (maxSerializeBack >= 0) { |
michael@0 | 77 | oldest = Math.max(0, history.index - maxSerializeBack); |
michael@0 | 78 | } else { // History.getEntryAtIndex(0, ...) is the oldest. |
michael@0 | 79 | oldest = 0; |
michael@0 | 80 | } |
michael@0 | 81 | |
michael@0 | 82 | let newest; |
michael@0 | 83 | let maxSerializeFwd = |
michael@0 | 84 | Services.prefs.getIntPref("browser.sessionstore.max_serialize_forward"); |
michael@0 | 85 | if (maxSerializeFwd >= 0) { |
michael@0 | 86 | newest = Math.min(history.count - 1, history.index + maxSerializeFwd); |
michael@0 | 87 | } else { // History.getEntryAtIndex(history.count - 1, ...) is the newest. |
michael@0 | 88 | newest = history.count - 1; |
michael@0 | 89 | } |
michael@0 | 90 | |
michael@0 | 91 | try { |
michael@0 | 92 | for (let i = oldest; i <= newest; i++) { |
michael@0 | 93 | let shEntry = history.getEntryAtIndex(i, false); |
michael@0 | 94 | let entry = this.serializeEntry(shEntry, isPinned); |
michael@0 | 95 | data.entries.push(entry); |
michael@0 | 96 | } |
michael@0 | 97 | } catch (ex) { |
michael@0 | 98 | // In some cases, getEntryAtIndex will throw. This seems to be due to |
michael@0 | 99 | // history.count being higher than it should be. By doing this in a |
michael@0 | 100 | // try-catch, we'll update history to where it breaks, print an error |
michael@0 | 101 | // message, and still save sessionstore.js. |
michael@0 | 102 | debug("SessionStore failed gathering complete history " + |
michael@0 | 103 | "for the focused window/tab. See bug 669196."); |
michael@0 | 104 | } |
michael@0 | 105 | |
michael@0 | 106 | // Set the one-based index of the currently active tab, |
michael@0 | 107 | // ensuring it isn't out of bounds if an exception was thrown above. |
michael@0 | 108 | data.index = Math.min(history.index - oldest + 1, data.entries.length); |
michael@0 | 109 | } |
michael@0 | 110 | |
michael@0 | 111 | // If either the session history isn't available yet or doesn't have any |
michael@0 | 112 | // valid entries, make sure we at least include the current page. |
michael@0 | 113 | if (data.entries.length == 0) { |
michael@0 | 114 | let uri = webNavigation.currentURI.spec; |
michael@0 | 115 | let body = webNavigation.document.body; |
michael@0 | 116 | // We landed here because the history is inaccessible or there are no |
michael@0 | 117 | // history entries. In that case we should at least record the docShell's |
michael@0 | 118 | // current URL as a single history entry. If the URL is not about:blank |
michael@0 | 119 | // or it's a blank tab that was modified (like a custom newtab page), |
michael@0 | 120 | // record it. For about:blank we explicitly want an empty array without |
michael@0 | 121 | // an 'index' property to denote that there are no history entries. |
michael@0 | 122 | if (uri != "about:blank" || (body && body.hasChildNodes())) { |
michael@0 | 123 | data.entries.push({ url: uri }); |
michael@0 | 124 | data.index = 1; |
michael@0 | 125 | } |
michael@0 | 126 | } |
michael@0 | 127 | |
michael@0 | 128 | return data; |
michael@0 | 129 | }, |
michael@0 | 130 | |
michael@0 | 131 | /** |
michael@0 | 132 | * Determines whether a given session history entry has been added dynamically. |
michael@0 | 133 | * |
michael@0 | 134 | * @param shEntry |
michael@0 | 135 | * The session history entry. |
michael@0 | 136 | * @return bool |
michael@0 | 137 | */ |
michael@0 | 138 | isDynamic: function (shEntry) { |
michael@0 | 139 | // shEntry.isDynamicallyAdded() is true for dynamically added |
michael@0 | 140 | // <iframe> and <frameset>, but also for <html> (the root of the |
michael@0 | 141 | // document) so we use shEntry.parent to ensure that we're not looking |
michael@0 | 142 | // at the root of the document |
michael@0 | 143 | return shEntry.parent && shEntry.isDynamicallyAdded(); |
michael@0 | 144 | }, |
michael@0 | 145 | |
michael@0 | 146 | /** |
michael@0 | 147 | * Get an object that is a serialized representation of a History entry. |
michael@0 | 148 | * |
michael@0 | 149 | * @param shEntry |
michael@0 | 150 | * nsISHEntry instance |
michael@0 | 151 | * @param isPinned |
michael@0 | 152 | * The tab is pinned and should be treated differently for privacy. |
michael@0 | 153 | * @return object |
michael@0 | 154 | */ |
michael@0 | 155 | serializeEntry: function (shEntry, isPinned) { |
michael@0 | 156 | let entry = { url: shEntry.URI.spec }; |
michael@0 | 157 | |
michael@0 | 158 | // Save some bytes and don't include the title property |
michael@0 | 159 | // if that's identical to the current entry's URL. |
michael@0 | 160 | if (shEntry.title && shEntry.title != entry.url) { |
michael@0 | 161 | entry.title = shEntry.title; |
michael@0 | 162 | } |
michael@0 | 163 | if (shEntry.isSubFrame) { |
michael@0 | 164 | entry.subframe = true; |
michael@0 | 165 | } |
michael@0 | 166 | |
michael@0 | 167 | let cacheKey = shEntry.cacheKey; |
michael@0 | 168 | if (cacheKey && cacheKey instanceof Ci.nsISupportsPRUint32 && |
michael@0 | 169 | cacheKey.data != 0) { |
michael@0 | 170 | // XXXbz would be better to have cache keys implement |
michael@0 | 171 | // nsISerializable or something. |
michael@0 | 172 | entry.cacheKey = cacheKey.data; |
michael@0 | 173 | } |
michael@0 | 174 | entry.ID = shEntry.ID; |
michael@0 | 175 | entry.docshellID = shEntry.docshellID; |
michael@0 | 176 | |
michael@0 | 177 | // We will include the property only if it's truthy to save a couple of |
michael@0 | 178 | // bytes when the resulting object is stringified and saved to disk. |
michael@0 | 179 | if (shEntry.referrerURI) |
michael@0 | 180 | entry.referrer = shEntry.referrerURI.spec; |
michael@0 | 181 | |
michael@0 | 182 | if (shEntry.srcdocData) |
michael@0 | 183 | entry.srcdocData = shEntry.srcdocData; |
michael@0 | 184 | |
michael@0 | 185 | if (shEntry.isSrcdocEntry) |
michael@0 | 186 | entry.isSrcdocEntry = shEntry.isSrcdocEntry; |
michael@0 | 187 | |
michael@0 | 188 | if (shEntry.baseURI) |
michael@0 | 189 | entry.baseURI = shEntry.baseURI.spec; |
michael@0 | 190 | |
michael@0 | 191 | if (shEntry.contentType) |
michael@0 | 192 | entry.contentType = shEntry.contentType; |
michael@0 | 193 | |
michael@0 | 194 | let x = {}, y = {}; |
michael@0 | 195 | shEntry.getScrollPosition(x, y); |
michael@0 | 196 | if (x.value != 0 || y.value != 0) |
michael@0 | 197 | entry.scroll = x.value + "," + y.value; |
michael@0 | 198 | |
michael@0 | 199 | // Collect owner data for the current history entry. |
michael@0 | 200 | try { |
michael@0 | 201 | let owner = this.serializeOwner(shEntry); |
michael@0 | 202 | if (owner) { |
michael@0 | 203 | entry.owner_b64 = owner; |
michael@0 | 204 | } |
michael@0 | 205 | } catch (ex) { |
michael@0 | 206 | // Not catching anything specific here, just possible errors |
michael@0 | 207 | // from writeCompoundObject() and the like. |
michael@0 | 208 | debug("Failed serializing owner data: " + ex); |
michael@0 | 209 | } |
michael@0 | 210 | |
michael@0 | 211 | entry.docIdentifier = shEntry.BFCacheEntry.ID; |
michael@0 | 212 | |
michael@0 | 213 | if (shEntry.stateData != null) { |
michael@0 | 214 | entry.structuredCloneState = shEntry.stateData.getDataAsBase64(); |
michael@0 | 215 | entry.structuredCloneVersion = shEntry.stateData.formatVersion; |
michael@0 | 216 | } |
michael@0 | 217 | |
michael@0 | 218 | if (!(shEntry instanceof Ci.nsISHContainer)) { |
michael@0 | 219 | return entry; |
michael@0 | 220 | } |
michael@0 | 221 | |
michael@0 | 222 | if (shEntry.childCount > 0) { |
michael@0 | 223 | let children = []; |
michael@0 | 224 | for (let i = 0; i < shEntry.childCount; i++) { |
michael@0 | 225 | let child = shEntry.GetChildAt(i); |
michael@0 | 226 | |
michael@0 | 227 | if (child && !this.isDynamic(child)) { |
michael@0 | 228 | // Don't try to restore framesets containing wyciwyg URLs. |
michael@0 | 229 | // (cf. bug 424689 and bug 450595) |
michael@0 | 230 | if (child.URI.schemeIs("wyciwyg")) { |
michael@0 | 231 | children.length = 0; |
michael@0 | 232 | break; |
michael@0 | 233 | } |
michael@0 | 234 | |
michael@0 | 235 | children.push(this.serializeEntry(child, isPinned)); |
michael@0 | 236 | } |
michael@0 | 237 | } |
michael@0 | 238 | |
michael@0 | 239 | if (children.length) { |
michael@0 | 240 | entry.children = children; |
michael@0 | 241 | } |
michael@0 | 242 | } |
michael@0 | 243 | |
michael@0 | 244 | return entry; |
michael@0 | 245 | }, |
michael@0 | 246 | |
michael@0 | 247 | /** |
michael@0 | 248 | * Serialize owner data contained in the given session history entry. |
michael@0 | 249 | * |
michael@0 | 250 | * @param shEntry |
michael@0 | 251 | * The session history entry. |
michael@0 | 252 | * @return The base64 encoded owner data. |
michael@0 | 253 | */ |
michael@0 | 254 | serializeOwner: function (shEntry) { |
michael@0 | 255 | if (!shEntry.owner) { |
michael@0 | 256 | return null; |
michael@0 | 257 | } |
michael@0 | 258 | |
michael@0 | 259 | let binaryStream = Cc["@mozilla.org/binaryoutputstream;1"]. |
michael@0 | 260 | createInstance(Ci.nsIObjectOutputStream); |
michael@0 | 261 | let pipe = Cc["@mozilla.org/pipe;1"].createInstance(Ci.nsIPipe); |
michael@0 | 262 | pipe.init(false, false, 0, 0xffffffff, null); |
michael@0 | 263 | binaryStream.setOutputStream(pipe.outputStream); |
michael@0 | 264 | binaryStream.writeCompoundObject(shEntry.owner, Ci.nsISupports, true); |
michael@0 | 265 | binaryStream.close(); |
michael@0 | 266 | |
michael@0 | 267 | // Now we want to read the data from the pipe's input end and encode it. |
michael@0 | 268 | let scriptableStream = Cc["@mozilla.org/binaryinputstream;1"]. |
michael@0 | 269 | createInstance(Ci.nsIBinaryInputStream); |
michael@0 | 270 | scriptableStream.setInputStream(pipe.inputStream); |
michael@0 | 271 | let ownerBytes = |
michael@0 | 272 | scriptableStream.readByteArray(scriptableStream.available()); |
michael@0 | 273 | |
michael@0 | 274 | // We can stop doing base64 encoding once our serialization into JSON |
michael@0 | 275 | // is guaranteed to handle all chars in strings, including embedded |
michael@0 | 276 | // nulls. |
michael@0 | 277 | return btoa(String.fromCharCode.apply(null, ownerBytes)); |
michael@0 | 278 | }, |
michael@0 | 279 | |
michael@0 | 280 | /** |
michael@0 | 281 | * Restores session history data for a given docShell. |
michael@0 | 282 | * |
michael@0 | 283 | * @param docShell |
michael@0 | 284 | * The docShell that owns the session history. |
michael@0 | 285 | * @param tabData |
michael@0 | 286 | * The tabdata including all history entries. |
michael@0 | 287 | */ |
michael@0 | 288 | restore: function (docShell, tabData) { |
michael@0 | 289 | let webNavigation = docShell.QueryInterface(Ci.nsIWebNavigation); |
michael@0 | 290 | let history = webNavigation.sessionHistory; |
michael@0 | 291 | |
michael@0 | 292 | if (history.count > 0) { |
michael@0 | 293 | history.PurgeHistory(history.count); |
michael@0 | 294 | } |
michael@0 | 295 | history.QueryInterface(Ci.nsISHistoryInternal); |
michael@0 | 296 | |
michael@0 | 297 | let idMap = { used: {} }; |
michael@0 | 298 | let docIdentMap = {}; |
michael@0 | 299 | for (let i = 0; i < tabData.entries.length; i++) { |
michael@0 | 300 | //XXXzpao Wallpaper patch for bug 514751 |
michael@0 | 301 | if (!tabData.entries[i].url) |
michael@0 | 302 | continue; |
michael@0 | 303 | history.addEntry(this.deserializeEntry(tabData.entries[i], |
michael@0 | 304 | idMap, docIdentMap), true); |
michael@0 | 305 | } |
michael@0 | 306 | }, |
michael@0 | 307 | |
michael@0 | 308 | /** |
michael@0 | 309 | * Expands serialized history data into a session-history-entry instance. |
michael@0 | 310 | * |
michael@0 | 311 | * @param entry |
michael@0 | 312 | * Object containing serialized history data for a URL |
michael@0 | 313 | * @param idMap |
michael@0 | 314 | * Hash for ensuring unique frame IDs |
michael@0 | 315 | * @param docIdentMap |
michael@0 | 316 | * Hash to ensure reuse of BFCache entries |
michael@0 | 317 | * @returns nsISHEntry |
michael@0 | 318 | */ |
michael@0 | 319 | deserializeEntry: function (entry, idMap, docIdentMap) { |
michael@0 | 320 | |
michael@0 | 321 | var shEntry = Cc["@mozilla.org/browser/session-history-entry;1"]. |
michael@0 | 322 | createInstance(Ci.nsISHEntry); |
michael@0 | 323 | |
michael@0 | 324 | shEntry.setURI(Utils.makeURI(entry.url)); |
michael@0 | 325 | shEntry.setTitle(entry.title || entry.url); |
michael@0 | 326 | if (entry.subframe) |
michael@0 | 327 | shEntry.setIsSubFrame(entry.subframe || false); |
michael@0 | 328 | shEntry.loadType = Ci.nsIDocShellLoadInfo.loadHistory; |
michael@0 | 329 | if (entry.contentType) |
michael@0 | 330 | shEntry.contentType = entry.contentType; |
michael@0 | 331 | if (entry.referrer) |
michael@0 | 332 | shEntry.referrerURI = Utils.makeURI(entry.referrer); |
michael@0 | 333 | if (entry.isSrcdocEntry) |
michael@0 | 334 | shEntry.srcdocData = entry.srcdocData; |
michael@0 | 335 | if (entry.baseURI) |
michael@0 | 336 | shEntry.baseURI = Utils.makeURI(entry.baseURI); |
michael@0 | 337 | |
michael@0 | 338 | if (entry.cacheKey) { |
michael@0 | 339 | var cacheKey = Cc["@mozilla.org/supports-PRUint32;1"]. |
michael@0 | 340 | createInstance(Ci.nsISupportsPRUint32); |
michael@0 | 341 | cacheKey.data = entry.cacheKey; |
michael@0 | 342 | shEntry.cacheKey = cacheKey; |
michael@0 | 343 | } |
michael@0 | 344 | |
michael@0 | 345 | if (entry.ID) { |
michael@0 | 346 | // get a new unique ID for this frame (since the one from the last |
michael@0 | 347 | // start might already be in use) |
michael@0 | 348 | var id = idMap[entry.ID] || 0; |
michael@0 | 349 | if (!id) { |
michael@0 | 350 | for (id = Date.now(); id in idMap.used; id++); |
michael@0 | 351 | idMap[entry.ID] = id; |
michael@0 | 352 | idMap.used[id] = true; |
michael@0 | 353 | } |
michael@0 | 354 | shEntry.ID = id; |
michael@0 | 355 | } |
michael@0 | 356 | |
michael@0 | 357 | if (entry.docshellID) |
michael@0 | 358 | shEntry.docshellID = entry.docshellID; |
michael@0 | 359 | |
michael@0 | 360 | if (entry.structuredCloneState && entry.structuredCloneVersion) { |
michael@0 | 361 | shEntry.stateData = |
michael@0 | 362 | Cc["@mozilla.org/docshell/structured-clone-container;1"]. |
michael@0 | 363 | createInstance(Ci.nsIStructuredCloneContainer); |
michael@0 | 364 | |
michael@0 | 365 | shEntry.stateData.initFromBase64(entry.structuredCloneState, |
michael@0 | 366 | entry.structuredCloneVersion); |
michael@0 | 367 | } |
michael@0 | 368 | |
michael@0 | 369 | if (entry.scroll) { |
michael@0 | 370 | var scrollPos = (entry.scroll || "0,0").split(","); |
michael@0 | 371 | scrollPos = [parseInt(scrollPos[0]) || 0, parseInt(scrollPos[1]) || 0]; |
michael@0 | 372 | shEntry.setScrollPosition(scrollPos[0], scrollPos[1]); |
michael@0 | 373 | } |
michael@0 | 374 | |
michael@0 | 375 | let childDocIdents = {}; |
michael@0 | 376 | if (entry.docIdentifier) { |
michael@0 | 377 | // If we have a serialized document identifier, try to find an SHEntry |
michael@0 | 378 | // which matches that doc identifier and adopt that SHEntry's |
michael@0 | 379 | // BFCacheEntry. If we don't find a match, insert shEntry as the match |
michael@0 | 380 | // for the document identifier. |
michael@0 | 381 | let matchingEntry = docIdentMap[entry.docIdentifier]; |
michael@0 | 382 | if (!matchingEntry) { |
michael@0 | 383 | matchingEntry = {shEntry: shEntry, childDocIdents: childDocIdents}; |
michael@0 | 384 | docIdentMap[entry.docIdentifier] = matchingEntry; |
michael@0 | 385 | } |
michael@0 | 386 | else { |
michael@0 | 387 | shEntry.adoptBFCacheEntry(matchingEntry.shEntry); |
michael@0 | 388 | childDocIdents = matchingEntry.childDocIdents; |
michael@0 | 389 | } |
michael@0 | 390 | } |
michael@0 | 391 | |
michael@0 | 392 | if (entry.owner_b64) { |
michael@0 | 393 | var ownerInput = Cc["@mozilla.org/io/string-input-stream;1"]. |
michael@0 | 394 | createInstance(Ci.nsIStringInputStream); |
michael@0 | 395 | var binaryData = atob(entry.owner_b64); |
michael@0 | 396 | ownerInput.setData(binaryData, binaryData.length); |
michael@0 | 397 | var binaryStream = Cc["@mozilla.org/binaryinputstream;1"]. |
michael@0 | 398 | createInstance(Ci.nsIObjectInputStream); |
michael@0 | 399 | binaryStream.setInputStream(ownerInput); |
michael@0 | 400 | try { // Catch possible deserialization exceptions |
michael@0 | 401 | shEntry.owner = binaryStream.readObject(true); |
michael@0 | 402 | } catch (ex) { debug(ex); } |
michael@0 | 403 | } |
michael@0 | 404 | |
michael@0 | 405 | if (entry.children && shEntry instanceof Ci.nsISHContainer) { |
michael@0 | 406 | for (var i = 0; i < entry.children.length; i++) { |
michael@0 | 407 | //XXXzpao Wallpaper patch for bug 514751 |
michael@0 | 408 | if (!entry.children[i].url) |
michael@0 | 409 | continue; |
michael@0 | 410 | |
michael@0 | 411 | // We're getting sessionrestore.js files with a cycle in the |
michael@0 | 412 | // doc-identifier graph, likely due to bug 698656. (That is, we have |
michael@0 | 413 | // an entry where doc identifier A is an ancestor of doc identifier B, |
michael@0 | 414 | // and another entry where doc identifier B is an ancestor of A.) |
michael@0 | 415 | // |
michael@0 | 416 | // If we were to respect these doc identifiers, we'd create a cycle in |
michael@0 | 417 | // the SHEntries themselves, which causes the docshell to loop forever |
michael@0 | 418 | // when it looks for the root SHEntry. |
michael@0 | 419 | // |
michael@0 | 420 | // So as a hack to fix this, we restrict the scope of a doc identifier |
michael@0 | 421 | // to be a node's siblings and cousins, and pass childDocIdents, not |
michael@0 | 422 | // aDocIdents, to _deserializeHistoryEntry. That is, we say that two |
michael@0 | 423 | // SHEntries with the same doc identifier have the same document iff |
michael@0 | 424 | // they have the same parent or their parents have the same document. |
michael@0 | 425 | |
michael@0 | 426 | shEntry.AddChild(this.deserializeEntry(entry.children[i], idMap, |
michael@0 | 427 | childDocIdents), i); |
michael@0 | 428 | } |
michael@0 | 429 | } |
michael@0 | 430 | |
michael@0 | 431 | return shEntry; |
michael@0 | 432 | }, |
michael@0 | 433 | |
michael@0 | 434 | }; |