Wed, 31 Dec 2014 07:53:36 +0100
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 });