/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ var EXPORTED_SYMBOLS = ["init", "map"]; var Cc = Components.classes; var Ci = Components.interfaces; var Cu = Components.utils; // imports var utils = {}; Cu.import('resource://mozmill/stdlib/utils.js', utils); var uuidgen = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator); /** * The window map is used to store information about the current state of * open windows, e.g. loaded state */ var map = { _windows : { }, /** * Check if a given window id is contained in the map of windows * * @param {Number} aWindowId * Outer ID of the window to check. * @returns {Boolean} True if the window is part of the map, otherwise false. */ contains : function (aWindowId) { return (aWindowId in this._windows); }, /** * Retrieve the value of the specified window's property. * * @param {Number} aWindowId * Outer ID of the window to check. * @param {String} aProperty * Property to retrieve the value from * @return {Object} Value of the window's property */ getValue : function (aWindowId, aProperty) { if (!this.contains(aWindowId)) { return undefined; } else { var win = this._windows[aWindowId]; return (aProperty in win) ? win[aProperty] : undefined; } }, /** * Remove the entry for a given window * * @param {Number} aWindowId * Outer ID of the window to check. */ remove : function (aWindowId) { if (this.contains(aWindowId)) { delete this._windows[aWindowId]; } // dump("* current map: " + JSON.stringify(this._windows) + "\n"); }, /** * Update the property value of a given window * * @param {Number} aWindowId * Outer ID of the window to check. * @param {String} aProperty * Property to update the value for * @param {Object} * Value to set */ update : function (aWindowId, aProperty, aValue) { if (!this.contains(aWindowId)) { this._windows[aWindowId] = { }; } this._windows[aWindowId][aProperty] = aValue; // dump("* current map: " + JSON.stringify(this._windows) + "\n"); }, /** * Update the internal loaded state of the given content window. To identify * an active (re)load action we make use of an uuid. * * @param {Window} aId - The outer id of the window to update * @param {Boolean} aIsLoaded - Has the window been loaded */ updatePageLoadStatus : function (aId, aIsLoaded) { this.update(aId, "loaded", aIsLoaded); var uuid = this.getValue(aId, "id_load_in_transition"); // If no uuid has been set yet or when the page gets unloaded create a new id if (!uuid || !aIsLoaded) { uuid = uuidgen.generateUUID(); this.update(aId, "id_load_in_transition", uuid); } // dump("*** Page status updated: id=" + aId + ", loaded=" + aIsLoaded + ", uuid=" + uuid + "\n"); }, /** * This method only applies to content windows, where we have to check if it has * been successfully loaded or reloaded. An uuid allows us to wait for the next * load action triggered by e.g. controller.open(). * * @param {Window} aId - The outer id of the content window to check * * @returns {Boolean} True if the content window has been loaded */ hasPageLoaded : function (aId) { var load_current = this.getValue(aId, "id_load_in_transition"); var load_handled = this.getValue(aId, "id_load_handled"); var isLoaded = this.contains(aId) && this.getValue(aId, "loaded") && (load_current !== load_handled); if (isLoaded) { // Backup the current uuid so we can check later if another page load happened. this.update(aId, "id_load_handled", load_current); } // dump("** Page has been finished loading: id=" + aId + ", status=" + isLoaded + ", uuid=" + load_current + "\n"); return isLoaded; } }; // Observer when a new top-level window is ready var windowReadyObserver = { observe: function (aSubject, aTopic, aData) { // Not in all cases we get a ChromeWindow. So ensure we really operate // on such an instance. Otherwise load events will not be handled. var win = utils.getChromeWindow(aSubject); // var id = utils.getWindowId(win); // dump("*** 'toplevel-window-ready' observer notification: id=" + id + "\n"); attachEventListeners(win); } }; // Observer when a top-level window is closed var windowCloseObserver = { observe: function (aSubject, aTopic, aData) { var id = utils.getWindowId(aSubject); // dump("*** 'outer-window-destroyed' observer notification: id=" + id + "\n"); map.remove(id); } }; // Bug 915554 // Support for the old Private Browsing Mode (eg. ESR17) // TODO: remove once ESR17 is no longer supported var enterLeavePrivateBrowsingObserver = { observe: function (aSubject, aTopic, aData) { handleAttachEventListeners(); } }; /** * Attach event listeners * * @param {ChromeWindow} aWindow * Window to attach listeners on. */ function attachEventListeners(aWindow) { // These are the event handlers var pageShowHandler = function (aEvent) { var doc = aEvent.originalTarget; // Only update the flag if we have a document as target // see https://bugzilla.mozilla.org/show_bug.cgi?id=690829 if ("defaultView" in doc) { var id = utils.getWindowId(doc.defaultView); // dump("*** 'pageshow' event: id=" + id + ", baseURI=" + doc.baseURI + "\n"); map.updatePageLoadStatus(id, true); } // We need to add/remove the unload/pagehide event listeners to preserve caching. aWindow.addEventListener("beforeunload", beforeUnloadHandler, true); aWindow.addEventListener("pagehide", pageHideHandler, true); }; var DOMContentLoadedHandler = function (aEvent) { var doc = aEvent.originalTarget; // Only update the flag if we have a document as target if ("defaultView" in doc) { var id = utils.getWindowId(doc.defaultView); // dump("*** 'DOMContentLoaded' event: id=" + id + ", baseURI=" + doc.baseURI + "\n"); // We only care about error pages for DOMContentLoaded var errorRegex = /about:.+(error)|(blocked)\?/; if (errorRegex.exec(doc.baseURI)) { // Wait about 1s to be sure the DOM is ready utils.sleep(1000); map.updatePageLoadStatus(id, true); } // We need to add/remove the unload event listener to preserve caching. aWindow.addEventListener("beforeunload", beforeUnloadHandler, true); } }; // beforeunload is still needed because pagehide doesn't fire before the page is unloaded. // still use pagehide for cases when beforeunload doesn't get fired var beforeUnloadHandler = function (aEvent) { var doc = aEvent.originalTarget; // Only update the flag if we have a document as target if ("defaultView" in doc) { var id = utils.getWindowId(doc.defaultView); // dump("*** 'beforeunload' event: id=" + id + ", baseURI=" + doc.baseURI + "\n"); map.updatePageLoadStatus(id, false); } aWindow.removeEventListener("beforeunload", beforeUnloadHandler, true); }; var pageHideHandler = function (aEvent) { var doc = aEvent.originalTarget; // Only update the flag if we have a document as target if ("defaultView" in doc) { var id = utils.getWindowId(doc.defaultView); // dump("*** 'pagehide' event: id=" + id + ", baseURI=" + doc.baseURI + "\n"); map.updatePageLoadStatus(id, false); } // If event.persisted is true the beforeUnloadHandler would never fire // and we have to remove the event handler here to avoid memory leaks. if (aEvent.persisted) aWindow.removeEventListener("beforeunload", beforeUnloadHandler, true); }; var onWindowLoaded = function (aEvent) { var id = utils.getWindowId(aWindow); // dump("*** 'load' event: id=" + id + ", baseURI=" + aWindow.document.baseURI + "\n"); map.update(id, "loaded", true); // Note: Error pages will never fire a "pageshow" event. For those we // have to wait for the "DOMContentLoaded" event. That's the final state. // Error pages will always have a baseURI starting with // "about:" followed by "error" or "blocked". aWindow.addEventListener("DOMContentLoaded", DOMContentLoadedHandler, true); // Page is ready aWindow.addEventListener("pageshow", pageShowHandler, true); // Leave page (use caching) aWindow.addEventListener("pagehide", pageHideHandler, true); }; // If the window has already been finished loading, call the load handler // directly. Otherwise attach it to the current window. if (aWindow.document.readyState === 'complete') { onWindowLoaded(); } else { aWindow.addEventListener("load", onWindowLoaded, false); } } // Attach event listeners to all already open top-level windows function handleAttachEventListeners() { var enumerator = Cc["@mozilla.org/appshell/window-mediator;1"]. getService(Ci.nsIWindowMediator).getEnumerator(""); while (enumerator.hasMoreElements()) { var win = enumerator.getNext(); attachEventListeners(win); } } function init() { // Activate observer for new top level windows var observerService = Cc["@mozilla.org/observer-service;1"]. getService(Ci.nsIObserverService); observerService.addObserver(windowReadyObserver, "toplevel-window-ready", false); observerService.addObserver(windowCloseObserver, "outer-window-destroyed", false); observerService.addObserver(enterLeavePrivateBrowsingObserver, "private-browsing", false); handleAttachEventListeners(); }