From 6c17af6214054657ed3886f4fcd40aed3f043804 Mon Sep 17 00:00:00 2001 From: Rob Campbell Date: Wed, 3 Oct 2012 13:41:00 -0400 Subject: [PATCH 01/18] Bug 797527 - Remove InsideOutBox.jsm, DOMPlate.jsm and friends; r=dcamp --- browser/devtools/highlighter/InsideOutBox.jsm | 804 ------- browser/devtools/highlighter/Makefile.in | 3 - browser/devtools/highlighter/TreePanel.jsm | 844 -------- browser/devtools/highlighter/domplate.jsm | 1891 ----------------- browser/devtools/highlighter/inspector.html | 18 - browser/devtools/highlighter/inspector.jsm | 2 +- browser/devtools/jar.mn | 1 - browser/devtools/shared/DOMHelpers.jsm | 124 ++ .../themes/gnomestripe/devtools/htmlpanel.css | 402 ---- browser/themes/gnomestripe/jar.mn | 1 - .../themes/pinstripe/devtools/htmlpanel.css | 392 ---- browser/themes/pinstripe/jar.mn | 1 - .../themes/winstripe/devtools/htmlpanel.css | 377 ---- browser/themes/winstripe/jar.mn | 2 - 14 files changed, 125 insertions(+), 4737 deletions(-) delete mode 100644 browser/devtools/highlighter/InsideOutBox.jsm delete mode 100644 browser/devtools/highlighter/TreePanel.jsm delete mode 100644 browser/devtools/highlighter/domplate.jsm delete mode 100644 browser/devtools/highlighter/inspector.html create mode 100644 browser/devtools/shared/DOMHelpers.jsm delete mode 100644 browser/themes/gnomestripe/devtools/htmlpanel.css delete mode 100644 browser/themes/pinstripe/devtools/htmlpanel.css delete mode 100644 browser/themes/winstripe/devtools/htmlpanel.css diff --git a/browser/devtools/highlighter/InsideOutBox.jsm b/browser/devtools/highlighter/InsideOutBox.jsm deleted file mode 100644 index b15dd5ebf873..000000000000 --- a/browser/devtools/highlighter/InsideOutBox.jsm +++ /dev/null @@ -1,804 +0,0 @@ -/* - * Software License Agreement (BSD License) - * - * Copyright (c) 2007, Parakey Inc. - * All rights reserved. - * - * Redistribution and use of this software in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * - * * Redistributions of source code must retain the above - * copyright notice, this list of conditions and the - * following disclaimer. - * - * * Redistributions in binary form must reproduce the above - * copyright notice, this list of conditions and the - * following disclaimer in the documentation and/or other - * materials provided with the distribution. - * - * * Neither the name of Parakey Inc. nor the names of its - * contributors may be used to endorse or promote products - * derived from this software without specific prior - * written permission of Parakey Inc. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR - * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND - * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR - * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL - * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER - * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT - * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -/* - * Creator: - * Joe Hewitt - * Contributors - * John J. Barton (IBM Almaden) - * Jan Odvarko (Mozilla Corp.) - * Max Stepanov (Aptana Inc.) - * Rob Campbell (Mozilla Corp.) - * Hans Hillen (Paciello Group, Mozilla) - * Curtis Bartley (Mozilla Corp.) - * Mike Collins (IBM Almaden) - * Kevin Decker - * Mike Ratcliffe (Comartis AG) - * Hernan Rodríguez Colmeiro - * Austin Andrews - * Christoph Dorn - * Steven Roussey (AppCenter Inc, Network54) - */ - -/////////////////////////////////////////////////////////////////////////// -//// InsideOutBox - -/** - * InsideOutBoxView is a simple interface definition for views implementing - * InsideOutBox controls. All implementors must define these methods. - * Implemented in InspectorUI. - */ - -/* -InsideOutBoxView = { - // - * Retrieves the parent object for a given child object. - * @param aChild - * The child node to retrieve the parent object for. - * @returns a DOM node | null - // - getParentObject: function(aChild) {}, - - // - * Retrieves a given child node. - * - * If both index and previousSibling are passed, the implementation - * may assume that previousSibling will be the return for getChildObject - * with index-1. - * @param aParent - * The parent object of the child object to retrieve. - * @param aIndex - * The index of the child object to retrieve from aParent. - * @param aPreviousSibling - * The previous sibling of the child object to retrieve. - * Supercedes aIndex. - * @returns a DOM object | null - // - getChildObject: function(aParent, aIndex, aPreviousSibling) {}, - - // - * Renders the HTML representation of the object. Should return an HTML - * object which will be displayed to the user. - * @param aObject - * The object to create the box object for. - * @param aIsRoot - * Is the object the root object. May not be used in all - * implementations. - * @returns an object box | null - // - createObjectBox: function(aObject, aIsRoot) {}, - - // - * Convenience wrappers for classList API. - * @param aObject - * DOM node to query/set. - * @param aClassName - * String containing the class name to query/set. - // - hasClass: function(aObject, aClassName) {}, - addClass: function(aObject, aClassName) {}, - removeClass: function(aObject, aClassName) {} -}; -*/ - -/** - * Creates a tree based on objects provided by a separate "view" object. - * - * Construction uses an "inside-out" algorithm, meaning that the view's job is - * first to tell us the ancestry of each object, and secondarily its - * descendants. - * - * Constructor - * @param aView - * The view requiring the InsideOutBox. - * @param aBox - * The box object containing the InsideOutBox. Required to add/remove - * children during box manipulation (toggling opened or closed). - */ - -var EXPORTED_SYMBOLS = ["InsideOutBox"]; - -const Cu = Components.utils; -Cu.import("resource:///modules/devtools/LayoutHelpers.jsm"); - -function InsideOutBox(aView, aBox) -{ - this.view = aView; - this.box = aBox; - - this.rootObject = null; - - this.rootObjectBox = null; - this.selectedObjectBox = null; - this.highlightedObjectBox = null; - this.scrollIntoView = false; -}; - -InsideOutBox.prototype = -{ - /** - * Highlight the given object node in the tree. - * @param aObject - * the object to highlight. - * @returns objectBox - */ - highlight: function IOBox_highlight(aObject) - { - let objectBox = this.createObjectBox(aObject); - this.highlightObjectBox(objectBox); - return objectBox; - }, - - /** - * Open the given object node in the tree. - * @param aObject - * The object node to open. - * @returns objectBox - */ - openObject: function IOBox_openObject(aObject) - { - let object = aObject; - let firstChild = this.view.getChildObject(object, 0); - if (firstChild) - object = firstChild; - - return this.openToObject(object); - }, - - /** - * Open the tree up to the given object node. - * @param aObject - * The object in the tree to open to. - * @returns objectBox - */ - openToObject: function IOBox_openToObject(aObject) - { - let objectBox = this.createObjectBox(aObject); - this.openObjectBox(objectBox); - return objectBox; - }, - - /** - * Select the given object node in the tree. - * @param aObject - * The object node to select. - * @param makeBoxVisible - * Boolean. Open the object box in the tree? - * @param forceOpen - * Force the object box open by expanding all elements in the tree? - * @param scrollIntoView - * Scroll the objectBox into view? - * @returns nsIDOMNode|null - * A DOM node that represents the "object box", the element that - * holds/displays the given aObject representation in the tree. If - * the object cannot be selected, if it is a stale object, null is - * returned. - */ - select: - function IOBox_select(aObject, makeBoxVisible, forceOpen, scrollIntoView) - { - let objectBox = this.createObjectBox(aObject); - if (!objectBox) { - return null; - } - this.selectObjectBox(objectBox, forceOpen); - if (makeBoxVisible) { - this.openObjectBox(objectBox); - } - if (scrollIntoView) { - // We want to center the label of the element, not the whole tag - // (which includes all of its children, and is vertically huge). - LayoutHelpers.scrollIntoViewIfNeeded(objectBox.firstElementChild); - } - - return objectBox; - }, - - /** - * Expands/contracts the given object, depending on its state. - * @param aObject - * The tree node to expand/contract. - */ - toggleObject: function IOBox_toggleObject(aObject) - { - let box = this.createObjectBox(aObject); - if (!(this.view.hasClass(box, "open"))) - this.expandObjectBox(box); - else - this.contractObjectBox(box); - }, - - /** - * Expand the given object in the tree. - * @param aObject - * The tree node to expand. - */ - expandObject: function IOBox_expandObject(aObject) - { - let objectBox = this.createObjectBox(aObject); - if (objectBox) - this.expandObjectBox(objectBox); - }, - - /** - * Contract the given object in the tree. - * @param aObject - * The tree node to contract. - */ - contractObject: function IOBox_contractObject(aObject) - { - let objectBox = this.createObjectBox(aObject); - if (objectBox) - this.contractObjectBox(objectBox); - }, - - /** - * General method for iterating over an object's ancestors and performing - * some function. - * @param aObject - * The object whose ancestors we wish to iterate over. - * @param aCallback - * The function to call with the object as argument. - */ - - iterateObjectAncestors: function IOBox_iterateObjectAncesors(aObject, aCallback) - { - let object = aObject; - if (!(aCallback && typeof(aCallback) == "function")) { - this.view._log("Illegal argument in IOBox.iterateObjectAncestors"); - return; - } - while ((object = this.getParentObjectBox(object))) - aCallback(object); - }, - - /** - * Highlight the given objectBox in the tree. - * @param aObjectBox - * The objectBox to highlight. - */ - highlightObjectBox: function IOBox_highlightObjectBox(aObjectBox) - { - let self = this; - - if (!aObjectBox) - return; - - if (this.highlightedObjectBox) { - this.view.removeClass(this.highlightedObjectBox, "highlighted"); - this.iterateObjectAncestors(this.highlightedObjectBox, function (box) { - self.view.removeClass(box, "highlightOpen"); - }); - } - - this.highlightedObjectBox = aObjectBox; - - this.view.addClass(aObjectBox, "highlighted"); - this.iterateObjectAncestors(this.highlightedObjectBox, function (box) { - self.view.addClass(box, "highlightOpen"); - }); - - aObjectBox.scrollIntoView(true); - }, - - /** - * Select the given objectBox in the tree, forcing it to be open if necessary. - * @param aObjectBox - * The objectBox to select. - * @param forceOpen - * Force the box (subtree) to be open? - */ - selectObjectBox: function IOBox_selectObjectBox(aObjectBox, forceOpen) - { - let isSelected = this.selectedObjectBox && - aObjectBox == this.selectedObjectBox; - - // aObjectBox is already selected, return - if (isSelected) - return; - - if (this.selectedObjectBox) - this.view.removeClass(this.selectedObjectBox, "selected"); - - this.selectedObjectBox = aObjectBox; - - if (aObjectBox) { - this.view.addClass(aObjectBox, "selected"); - - // Force it open the first time it is selected - if (forceOpen) - this.expandObjectBox(aObjectBox, true); - } - }, - - /** - * Returns the next object box in the tree for navigation purposes. - */ - nextObjectBox: function IOBox_nextObjectBox(aBoxObject) - { - let candidate; - let boxObject = aBoxObject || this.selectedObjectBox; - if (!boxObject) - return this.rootObjectBox; - - // If expanded, return the first child. - let isOpen = this.view.hasClass(boxObject, "open"); - let childObjectBox = this.getChildObjectBox(boxObject); - if (isOpen && childObjectBox && childObjectBox.firstChild) { - candidate = childObjectBox.firstChild; - } else { - // Otherwise we get the next available sibling. - while (boxObject) { - if (boxObject.nextSibling) { - boxObject = boxObject.nextSibling; - break; - } - boxObject = this.getParentObjectBox(boxObject); - } - candidate = boxObject; - } - - // If the node is not an element (comments or text nodes), we - // jump to the next line. - if (candidate && - candidate.repObject.nodeType != candidate.repObject.ELEMENT_NODE) { - return this.nextObjectBox(candidate); - } - - return candidate; - }, - - /** - * Returns the next object in the tree for navigation purposes. - */ - nextObject: function IOBox_nextObject() - { - let next = this.nextObjectBox(); - return next ? next.repObject : null; - }, - - /** - * Returns the object that is below the selection. - * - * @param aDistance Number of lines to jump. - */ - farNextObject: function IOBox_farPreviousProject(aDistance) - { - let boxObject = this.selectedObjectBox; - while (aDistance-- > 0) { - let newBoxObject = this.nextObjectBox(boxObject); - if (!newBoxObject) { - break; - } - boxObject = newBoxObject; - } - return boxObject ? boxObject.repObject : null; - }, - - /** - * Returns the last visible child box of an object box. - */ - lastVisible: function IOBox_lastVisibleChild(aNode) - { - if (!this.view.hasClass(aNode, "open")) - return aNode; - - let childBox = this.getChildObjectBox(aNode); - if (!childBox || !childBox.lastChild) - return aNode; - - return this.lastVisible(childBox.lastChild); - }, - - /** - * Returns the previous object box in the tree for navigation purposes. - */ - previousObjectBox: function IOBox_previousObjectBox(aBoxObject) - { - let boxObject = aBoxObject || this.selectedObjectBox; - if (!boxObject) - return this.rootObjectBox; - - let candidate; - let sibling = boxObject.previousSibling; - if (sibling) { - candidate = this.lastVisible(sibling); - } else { - candidate = this.getParentObjectBox(boxObject); - } - - // If the node is not an element (comments or text nodes), we - // jump to the previous line. - if (candidate && - candidate.repObject.nodeType != candidate.repObject.ELEMENT_NODE) { - return this.previousObjectBox(candidate); - } - - return candidate; - }, - - /** - * Returns the previous object in the tree for navigation purposes. - */ - previousObject: function IOBox_previousObject() - { - let boxObject = this.previousObjectBox(); - return boxObject ? boxObject.repObject : null; - }, - - /** - * Returns the object that is above the selection. - * - * @param aDistance Number of lines to jump. - */ - farPreviousObject: function IOBox_farPreviousProject(aDistance) - { - let boxObject = this.selectedObjectBox; - while (aDistance-- > 0) { - let newBoxObject = this.previousObjectBox(boxObject); - if (!newBoxObject) { - break; - } - boxObject = newBoxObject; - if (boxObject === this.rootObjectBox) - break; - } - return boxObject ? boxObject.repObject : null; - }, - - /** - * Open the ancestors of the given object box. - * @param aObjectBox - * The object box to open. - */ - openObjectBox: function IOBox_openObjectBox(aObjectBox) - { - if (!aObjectBox) - return; - - let self = this; - this.iterateObjectAncestors(aObjectBox, function (box) { - self.view.addClass(box, "open"); - let labelBox = box.querySelector(".nodeLabelBox"); - if (labelBox) - labelBox.setAttribute("aria-expanded", "true"); - }); - }, - - /** - * Expand the given object box. - * @param aObjectBox - * The object box to expand. - */ - expandObjectBox: function IOBox_expandObjectBox(aObjectBox) - { - let nodeChildBox = this.getChildObjectBox(aObjectBox); - - // no children means nothing to expand, return - if (!nodeChildBox) - return; - - if (!aObjectBox.populated) { - let firstChild = this.view.getChildObject(aObjectBox.repObject, 0); - this.populateChildBox(firstChild, nodeChildBox); - } - let labelBox = aObjectBox.querySelector(".nodeLabelBox"); - if (labelBox) - labelBox.setAttribute("aria-expanded", "true"); - this.view.addClass(aObjectBox, "open"); - }, - - /** - * Contract the given object box. - * @param aObjectBox - * The object box to contract. - */ - contractObjectBox: function IOBox_contractObjectBox(aObjectBox) - { - this.view.removeClass(aObjectBox, "open"); - let nodeLabel = aObjectBox.querySelector(".nodeLabel"); - let labelBox = nodeLabel.querySelector(".nodeLabelBox"); - if (labelBox) - labelBox.setAttribute("aria-expanded", "false"); - }, - - /** - * Toggle the given object box, forcing open if requested. - * @param aObjectBox - * The object box to toggle. - * @param forceOpen - * Force the objectbox open? - */ - toggleObjectBox: function IOBox_toggleObjectBox(aObjectBox, forceOpen) - { - let isOpen = this.view.hasClass(aObjectBox, "open"); - - if (!forceOpen && isOpen) - this.contractObjectBox(aObjectBox); - else if (!isOpen) - this.expandObjectBox(aObjectBox); - }, - - /** - * Creates all of the boxes for an object, its ancestors, and siblings. - * @param aObject - * The tree node to create the object boxes for. - * @returns anObjectBox or null - */ - createObjectBox: function IOBox_createObjectBox(aObject) - { - if (!aObject) - return null; - - this.rootObject = this.getRootNode(aObject) || aObject; - - // Get or create all of the boxes for the target and its ancestors - let objectBox = this.createObjectBoxes(aObject, this.rootObject); - - if (!objectBox) - return null; - - if (aObject == this.rootObject) - return objectBox; - - return this.populateChildBox(aObject, objectBox.parentNode); - }, - - /** - * Creates all of the boxes for an object, its ancestors, and siblings up to - * a root. - * @param aObject - * The tree's object node to create the object boxes for. - * @param aRootObject - * The root object at which to stop building object boxes. - * @returns an object box or null - */ - createObjectBoxes: function IOBox_createObjectBoxes(aObject, aRootObject) - { - if (!aObject) - return null; - - if (aObject == aRootObject) { - if (!this.rootObjectBox || this.rootObjectBox.repObject != aRootObject) { - if (this.rootObjectBox) { - try { - this.box.removeChild(this.rootObjectBox); - } catch (exc) { - this.view._log("this.box.removeChild(this.rootObjectBox) FAILS " + - this.box + " must not contain " + this.rootObjectBox); - } - } - - this.highlightedObjectBox = null; - this.selectedObjectBox = null; - this.rootObjectBox = this.view.createObjectBox(aObject, true); - this.box.appendChild(this.rootObjectBox); - } - return this.rootObjectBox; - } - - let parentNode = this.view.getParentObject(aObject); - let parentObjectBox = this.createObjectBoxes(parentNode, aRootObject); - - if (!parentObjectBox) - return null; - - let parentChildBox = this.getChildObjectBox(parentObjectBox); - - if (!parentChildBox) - return null; - - let childObjectBox = this.findChildObjectBox(parentChildBox, aObject); - - return childObjectBox ? childObjectBox - : this.populateChildBox(aObject, parentChildBox); - }, - - /** - * Locate the object box for a given object node. - * @param aObject - * The given object node in the tree. - * @returns an object box or null. - */ - findObjectBox: function IOBox_findObjectBox(aObject) - { - if (!aObject) - return null; - - if (aObject == this.rootObject) - return this.rootObjectBox; - - let parentNode = this.view.getParentObject(aObject); - let parentObjectBox = this.findObjectBox(parentNode); - if (!parentObjectBox) - return null; - - let parentChildBox = this.getChildObjectBox(parentObjectBox); - if (!parentChildBox) - return null; - - return this.findChildObjectBox(parentChildBox, aObject); - }, - - getAncestorByClass: function IOBox_getAncestorByClass(node, className) - { - for (let parent = node; parent; parent = parent.parentNode) { - if (this.view.hasClass(parent, className)) - return parent; - } - - return null; - }, - - /** - * We want all children of the parent of repObject. - */ - populateChildBox: function IOBox_populateChildBox(repObject, nodeChildBox) - { - if (!repObject) - return null; - - let parentObjectBox = this.getAncestorByClass(nodeChildBox, "nodeBox"); - - if (parentObjectBox.populated) - return this.findChildObjectBox(nodeChildBox, repObject); - - let lastSiblingBox = this.getChildObjectBox(nodeChildBox); - let siblingBox = nodeChildBox.firstChild; - let targetBox = null; - let view = this.view; - let targetSibling = null; - let parentNode = view.getParentObject(repObject); - - for (let i = 0; 1; ++i) { - targetSibling = view.getChildObject(parentNode, i, targetSibling); - if (!targetSibling) - break; - - // Check if we need to start appending, or continue to insert before - if (lastSiblingBox && lastSiblingBox.repObject == targetSibling) - lastSiblingBox = null; - - if (!siblingBox || siblingBox.repObject != targetSibling) { - let newBox = view.createObjectBox(targetSibling); - if (newBox) { - if (lastSiblingBox) - nodeChildBox.insertBefore(newBox, lastSiblingBox); - else - nodeChildBox.appendChild(newBox); - } - - siblingBox = newBox; - } - - if (targetSibling == repObject) - targetBox = siblingBox; - - if (siblingBox && siblingBox.repObject == targetSibling) - siblingBox = siblingBox.nextSibling; - } - - if (targetBox) - parentObjectBox.populated = true; - - return targetBox; - }, - - /** - * Get the parent object box of a given object box. - * @params aObjectBox - * The object box of the parent. - * @returns an object box or null - */ - getParentObjectBox: function IOBox_getParentObjectBox(aObjectBox) - { - let parent = aObjectBox.parentNode ? aObjectBox.parentNode.parentNode : null; - return parent && parent.repObject ? parent : null; - }, - - /** - * Get the child object box of a given object box. - * @param aObjectBox - * The object box whose child you want. - * @returns an object box or null - */ - getChildObjectBox: function IOBox_getChildObjectBox(aObjectBox) - { - return aObjectBox.querySelector(".nodeChildBox"); - }, - - /** - * Find the child object box for a given repObject within the subtree - * rooted at aParentNodeBox. - * @param aParentNodeBox - * root of the subtree in which to search for repObject. - * @param aRepObject - * The object you wish to locate in the subtree. - * @returns an object box or null - */ - findChildObjectBox: function IOBox_findChildObjectBox(aParentNodeBox, aRepObject) - { - let childBox = aParentNodeBox.firstChild; - while (childBox) { - if (childBox.repObject == aRepObject) - return childBox; - childBox = childBox.nextSibling; - } - return null; // not found - }, - - /** - * Determines if the given node is an ancestor of the current root. - * @param aNode - * The node to look for within the tree. - * @returns boolean - */ - isInExistingRoot: function IOBox_isInExistingRoot(aNode) - { - let parentNode = aNode; - while (parentNode && parentNode != this.rootObject) { - parentNode = this.view.getParentObject(parentNode); - } - return parentNode == this.rootObject; - }, - - /** - * Get the root node of a given node. - * @param aNode - * The node whose root you wish to retrieve. - * @returns a root node or null - */ - getRootNode: function IOBox_getRootNode(aNode) - { - let node = aNode; - let tmpNode; - while ((tmpNode = this.view.getParentObject(node))) - node = tmpNode; - - return node; - }, - - /** - * Clean up our mess. - */ - destroy: function IOBox_destroy() - { - delete this.view; - delete this.box; - delete this.rootObject; - delete this.rootObjectBox; - delete this.selectedObjectBox; - delete this.highlightedObjectBox; - delete this.scrollIntoView; - } -}; diff --git a/browser/devtools/highlighter/Makefile.in b/browser/devtools/highlighter/Makefile.in index 005ad0bf92ad..8187267f5b74 100644 --- a/browser/devtools/highlighter/Makefile.in +++ b/browser/devtools/highlighter/Makefile.in @@ -11,9 +11,6 @@ VPATH = @srcdir@ include $(DEPTH)/config/autoconf.mk EXTRA_JS_MODULES = \ - domplate.jsm \ - InsideOutBox.jsm \ - TreePanel.jsm \ highlighter.jsm \ $(NULL) diff --git a/browser/devtools/highlighter/TreePanel.jsm b/browser/devtools/highlighter/TreePanel.jsm deleted file mode 100644 index 0761fa2ff82e..000000000000 --- a/browser/devtools/highlighter/TreePanel.jsm +++ /dev/null @@ -1,844 +0,0 @@ -/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ -/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ -/* 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/. */ - -const Cu = Components.utils; -const Ci = Components.interfaces; - -Cu.import("resource:///modules/domplate.jsm"); -Cu.import("resource:///modules/InsideOutBox.jsm"); -Cu.import("resource://gre/modules/Services.jsm"); -Cu.import("resource:///modules/inspector.jsm"); -Cu.import("resource:///modules/devtools/LayoutHelpers.jsm"); - -var EXPORTED_SYMBOLS = ["TreePanel", "DOMHelpers"]; - -const INSPECTOR_URI = "chrome://browser/content/inspector.html"; - -/** - * TreePanel - * A container for the Inspector's HTML Tree Panel widget constructor function. - * @param aContext nsIDOMWindow (xulwindow) - * @param aIUI global InspectorUI object - */ -function TreePanel(aContext, aIUI) { - this._init(aContext, aIUI); -}; - -TreePanel.prototype = { - showTextNodesWithWhitespace: false, - id: "treepanel", // DO NOT LOCALIZE - _open: false, - - /** - * The tree panel container element. - * @returns xul:panel|xul:vbox|null - * xul:panel is returned when the tree panel is not docked, or - * xul:vbox when when the tree panel is docked. - * null is returned when no container is available. - */ - get container() - { - return this.document.getElementById("inspector-tree-box"); - }, - - /** - * Main TreePanel boot-strapping method. Initialize the TreePanel with the - * originating context and the InspectorUI global. - * @param aContext nsIDOMWindow (xulwindow) - * @param aIUI global InspectorUI object - */ - _init: function TP__init(aContext, aIUI) - { - this.IUI = aIUI; - this.window = aContext; - this.document = this.window.document; - this.button = - this.IUI.chromeDoc.getElementById("inspector-treepanel-toolbutton"); - - domplateUtils.setDOM(this.window); - - this.DOMHelpers = new DOMHelpers(this.window); - - let isOpen = this.isOpen.bind(this); - - this.editingEvents = {}; - }, - - /** - * Initialization function for the TreePanel. - */ - initializeIFrame: function TP_initializeIFrame() - { - if (!this.initializingTreePanel || this.treeLoaded) { - return; - } - this.treeBrowserDocument = this.treeIFrame.contentDocument; - this.treePanelDiv = this.treeBrowserDocument.createElement("div"); - this.treeBrowserDocument.body.appendChild(this.treePanelDiv); - this.treePanelDiv.ownerPanel = this; - this.ioBox = new InsideOutBox(this, this.treePanelDiv); - this.ioBox.createObjectBox(this.IUI.win.document.documentElement); - this.treeLoaded = true; - this._boundTreeKeyPress = this.onTreeKeyPress.bind(this); - this.treeIFrame.addEventListener("keypress", this._boundTreeKeyPress.bind(this), true); - this.treeIFrame.addEventListener("click", this.onTreeClick.bind(this), false); - this.treeIFrame.addEventListener("dblclick", this.onTreeDblClick.bind(this), false); - this.treeIFrame.focus(); - delete this.initializingTreePanel; - Services.obs.notifyObservers(null, - this.IUI.INSPECTOR_NOTIFICATIONS.TREEPANELREADY, null); - if (this.pendingSelection) { - this.select(this.pendingSelection.node, this.pendingSelection.scroll); - delete this.pendingSelection; - } - }, - - /** - * Open the inspector's tree panel and initialize it. - */ - open: function TP_open() - { - if (this._open) { - return; - } - - this._open = true; - - this.button.setAttribute("checked", true); - this.initializingTreePanel = true; - - this.treeIFrame = this.document.getElementById("inspector-tree-iframe"); - if (!this.treeIFrame) { - this.treeIFrame = this.document.createElement("iframe"); - this.treeIFrame.setAttribute("id", "inspector-tree-iframe"); - this.treeIFrame.flex = 1; - this.treeIFrame.setAttribute("type", "content"); - this.treeIFrame.setAttribute("context", "inspector-node-popup"); - } - - let treeBox = null; - treeBox = this.document.createElement("vbox"); - treeBox.id = "inspector-tree-box"; - treeBox.state = "open"; - try { - treeBox.height = - Services.prefs.getIntPref("devtools.inspector.htmlHeight"); - } catch(e) { - treeBox.height = 112; - } - - treeBox.minHeight = 64; - - this.splitter = this.document.createElement("splitter"); - this.splitter.id = "inspector-tree-splitter"; - this.splitter.className = "devtools-horizontal-splitter"; - - let container = this.document.getElementById("appcontent"); - container.appendChild(this.splitter); - container.appendChild(treeBox); - - treeBox.appendChild(this.treeIFrame); - - this._boundLoadedInitializeTreePanel = function loadedInitializeTreePanel() - { - this.treeIFrame.removeEventListener("load", - this._boundLoadedInitializeTreePanel, true); - delete this._boundLoadedInitializeTreePanel; - this.initializeIFrame(); - }.bind(this); - - this.treeIFrame.addEventListener("load", - this._boundLoadedInitializeTreePanel, true); - - let src = this.treeIFrame.getAttribute("src"); - if (src != INSPECTOR_URI) { - this.treeIFrame.setAttribute("src", INSPECTOR_URI); - } else { - this.treeIFrame.contentWindow.location.reload(); - } - }, - - /** - * Close the TreePanel. - */ - close: function TP_close() - { - this._open = false; - - // Stop caring about the tree iframe load if it's in progress. - if (this._boundLoadedInitializeTreePanel) { - this.treeIFrame.removeEventListener("load", - this._boundLoadedInitializeTreePanel, true); - delete this._boundLoadedInitializeTreePanel; - } - - this.button.removeAttribute("checked"); - let treeBox = this.container; - Services.prefs.setIntPref("devtools.inspector.htmlHeight", treeBox.height); - let treeBoxParent = treeBox.parentNode; - treeBoxParent.removeChild(this.splitter); - treeBoxParent.removeChild(treeBox); - - if (this.treePanelDiv) { - this.treePanelDiv.ownerPanel = null; - let parent = this.treePanelDiv.parentNode; - parent.removeChild(this.treePanelDiv); - this.treeIFrame.removeEventListener("keypress", this._boundTreeKeyPress, true); - delete this.treePanelDiv; - delete this.treeBrowserDocument; - } - - if (this.ioBox) { - this.ioBox.destroy(); - delete this.ioBox; - } - - this.treeLoaded = false; - }, - - /** - * Is the TreePanel open? - * @returns boolean - */ - isOpen: function TP_isOpen() - { - return this._open; - }, - - /** - * Toggle the TreePanel. - */ - toggle: function TP_toggle() - { - this.isOpen() ? this.close() : this.open(); - }, - - /** - * Create the ObjectBox for the given object. - * @param object nsIDOMNode - * @param isRoot boolean - Is this the root object? - * @returns InsideOutBox - */ - createObjectBox: function TP_createObjectBox(object, isRoot) - { - let tag = domplateUtils.getNodeTag(object); - if (tag) - return tag.replace({object: object}, this.treeBrowserDocument); - }, - - getParentObject: function TP_getParentObject(node) - { - return this.DOMHelpers.getParentObject(node); - }, - - getChildObject: function TP_getChildObject(node, index, previousSibling) - { - return this.DOMHelpers.getChildObject(node, index, previousSibling, - this.showTextNodesWithWhitespace); - }, - - getFirstChild: function TP_getFirstChild(node) - { - return this.DOMHelpers.getFirstChild(node); - }, - - getNextSibling: function TP_getNextSibling(node) - { - return this.DOMHelpers.getNextSibling(node); - }, - - ///////////////////////////////////////////////////////////////////// - // Event Handling - - /** - * Handle click events in the html tree panel. - * @param aEvent - * The mouse event. - */ - onTreeClick: function TP_onTreeClick(aEvent) - { - let node; - let target = aEvent.target; - let hitTwisty = false; - if (this.hasClass(target, "twisty")) { - node = this.getRepObject(aEvent.target.nextSibling); - hitTwisty = true; - } else { - node = this.getRepObject(aEvent.target); - } - - if (node) { - if (hitTwisty) { - this.ioBox.toggleObject(node); - } else { - if (this.IUI.inspecting) { - this.IUI.stopInspecting(true); - } else { - this.navigate(node); - } - } - } - }, - - /** - * Handle double-click events in the html tree panel. - * Double-clicking an attribute name or value allows it to be edited. - * @param aEvent - * The mouse event. - */ - onTreeDblClick: function TP_onTreeDblClick(aEvent) - { - // if already editing an attribute value, double-clicking elsewhere - // in the tree is the same as a click, which dismisses the editor - if (this.editingContext) - this.closeEditor(); - - let target = aEvent.target; - - if (!this.hasClass(target, "editable")) { - return; - } - - let repObj = this.getRepObject(target); - - if (this.hasClass(target, "nodeValue")) { - let attrName = target.getAttribute("data-attributeName"); - let attrVal = target.innerHTML; - - this.editAttribute(target, repObj, attrName, attrVal); - } - - if (this.hasClass(target, "nodeName")) { - let attrName = target.innerHTML; - let attrValNode = target.nextSibling.nextSibling; // skip 2 (=) - - if (attrValNode) - this.editAttribute(target, repObj, attrName, attrValNode.innerHTML); - } - }, - - navigate: function TP_navigate(node) - { - if (!node) - return; - this.ioBox.select(node, false, false, true); - - if (this.IUI.highlighter.isNodeHighlightable(node)) { - this.IUI.select(node, true, false, "treepanel"); - this.IUI.highlighter.highlight(node); - } - }, - - onTreeKeyPress: function TP_onTreeKeyPress(aEvent) - { - let handled = true; - switch(aEvent.keyCode) { - case Ci.nsIDOMKeyEvent.DOM_VK_LEFT: - this.ioBox.contractObjectBox(this.ioBox.selectedObjectBox); - break; - case Ci.nsIDOMKeyEvent.DOM_VK_RIGHT: - this.ioBox.expandObjectBox(this.ioBox.selectedObjectBox); - break; - case Ci.nsIDOMKeyEvent.DOM_VK_UP: - this.navigate(this.ioBox.previousObject()); - break; - case Ci.nsIDOMKeyEvent.DOM_VK_DOWN: - this.navigate(this.ioBox.nextObject()); - break; - case Ci.nsIDOMKeyEvent.DOM_VK_PAGE_UP: - this.navigate(this.ioBox.farPreviousObject(10)); - break; - case Ci.nsIDOMKeyEvent.DOM_VK_PAGE_DOWN: - this.navigate(this.ioBox.farNextObject(10)); - break; - case Ci.nsIDOMKeyEvent.DOM_VK_HOME: - this.navigate(this.ioBox.rootObject); - break; - default: - handled = false; - } - if (handled) { - aEvent.stopPropagation(); - aEvent.preventDefault(); - } - }, - - /** - * Starts the editor for an attribute name or value. - * @param aAttrObj - * The DOM object representing the attribute name or value in the HTML - * Tree. - * @param aRepObj - * The original DOM (target) object being inspected/edited - * @param aAttrName - * The name of the attribute being edited - * @param aAttrVal - * The current value of the attribute being edited - */ - editAttribute: - function TP_editAttribute(aAttrObj, aRepObj, aAttrName, aAttrVal) - { - let editor = this.treeBrowserDocument.getElementById("attribute-editor"); - let editorInput = - this.treeBrowserDocument.getElementById("attribute-editor-input"); - let attrDims = aAttrObj.getBoundingClientRect(); - // figure out actual viewable viewport dimensions (sans scrollbars) - let viewportWidth = this.treeBrowserDocument.documentElement.clientWidth; - let viewportHeight = this.treeBrowserDocument.documentElement.clientHeight; - - // saves the editing context for use when the editor is saved/closed - this.editingContext = { - attrObj: aAttrObj, - repObj: aRepObj, - attrName: aAttrName, - attrValue: aAttrVal - }; - - // highlight attribute-value node in tree while editing - this.addClass(aAttrObj, "editingAttributeValue"); - - // show the editor - this.addClass(editor, "editing"); - - // offset the editor below the attribute-value node being edited - let editorVerticalOffset = 2; - - // keep the editor comfortably within the bounds of the viewport - let editorViewportBoundary = 5; - - // outer editor is sized based on the box inside it - editorInput.style.width = Math.min(attrDims.width, viewportWidth - - editorViewportBoundary) + "px"; - editorInput.style.height = Math.min(attrDims.height, viewportHeight - - editorViewportBoundary) + "px"; - let editorDims = editor.getBoundingClientRect(); - - // calculate position for the editor according to the attribute node - let editorLeft = attrDims.left + this.treeIFrame.contentWindow.scrollX - - // center the editor against the attribute value - ((editorDims.width - attrDims.width) / 2); - let editorTop = attrDims.top + this.treeIFrame.contentWindow.scrollY + - attrDims.height + editorVerticalOffset; - - // but, make sure the editor stays within the visible viewport - editorLeft = Math.max(0, Math.min( - (this.treeIFrame.contentWindow.scrollX + - viewportWidth - editorDims.width), - editorLeft) - ); - editorTop = Math.max(0, Math.min( - (this.treeIFrame.contentWindow.scrollY + - viewportHeight - editorDims.height), - editorTop) - ); - - // position the editor - editor.style.left = editorLeft + "px"; - editor.style.top = editorTop + "px"; - - // set and select the text - if (this.hasClass(aAttrObj, "nodeValue")) { - editorInput.value = aAttrVal; - editorInput.select(); - } else { - editorInput.value = aAttrName; - editorInput.select(); - } - - // listen for editor specific events - this.bindEditorEvent(editor, "click", function(aEvent) { - aEvent.stopPropagation(); - }); - this.bindEditorEvent(editor, "dblclick", function(aEvent) { - aEvent.stopPropagation(); - }); - this.bindEditorEvent(editor, "keypress", - this.handleEditorKeypress.bind(this)); - - // event notification - Services.obs.notifyObservers(null, this.IUI.INSPECTOR_NOTIFICATIONS.EDITOR_OPENED, - null); - }, - - /** - * Handle binding an event handler for the editor. - * (saves the callback for easier unbinding later) - * @param aEditor - * The DOM object for the editor - * @param aEventName - * The name of the event to listen for - * @param aEventCallback - * The callback to bind to the event (and also to save for later - * unbinding) - */ - bindEditorEvent: - function TP_bindEditorEvent(aEditor, aEventName, aEventCallback) - { - this.editingEvents[aEventName] = aEventCallback; - aEditor.addEventListener(aEventName, aEventCallback, false); - }, - - /** - * Handle unbinding an event handler from the editor. - * (unbinds the previously bound and saved callback) - * @param aEditor - * The DOM object for the editor - * @param aEventName - * The name of the event being listened for - */ - unbindEditorEvent: function TP_unbindEditorEvent(aEditor, aEventName) - { - aEditor.removeEventListener(aEventName, this.editingEvents[aEventName], - false); - this.editingEvents[aEventName] = null; - }, - - /** - * Handle keypress events in the editor. - * @param aEvent - * The keyboard event. - */ - handleEditorKeypress: function TP_handleEditorKeypress(aEvent) - { - if (aEvent.which == this.window.KeyEvent.DOM_VK_RETURN) { - this.saveEditor(); - aEvent.preventDefault(); - aEvent.stopPropagation(); - } else if (aEvent.keyCode == this.window.KeyEvent.DOM_VK_ESCAPE) { - this.closeEditor(); - aEvent.preventDefault(); - aEvent.stopPropagation(); - } - }, - - /** - * Close the editor and cleanup. - */ - closeEditor: function TP_closeEditor() - { - if (!this.treeBrowserDocument) // already closed, bug 706092 - return; - - let editor = this.treeBrowserDocument.getElementById("attribute-editor"); - - let editorInput = - this.treeBrowserDocument.getElementById("attribute-editor-input"); - - // remove highlight from attribute-value node in tree - this.removeClass(this.editingContext.attrObj, "editingAttributeValue"); - - // hide editor - this.removeClass(editor, "editing"); - - // stop listening for editor specific events - this.unbindEditorEvent(editor, "click"); - this.unbindEditorEvent(editor, "dblclick"); - this.unbindEditorEvent(editor, "keypress"); - - // clean up after the editor - editorInput.value = ""; - editorInput.blur(); - this.editingContext = null; - this.editingEvents = {}; - - // event notification - Services.obs.notifyObservers(null, this.IUI.INSPECTOR_NOTIFICATIONS.EDITOR_CLOSED, - null); - }, - - /** - * Commit the edits made in the editor, then close it. - */ - saveEditor: function TP_saveEditor() - { - let editorInput = - this.treeBrowserDocument.getElementById("attribute-editor-input"); - let dirty = false; - - if (this.hasClass(this.editingContext.attrObj, "nodeValue")) { - // set the new attribute value on the original target DOM element - this.editingContext.repObj.setAttribute(this.editingContext.attrName, - editorInput.value); - - // update the HTML tree attribute value - this.editingContext.attrObj.innerHTML = editorInput.value; - dirty = true; - } - - if (this.hasClass(this.editingContext.attrObj, "nodeName")) { - // remove the original attribute from the original target DOM element - this.editingContext.repObj.removeAttribute(this.editingContext.attrName); - - // set the new attribute value on the original target DOM element - this.editingContext.repObj.setAttribute(editorInput.value, - this.editingContext.attrValue); - - // update the HTML tree attribute value - this.editingContext.attrObj.innerHTML = editorInput.value; - dirty = true; - } - - this.IUI.isDirty = dirty; - this.IUI.nodeChanged("treepanel"); - - // event notification - Services.obs.notifyObservers(null, this.IUI.INSPECTOR_NOTIFICATIONS.EDITOR_SAVED, - null); - - this.closeEditor(); - }, - - /** - * Simple tree select method. - * @param aNode the DOM node in the content document to select. - * @param aScroll boolean scroll to the visible node? - */ - select: function TP_select(aNode, aScroll, aFrom) - { - if (this.ioBox) { - this.ioBox.select(aNode, true, aFrom != "treepanel", aScroll); - } else { - this.pendingSelection = { node: aNode, scroll: aScroll }; - } - }, - - /////////////////////////////////////////////////////////////////////////// - //// Utility functions - - /** - * Does the given object have a class attribute? - * @param aNode - * the DOM node. - * @param aClass - * The class string. - * @returns boolean - */ - hasClass: function TP_hasClass(aNode, aClass) - { - if (!(aNode instanceof this.window.Element)) - return false; - return aNode.classList.contains(aClass); - }, - - /** - * Add the class name to the given object. - * @param aNode - * the DOM node. - * @param aClass - * The class string. - */ - addClass: function TP_addClass(aNode, aClass) - { - if (aNode instanceof this.window.Element) - aNode.classList.add(aClass); - }, - - /** - * Remove the class name from the given object - * @param aNode - * the DOM node. - * @param aClass - * The class string. - */ - removeClass: function TP_removeClass(aNode, aClass) - { - if (aNode instanceof this.window.Element) - aNode.classList.remove(aClass); - }, - - /** - * Get the "repObject" from the HTML panel's domplate-constructed DOM node. - * In this system, a "repObject" is the Object being Represented by the box - * object. It is the "real" object that we're building our facade around. - * - * @param element - * The element in the HTML panel the user clicked. - * @returns either a real node or null - */ - getRepObject: function TP_getRepObject(element) - { - let target = null; - for (let child = element; child; child = child.parentNode) { - if (this.hasClass(child, "repTarget")) - target = child; - - if (child.repObject) { - if (!target && this.hasClass(child.repObject, "repIgnore")) - break; - else - return child.repObject; - } - } - return null; - }, - - /** - * Remove a node box from the tree view. - * @param aElement - * The DOM node to remove from the HTML IOBox. - */ - deleteChildBox: function TP_deleteChildBox(aElement) - { - let childBox = this.ioBox.findObjectBox(aElement); - if (!childBox) { - return; - } - childBox.parentNode.removeChild(childBox); - }, - - /** - * Destructor function. Cleanup. - */ - destroy: function TP_destroy() - { - if (this.isOpen()) { - this.close(); - } - - domplateUtils.setDOM(null); - - if (this.DOMHelpers) { - this.DOMHelpers.destroy(); - delete this.DOMHelpers; - } - - if (this.treePanelDiv) { - this.treePanelDiv.ownerPanel = null; - let parent = this.treePanelDiv.parentNode; - parent.removeChild(this.treePanelDiv); - delete this.treePanelDiv; - delete this.treeBrowserDocument; - } - - if (this.treeIFrame) { - this.treeIFrame.removeEventListener("dblclick", this.onTreeDblClick, false); - this.treeIFrame.removeEventListener("click", this.onTreeClick, false); - let parent = this.treeIFrame.parentNode; - parent.removeChild(this.treeIFrame); - delete this.treeIFrame; - } - } -}; - - -/** - * DOMHelpers - * Makes DOM traversal easier. Goes through iframes. - * - * @constructor - * @param nsIDOMWindow aWindow - * The content window, owning the document to traverse. - */ -function DOMHelpers(aWindow) { - this.window = aWindow; -}; - -DOMHelpers.prototype = { - getParentObject: function Helpers_getParentObject(node) - { - let parentNode = node ? node.parentNode : null; - - if (!parentNode) { - // Documents have no parentNode; Attr, Document, DocumentFragment, Entity, - // and Notation. top level windows have no parentNode - if (node && node == this.window.Node.DOCUMENT_NODE) { - // document type - if (node.defaultView) { - let embeddingFrame = node.defaultView.frameElement; - if (embeddingFrame) - return embeddingFrame.parentNode; - } - } - // a Document object without a parentNode or window - return null; // top level has no parent - } - - if (parentNode.nodeType == this.window.Node.DOCUMENT_NODE) { - if (parentNode.defaultView) { - return parentNode.defaultView.frameElement; - } - // parent is document element, but no window at defaultView. - return null; - } - - if (!parentNode.localName) - return null; - - return parentNode; - }, - - getChildObject: function Helpers_getChildObject(node, index, previousSibling, - showTextNodesWithWhitespace) - { - if (!node) - return null; - - if (node.contentDocument) { - // then the node is a frame - if (index == 0) { - return node.contentDocument.documentElement; // the node's HTMLElement - } - return null; - } - - if (node instanceof this.window.GetSVGDocument) { - let svgDocument = node.getSVGDocument(); - if (svgDocument) { - // then the node is a frame - if (index == 0) { - return svgDocument.documentElement; // the node's SVGElement - } - return null; - } - } - - let child = null; - if (previousSibling) // then we are walking - child = this.getNextSibling(previousSibling); - else - child = this.getFirstChild(node); - - if (showTextNodesWithWhitespace) - return child; - - for (; child; child = this.getNextSibling(child)) { - if (!this.isWhitespaceText(child)) - return child; - } - - return null; // we have no children worth showing. - }, - - getFirstChild: function Helpers_getFirstChild(node) - { - let SHOW_ALL = Components.interfaces.nsIDOMNodeFilter.SHOW_ALL; - this.treeWalker = node.ownerDocument.createTreeWalker(node, - SHOW_ALL, null, false); - return this.treeWalker.firstChild(); - }, - - getNextSibling: function Helpers_getNextSibling(node) - { - let next = this.treeWalker.nextSibling(); - - if (!next) - delete this.treeWalker; - - return next; - }, - - isWhitespaceText: function Helpers_isWhitespaceText(node) - { - return node.nodeType == this.window.Node.TEXT_NODE && - !/[^\s]/.exec(node.nodeValue); - }, - - destroy: function Helpers_destroy() - { - delete this.window; - delete this.treeWalker; - } -}; diff --git a/browser/devtools/highlighter/domplate.jsm b/browser/devtools/highlighter/domplate.jsm deleted file mode 100644 index 1f6e5b67cf0c..000000000000 --- a/browser/devtools/highlighter/domplate.jsm +++ /dev/null @@ -1,1891 +0,0 @@ -/* - * Software License Agreement (BSD License) - * - * Copyright (c) 2007, Parakey Inc. - * All rights reserved. - * - * Redistribution and use of this software in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * - * * Redistributions of source code must retain the above - * copyright notice, this list of conditions and the - * following disclaimer. - * - * * Redistributions in binary form must reproduce the above - * copyright notice, this list of conditions and the - * following disclaimer in the documentation and/or other - * materials provided with the distribution. - * - * * Neither the name of Parakey Inc. nor the names of its - * contributors may be used to endorse or promote products - * derived from this software without specific prior - * written permission of Parakey Inc. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR - * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND - * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR - * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL - * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER - * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT - * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -/* - * Creator: - * Joe Hewitt - * Contributors: - * John J. Barton (IBM Almaden) - * Jan Odvarko (Mozilla Corp.) - * Max Stepanov (Aptana Inc.) - * Rob Campbell (Mozilla Corp.) - * Hans Hillen (Paciello Group, Mozilla) - * Curtis Bartley (Mozilla Corp.) - * Mike Collins (IBM Almaden) - * Kevin Decker - * Mike Ratcliffe (Comartis AG) - * Hernan Rodríguez Colmeiro - * Austin Andrews - * Christoph Dorn - * Steven Roussey (AppCenter Inc, Network54) - * Firefox Port Contributors: - * Rob Campbell - * Julian Viereck - * Mihai Sucan - */ - -var EXPORTED_SYMBOLS = ["domplate", "HTMLTemplates", "domplateUtils"]; - -const Ci = Components.interfaces; -const Cu = Components.utils; - -const invisibleTags = { - "head": true, - "base": true, - "basefont": true, - "isindex": true, - "link": true, - "meta": true, - "script": true, - "style": true, - "title": true, -}; - -// End tags for void elements are forbidden -// http://wiki.whatwg.org/wiki/HTML_vs._XHTML -const selfClosingTags = { - "meta": 1, - "link": 1, - "area": 1, - "base": 1, - "col": 1, - "input": 1, - "img": 1, - "br": 1, - "hr": 1, - "param": 1, - "embed": 1 -}; - -const reNotWhitespace = /[^\s]/; -const showTextNodesWithWhitespace = false; - -var DOM = {}; -var domplateUtils = {}; - -/** - * Utility function to allow outside caller to set a global scope within - * domplate's DOM object. Specifically for access to DOM constants and classes. - * @param aGlobal - * The global object whose scope we wish to capture. - */ -domplateUtils.setDOM = function(aGlobal) -{ - DOM = aGlobal; - if (!aGlobal) { - womb = null; - } -}; - -/** - * main domplate constructor function. - */ - -let domplate = function() -{ - let lastSubject; - for (let i = 0; i < arguments.length; ++i) - lastSubject = lastSubject ? copyObject(lastSubject, arguments[i]) : arguments[i]; - - for (let name in lastSubject) { - let val = lastSubject[name]; - if (isTag(val)) - val.tag.subject = lastSubject; - } - - return lastSubject; -}; - -var womb = null; - -/////////////////////////////////////////////////////////////////////////// -//// Base functions - -function DomplateTag(tagName) -{ - this.tagName = tagName; -} - -function DomplateEmbed() -{ -} - -function DomplateLoop() -{ -} - -/////////////////////////////////////////////////////////////////////////// -//// Definitions - -domplate.context = function(context, fn) -{ - let lastContext = domplate.lastContext; - domplate.topContext = context; - fn.apply(context); - domplate.topContext = lastContext; -}; - -domplate.TAG = function() -{ - let embed = new DomplateEmbed(); - return embed.merge(arguments); -}; - -domplate.FOR = function() -{ - let loop = new DomplateLoop(); - return loop.merge(arguments); -}; - -DomplateTag.prototype = -{ - /** - * Initializer for DOM templates. Called to create new Functions objects - * like TR, TD, OBJLINK, etc. See defineTag - * @param args keyword argments for the template, the {} brace stuff after - * the tag name, eg TR({...}, TD(... - * @param oldTag a nested tag, eg the TD tag in TR({...}, TD(... - */ - merge: function(args, oldTag) - { - if (oldTag) - this.tagName = oldTag.tagName; - - this.context = oldTag ? oldTag.context : null; // normally null on construction - this.subject = oldTag ? oldTag.subject : null; - this.attrs = oldTag ? copyObject(oldTag.attrs) : {}; - this.classes = oldTag ? copyObject(oldTag.classes) : {}; - this.props = oldTag ? copyObject(oldTag.props) : null; - this.listeners = oldTag ? copyArray(oldTag.listeners) : null; - this.children = oldTag ? copyArray(oldTag.children) : []; - this.vars = oldTag ? copyArray(oldTag.vars) : []; - - let attrs = args.length ? args[0] : null; - let hasAttrs = typeof(attrs) == "object" && !isTag(attrs); - - // Do not clear children, they can be copied from the oldTag. - //this.children = []; - - if (domplate.topContext) - this.context = domplate.topContext; - - if (args.length) - parseChildren(args, hasAttrs ? 1 : 0, this.vars, this.children); - - if (hasAttrs) - this.parseAttrs(attrs); - - return creator(this, DomplateTag); - }, - - /** - * Parse node attributes. - * @param args - * Object of arguments to process. - */ - parseAttrs: function(args) - { - for (let name in args) { - let val = parseValue(args[name]); - readPartNames(val, this.vars); - - if (name.indexOf("on") == 0) { - let eventName = name.substr(2); - if (!this.listeners) - this.listeners = []; - this.listeners.push(eventName, val); - } else if (name[0] == "_") { - let propName = name.substr(1); - if (!this.props) - this.props = {}; - this.props[propName] = val; - } else if (name[0] == "$") { - let className = name.substr(1); - if (!this.classes) - this.classes = {}; - this.classes[className] = val; - } else { - if (name == "class" && this.attrs.hasOwnProperty(name)) - this.attrs[name] += " " + val; - else - this.attrs[name] = val; - } - } - }, - - compile: function() - { - if (this.renderMarkup) - return; - - this.compileMarkup(); - this.compileDOM(); - }, - - compileMarkup: function() - { - this.markupArgs = []; - let topBlock = [], topOuts = [], blocks = [], - info = {args: this.markupArgs, argIndex: 0}; - - this.generateMarkup(topBlock, topOuts, blocks, info); - this.addCode(topBlock, topOuts, blocks); - - let fnBlock = ['(function (__code__, __context__, __in__, __out__']; - for (let i = 0; i < info.argIndex; ++i) - fnBlock.push(', s', i); - fnBlock.push(') {\n'); - - if (this.subject) - fnBlock.push('with (this) {\n'); - if (this.context) - fnBlock.push('with (__context__) {\n'); - fnBlock.push('with (__in__) {\n'); - - fnBlock.push.apply(fnBlock, blocks); - - if (this.subject) - fnBlock.push('}\n'); - if (this.context) - fnBlock.push('}\n'); - - fnBlock.push('}})\n'); - - function __link__(tag, code, outputs, args) - { - tag.tag.compile(); - - let tagOutputs = []; - let markupArgs = [code, tag.tag.context, args, tagOutputs]; - markupArgs.push.apply(markupArgs, tag.tag.markupArgs); - tag.tag.renderMarkup.apply(tag.tag.subject, markupArgs); - - outputs.push(tag); - outputs.push(tagOutputs); - } - - function __escape__(value) - { - function replaceChars(ch) - { - switch (ch) { - case "<": - return "<"; - case ">": - return ">"; - case "&": - return "&"; - case "'": - return "'"; - case '"': - return """; - } - return "?"; - }; - return String(value).replace(/[<>&"']/g, replaceChars); - } - - function __loop__(iter, outputs, fn) - { - let iterOuts = []; - outputs.push(iterOuts); - - if (iter instanceof Array) - iter = new ArrayIterator(iter); - - try { - while (1) { - let value = iter.next(); - let itemOuts = [0, 0]; - iterOuts.push(itemOuts); - fn.apply(this, [value, itemOuts]); - } - } catch (exc) { - if (exc != StopIteration) - throw exc; - } - } - - let js = fnBlock.join(""); - this.renderMarkup = eval(js); - }, - - getVarNames: function(args) - { - if (this.vars) - args.push.apply(args, this.vars); - - for (let i = 0; i < this.children.length; ++i) { - let child = this.children[i]; - if (isTag(child)) - child.tag.getVarNames(args); - else if (child instanceof Parts) { - for (let i = 0; i < child.parts.length; ++i) { - if (child.parts[i] instanceof Variable) { - let name = child.parts[i].name; - let names = name.split("."); - args.push(names[0]); - } - } - } - } - }, - - generateMarkup: function(topBlock, topOuts, blocks, info) - { - topBlock.push(',"<', this.tagName, '"'); - - for (let name in this.attrs) { - if (name != "class") { - let val = this.attrs[name]; - topBlock.push(', " ', name, '=\\""'); - addParts(val, ',', topBlock, info, true); - topBlock.push(', "\\""'); - } - } - - if (this.listeners) { - for (let i = 0; i < this.listeners.length; i += 2) - readPartNames(this.listeners[i+1], topOuts); - } - - if (this.props) { - for (let name in this.props) - readPartNames(this.props[name], topOuts); - } - - if (this.attrs.hasOwnProperty("class") || this.classes) { - topBlock.push(', " class=\\""'); - if (this.attrs.hasOwnProperty("class")) - addParts(this.attrs["class"], ',', topBlock, info, true); - topBlock.push(', " "'); - for (let name in this.classes) { - topBlock.push(', ('); - addParts(this.classes[name], '', topBlock, info); - topBlock.push(' ? "', name, '" + " " : "")'); - } - topBlock.push(', "\\""'); - } - topBlock.push(',">"'); - - this.generateChildMarkup(topBlock, topOuts, blocks, info); - topBlock.push(',""'); - }, - - generateChildMarkup: function(topBlock, topOuts, blocks, info) - { - for (let i = 0; i < this.children.length; ++i) { - let child = this.children[i]; - if (isTag(child)) - child.tag.generateMarkup(topBlock, topOuts, blocks, info); - else - addParts(child, ',', topBlock, info, true); - } - }, - - addCode: function(topBlock, topOuts, blocks) - { - if (topBlock.length) - blocks.push('__code__.push(""', topBlock.join(""), ');\n'); - if (topOuts.length) - blocks.push('__out__.push(', topOuts.join(","), ');\n'); - topBlock.splice(0, topBlock.length); - topOuts.splice(0, topOuts.length); - }, - - addLocals: function(blocks) - { - let varNames = []; - this.getVarNames(varNames); - - let map = {}; - for (let i = 0; i < varNames.length; ++i) { - let name = varNames[i]; - if ( map.hasOwnProperty(name) ) - continue; - - map[name] = 1; - let names = name.split("."); - blocks.push('var ', names[0] + ' = ' + '__in__.' + names[0] + ';\n'); - } - }, - - compileDOM: function() - { - let path = []; - let blocks = []; - this.domArgs = []; - path.embedIndex = 0; - path.loopIndex = 0; - path.staticIndex = 0; - path.renderIndex = 0; - let nodeCount = this.generateDOM(path, blocks, this.domArgs); - - let fnBlock = ['(function (root, context, o']; - - for (let i = 0; i < path.staticIndex; ++i) - fnBlock.push(', ', 's'+i); - - for (let i = 0; i < path.renderIndex; ++i) - fnBlock.push(', ', 'd'+i); - - fnBlock.push(') {\n'); - for (let i = 0; i < path.loopIndex; ++i) - fnBlock.push('var l', i, ' = 0;\n'); - for (let i = 0; i < path.embedIndex; ++i) - fnBlock.push('var e', i, ' = 0;\n'); - - if (this.subject) - fnBlock.push('with (this) {\n'); - if (this.context) - fnBlock.push('with (context) {\n'); - - fnBlock.push(blocks.join("")); - - if (this.subject) - fnBlock.push('}\n'); - if (this.context) - fnBlock.push('}\n'); - - fnBlock.push('return ', nodeCount, ';\n'); - fnBlock.push('})\n'); - - function __bind__(object, fn) - { - return function(event) { return fn.apply(object, [event]); } - } - - function __link__(node, tag, args) - { - if (!tag || !tag.tag) - return; - - tag.tag.compile(); - - let domArgs = [node, tag.tag.context, 0]; - domArgs.push.apply(domArgs, tag.tag.domArgs); - domArgs.push.apply(domArgs, args); - - return tag.tag.renderDOM.apply(tag.tag.subject, domArgs); - } - - function __loop__(iter, fn) - { - let nodeCount = 0; - for (let i = 0; i < iter.length; ++i) { - iter[i][0] = i; - iter[i][1] = nodeCount; - nodeCount += fn.apply(this, iter[i]); - } - return nodeCount; - } - - function __path__(parent, offset) - { - let root = parent; - - for (let i = 2; i < arguments.length; ++i) { - let index = arguments[i]; - if (i == 3) - index += offset; - - if (index == -1) - parent = parent.parentNode; - else - parent = parent.childNodes[index]; - } - - return parent; - } - let js = fnBlock.join(""); - // Exceptions on this line are often in the eval - this.renderDOM = eval(js); - }, - - generateDOM: function(path, blocks, args) - { - if (this.listeners || this.props) - this.generateNodePath(path, blocks); - - if (this.listeners) { - for (let i = 0; i < this.listeners.length; i += 2) { - let val = this.listeners[i+1]; - let arg = generateArg(val, path, args); - blocks.push('node.addEventListener("', this.listeners[i], - '", __bind__(this, ', arg, '), false);\n'); - } - } - - if (this.props) { - for (let name in this.props) { - let val = this.props[name]; - let arg = generateArg(val, path, args); - blocks.push('node.', name, ' = ', arg, ';\n'); - } - } - - this.generateChildDOM(path, blocks, args); - return 1; - }, - - generateNodePath: function(path, blocks) - { - blocks.push("var node = __path__(root, o"); - for (let i = 0; i < path.length; ++i) - blocks.push(",", path[i]); - blocks.push(");\n"); - }, - - generateChildDOM: function(path, blocks, args) - { - path.push(0); - for (let i = 0; i < this.children.length; ++i) { - let child = this.children[i]; - if (isTag(child)) - path[path.length - 1] += '+' + child.tag.generateDOM(path, blocks, args); - else - path[path.length - 1] += '+1'; - } - path.pop(); - }, - - /* - * We are just hiding from javascript.options.strict. For some reasons it's - * ok if we return undefined here. - * @return null or undefined or possibly a context. - */ - getContext: function() - { - return this.context; - } -}; - -// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - -DomplateEmbed.prototype = copyObject(DomplateTag.prototype, -{ - merge: function(args, oldTag) - { - this.value = oldTag ? oldTag.value : parseValue(args[0]); - this.attrs = oldTag ? oldTag.attrs : {}; - this.vars = oldTag ? copyArray(oldTag.vars) : []; - - let attrs = args[1]; - for (let name in attrs) { - let val = parseValue(attrs[name]); - this.attrs[name] = val; - readPartNames(val, this.vars); - } - - return creator(this, DomplateEmbed); - }, - - getVarNames: function(names) - { - if (this.value instanceof Parts) - names.push(this.value.parts[0].name); - - if (this.vars) - names.push.apply(names, this.vars); - }, - - generateMarkup: function(topBlock, topOuts, blocks, info) - { - this.addCode(topBlock, topOuts, blocks); - - blocks.push('__link__('); - addParts(this.value, '', blocks, info); - blocks.push(', __code__, __out__, {\n'); - - let lastName = null; - for (let name in this.attrs) { - if (lastName) - blocks.push(','); - lastName = name; - - let val = this.attrs[name]; - blocks.push('"', name, '":'); - addParts(val, '', blocks, info); - } - - blocks.push('});\n'); - }, - - generateDOM: function(path, blocks, args) - { - let embedName = 'e' + path.embedIndex++; - - this.generateNodePath(path, blocks); - - let valueName = 'd' + path.renderIndex++; - let argsName = 'd' + path.renderIndex++; - blocks.push(embedName + ' = __link__(node, ', valueName, ', ', - argsName, ');\n'); - - return embedName; - } -}); - -// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - -DomplateLoop.prototype = copyObject(DomplateTag.prototype, -{ - merge: function(args, oldTag) - { - this.isLoop = true; - this.varName = oldTag ? oldTag.varName : args[0]; - this.iter = oldTag ? oldTag.iter : parseValue(args[1]); - this.vars = []; - - this.children = oldTag ? copyArray(oldTag.children) : []; - - let offset = Math.min(args.length, 2); - parseChildren(args, offset, this.vars, this.children); - - return creator(this, DomplateLoop); - }, - - getVarNames: function(names) - { - if (this.iter instanceof Parts) - names.push(this.iter.parts[0].name); - - DomplateTag.prototype.getVarNames.apply(this, [names]); - }, - - generateMarkup: function(topBlock, topOuts, blocks, info) - { - this.addCode(topBlock, topOuts, blocks); - - let iterName; - if (this.iter instanceof Parts) { - let part = this.iter.parts[0]; - iterName = part.name; - - if (part.format) { - for (let i = 0; i < part.format.length; ++i) - iterName = part.format[i] + "(" + iterName + ")"; - } - } else - iterName = this.iter; - - blocks.push('__loop__.apply(this, [', iterName, ', __out__, function(', this.varName, ', __out__) {\n'); - this.generateChildMarkup(topBlock, topOuts, blocks, info); - this.addCode(topBlock, topOuts, blocks); - blocks.push('}]);\n'); - }, - - generateDOM: function(path, blocks, args) - { - let iterName = 'd' + path.renderIndex++; - let counterName = 'i' + path.loopIndex; - let loopName = 'l' + path.loopIndex++; - - if (!path.length) - path.push(-1, 0); - - let preIndex = path.renderIndex; - path.renderIndex = 0; - - let nodeCount = 0; - - let subBlocks = []; - let basePath = path[path.length-1]; - for (let i = 0; i < this.children.length; ++i) { - path[path.length - 1] = basePath + '+' + loopName + '+' + nodeCount; - - let child = this.children[i]; - if (isTag(child)) - nodeCount += '+' + child.tag.generateDOM(path, subBlocks, args); - else - nodeCount += '+1'; - } - - path[path.length - 1] = basePath + '+' + loopName; - - blocks.push(loopName,' = __loop__.apply(this, [', - iterName, ', function(', counterName, ',', loopName); - for (let i = 0; i < path.renderIndex; ++i) - blocks.push(',d' + i); - blocks.push(') {\n'); - blocks.push(subBlocks.join("")); - blocks.push('return ', nodeCount, ';\n'); - blocks.push('}]);\n'); - - path.renderIndex = preIndex; - - return loopName; - } -}); - -/////////////////////////////////////////////////////////////////////////// - -function Variable(name, format) -{ - this.name = name; - this.format = format; -} - -function Parts(parts) -{ - this.parts = parts; -} - -/////////////////////////////////////////////////////////////////////////// - -function parseParts(str) -{ - let re = /\$([_A-Za-z][_A-Za-z0-9.|]*)/g; - let index = 0; - let parts = []; - - let m; - while (m = re.exec(str)) { - let pre = str.substr(index, (re.lastIndex-m[0].length)-index); - if (pre) - parts.push(pre); - - let expr = m[1].split("|"); - parts.push(new Variable(expr[0], expr.slice(1))); - index = re.lastIndex; - } - - if (!index) - return str; - - let post = str.substr(index); - if (post) - parts.push(post); - - return new Parts(parts); -} - -function parseValue(val) -{ - return typeof(val) == 'string' ? parseParts(val) : val; -} - -function parseChildren(args, offset, vars, children) -{ - for (let i = offset; i < args.length; ++i) { - let val = parseValue(args[i]); - children.push(val); - readPartNames(val, vars); - } -} - -function readPartNames(val, vars) -{ - if (val instanceof Parts) { - for (let i = 0; i < val.parts.length; ++i) { - let part = val.parts[i]; - if (part instanceof Variable) - vars.push(part.name); - } - } -} - -function generateArg(val, path, args) -{ - if (val instanceof Parts) { - let vals = []; - for (let i = 0; i < val.parts.length; ++i) { - let part = val.parts[i]; - if (part instanceof Variable) { - let varName = 'd' + path.renderIndex++; - if (part.format) { - for (let j = 0; j < part.format.length; ++j) - varName = part.format[j] + '(' + varName + ')'; - } - - vals.push(varName); - } - else - vals.push('"' + part.replace(/"/g, '\\"') + '"'); - } - - return vals.join('+'); - } else { - args.push(val); - return 's' + path.staticIndex++; - } -} - -function addParts(val, delim, block, info, escapeIt) -{ - let vals = []; - if (val instanceof Parts) { - for (let i = 0; i < val.parts.length; ++i) { - let part = val.parts[i]; - if (part instanceof Variable) { - let partName = part.name; - if (part.format) { - for (let j = 0; j < part.format.length; ++j) - partName = part.format[j] + "(" + partName + ")"; - } - - if (escapeIt) - vals.push("__escape__(" + partName + ")"); - else - vals.push(partName); - } - else - vals.push('"' + part + '"'); - } - } else if (isTag(val)) { - info.args.push(val); - vals.push('s' + info.argIndex++); - } else - vals.push('"' + val + '"'); - - let parts = vals.join(delim); - if (parts) - block.push(delim, parts); -} - -function isTag(obj) -{ - return (typeof(obj) == "function" || obj instanceof Function) && !!obj.tag; -} - -/////////////////////////////////////////////////////////////////////////// -//// creator - -function creator(tag, cons) -{ - let fn = new Function( - "var tag = arguments.callee.tag;" + - "var cons = arguments.callee.cons;" + - "var newTag = new cons();" + - "return newTag.merge(arguments, tag);"); - - fn.tag = tag; - fn.cons = cons; - extend(fn, Renderer); - - return fn; -} - -/////////////////////////////////////////////////////////////////////////// -//// Utility functions - -function arrayInsert(array, index, other) -{ - for (let i = 0; i < other.length; ++i) - array.splice(i+index, 0, other[i]); - - return array; -} - -function cloneArray(array, fn) -{ - let newArray = []; - - if (fn) - for (let i = 0; i < array.length; ++i) - newArray.push(fn(array[i])); - else - for (let i = 0; i < array.length; ++i) - newArray.push(array[i]); - - return newArray; -} - -// fn, thisObject, args => thisObject.fn(args, arguments); -function bind() -{ - let args = cloneArray(arguments), fn = args.shift(), object = args.shift(); - return function bind() - { - return fn.apply(object, arrayInsert(cloneArray(args), 0, arguments)); - } -} - -function copyArray(oldArray) -{ - let array = []; - if (oldArray) - for (let i = 0; i < oldArray.length; ++i) - array.push(oldArray[i]); - return array; -} - -function copyObject(l, r) -{ - let m = {}; - extend(m, l); - extend(m, r); - return m; -} - -function escapeNewLines(value) -{ - return value.replace(/\r/gm, "\\r").replace(/\n/gm, "\\n"); -} - -function extend(l, r) -{ - for (let n in r) - l[n] = r[n]; -} - -function cropString(text, limit, alterText) -{ - if (!alterText) - alterText = "..."; //… - - text = text + ""; - - if (!limit) - limit = 88; // todo - var halfLimit = (limit / 2); - halfLimit -= 2; // adjustment for alterText's increase in size - - if (text.length > limit) - return text.substr(0, halfLimit) + alterText + - text.substr(text.length - halfLimit); - else - return text; -} - -function cropMultipleLines(text, limit) -{ - return escapeNewLines(this.cropString(text, limit)); -} - -function isVisible(elt) -{ - if (elt.localName) { - return elt.offsetWidth > 0 || elt.offsetHeight > 0 || - elt.localName.toLowerCase() in invisibleTags; - } else { - return elt.offsetWidth > 0 || elt.offsetHeight > 0; - } - // || isElementSVG(elt) || isElementMathML(elt); -} - -// Local Helpers - -function isElementXHTML(node) -{ - return node.nodeName == node.nodeName.toLowerCase(); -} - -function isContainerElement(element) -{ - let tag = element.localName.toLowerCase(); - switch (tag) { - case "script": - case "style": - case "iframe": - case "frame": - case "tabbrowser": - case "browser": - return true; - case "link": - return element.getAttribute("rel") == "stylesheet"; - case "embed": - return element.getSVGDocument(); - } - return false; -} - -domplateUtils.isWhitespace = function isWhitespace(text) -{ - return !reNotWhitespace.exec(text); -}; - -domplateUtils.isWhitespaceText = function isWhitespaceText(node) -{ - if (node instanceof DOM.HTMLAppletElement) - return false; - return node.nodeType == DOM.Node.TEXT_NODE && this.isWhitespace(node.nodeValue); -} - -function isSelfClosing(element) -{ - //if (isElementSVG(element) || isElementMathML(element)) - // return true; - var tag = element.localName.toLowerCase(); - return (selfClosingTags.hasOwnProperty(tag)); -}; - -function isEmptyElement(element) -{ - if (showTextNodesWithWhitespace) { - return !element.firstChild && isSelfClosing(element); - } else { - for (let child = element.firstChild; child; child = child.nextSibling) { - if (!domplateUtils.isWhitespaceText(child)) - return false; - } - } - return isSelfClosing(element); -} - -function getEmptyElementTag(node) -{ - let isXhtml= isElementXHTML(node); - if (isXhtml) - return HTMLTemplates.XEmptyElement.tag; - else - return HTMLTemplates.EmptyElement.tag; -} - -/** - * Determines if the given node has any children which are elements. - * - * @param {Element} element Element to test. - * @return true if immediate children of type Element exist, false otherwise - */ -function hasNoElementChildren(element) -{ - if (element.childElementCount != 0) - return false; - - return true; -} - -domplateUtils.getNodeTag = function getNodeTag(node, expandAll) -{ - if (node instanceof DOM.Element) { - if (node instanceof DOM.HTMLHtmlElement && node.ownerDocument - && node.ownerDocument.doctype) - return HTMLTemplates.HTMLHtmlElement.tag; - else if (node instanceof DOM.HTMLAppletElement) - return getEmptyElementTag(node); - else if (isContainerElement(node)) - return HTMLTemplates.Element.tag; - else if (isEmptyElement(node)) - return getEmptyElementTag(node); - else if (hasNoElementChildren(node)) - return HTMLTemplates.TextElement.tag; - else - return HTMLTemplates.Element.tag; - } - else if (node instanceof DOM.Text) - return HTMLTemplates.TextNode.tag; - else if (node instanceof DOM.CDATASection) - return HTMLTemplates.CDATANode.tag; - else if (node instanceof DOM.Comment) - return HTMLTemplates.CommentNode.tag; - else if (node instanceof DOM.SourceText) - return HTMLTemplates.SourceText.tag; - else - return HTMLTemplates.Nada.tag; -} - -function getNodeBoxTag(nodeBox) -{ - let re = /([^\s]+)NodeBox/; - let m = re.exec(nodeBox.className); - if (!m) - return null; - - let nodeBoxType = m[1]; - if (nodeBoxType == "container") - return HTMLTemplates.Element.tag; - else if (nodeBoxType == "text") - return HTMLTemplates.TextElement.tag; - else if (nodeBoxType == "empty") - return HTMLTemplates.EmptyElement.tag; -} - -/////////////////////////////////////////////////////////////////////////// -//// ArrayIterator - -function ArrayIterator(array) -{ - let index = -1; - - this.next = function() - { - if (++index >= array.length) - throw StopIteration; - - return array[index]; - }; -} - -function StopIteration() {} - -domplate.$break = function() -{ - throw StopIteration; -}; - -/////////////////////////////////////////////////////////////////////////// -//// Renderer - -var Renderer = -{ - renderHTML: function(args, outputs, self) - { - let code = []; - let markupArgs = [code, this.tag.getContext(), args, outputs]; - markupArgs.push.apply(markupArgs, this.tag.markupArgs); - this.tag.renderMarkup.apply(self ? self : this.tag.subject, markupArgs); - return code.join(""); - }, - - insertRows: function(args, before, self) - { - if (!args) - args = {}; - - this.tag.compile(); - - let outputs = []; - let html = this.renderHTML(args, outputs, self); - - let doc = before.ownerDocument; - let table = doc.createElement("table"); - table.innerHTML = html; - - let tbody = table.firstChild; - let parent = before.localName.toLowerCase() == "tr" ? before.parentNode : before; - let after = before.localName.toLowerCase() == "tr" ? before.nextSibling : null; - - let firstRow = tbody.firstChild, lastRow; - while (tbody.firstChild) { - lastRow = tbody.firstChild; - if (after) - parent.insertBefore(lastRow, after); - else - parent.appendChild(lastRow); - } - - // To save the next poor soul: - // In order to properly apply properties and event handlers on elements - // constructed by a FOR tag, the tag needs to be able to iterate up and - // down the tree, meaning if FOR is the root element as is the case with - // many insertRows calls, it will need to iterator over portions of the - // new parent. - // - // To achieve this end, __path__ defines the -1 operator which allows - // parent traversal. When combined with the offset that we calculate - // below we are able to iterate over the elements. - // - // This fails when applied to a non-loop element as non-loop elements - // Do not generate to proper path to bounce up and down the tree. - let offset = 0; - if (this.tag.isLoop) { - let node = firstRow.parentNode.firstChild; - for (; node && node != firstRow; node = node.nextSibling) - ++offset; - } - - // strict warning: this.tag.context undefined - let domArgs = [firstRow, this.tag.getContext(), offset]; - domArgs.push.apply(domArgs, this.tag.domArgs); - domArgs.push.apply(domArgs, outputs); - - this.tag.renderDOM.apply(self ? self : this.tag.subject, domArgs); - return [firstRow, lastRow]; - }, - - insertBefore: function(args, before, self) - { - return this.insertNode(args, before.ownerDocument, - function(frag) { - before.parentNode.insertBefore(frag, before); - }, self); - }, - - insertAfter: function(args, after, self) - { - return this.insertNode(args, after.ownerDocument, - function(frag) { - after.parentNode.insertBefore(frag, after.nextSibling); - }, self); - }, - - insertNode: function(args, doc, inserter, self) - { - if (!args) - args = {}; - - this.tag.compile(); - - let outputs = []; - let html = this.renderHTML(args, outputs, self); - - let range = doc.createRange(); - range.selectNode(doc.body); - let frag = range.createContextualFragment(html); - - let root = frag.firstChild; - root = inserter(frag) || root; - - let domArgs = [root, this.tag.context, 0]; - domArgs.push.apply(domArgs, this.tag.domArgs); - domArgs.push.apply(domArgs, outputs); - - this.tag.renderDOM.apply(self ? self : this.tag.subject, domArgs); - - return root; - }, - - replace: function(args, parent, self) - { - if (!args) - args = {}; - - this.tag.compile(); - - let outputs = []; - let html = this.renderHTML(args, outputs, self); - - let root; - if (parent.nodeType == DOM.Node.ELEMENT_NODE) { - parent.innerHTML = html; - root = parent.firstChild; - } else { - if (!parent || parent.nodeType != DOM.Node.DOCUMENT_NODE) - return; - - if (!womb || womb.ownerDocument != parent) - womb = parent.createElement("div"); - - womb.innerHTML = html; - - root = womb.firstChild; - } - - let domArgs = [root, this.tag.context, 0]; - domArgs.push.apply(domArgs, this.tag.domArgs); - domArgs.push.apply(domArgs, outputs); - this.tag.renderDOM.apply(self ? self : this.tag.subject, domArgs); - - return root; - }, - - append: function(args, parent, self) - { - if (!args) - args = {}; - - this.tag.compile(); - - let outputs = []; - let html = this.renderHTML(args, outputs, self); - - if (!womb || womb.ownerDocument != parent.ownerDocument) - womb = parent.ownerDocument.createElement("div"); - womb.innerHTML = html; - - let root = womb.firstChild; - while (womb.firstChild) - parent.appendChild(womb.firstChild); - - let domArgs = [root, this.tag.context, 0]; - domArgs.push.apply(domArgs, this.tag.domArgs); - domArgs.push.apply(domArgs, outputs); - - this.tag.renderDOM.apply(self ? self : this.tag.subject, domArgs); - - return root; - } -}; - -/////////////////////////////////////////////////////////////////////////// -//// defineTags macro - -/** - * Create default tags for a list of tag names. - * @param Arguments - * list of string arguments - */ - -function defineTags() -{ - for (let i = 0; i < arguments.length; ++i) { - let tagName = arguments[i]; - let fn = new Function("var newTag = new DomplateTag('" + tagName + - "'); return newTag.merge(arguments);"); - - let fnName = tagName.toUpperCase(); - domplate[fnName] = fn; - } -} - -defineTags( - "a", "button", "br", "canvas", "col", "colgroup", "div", "fieldset", "form", - "h1", "h2", "h3", "hr", "img", "input", "label", "legend", "li", "ol", - "optgroup", "option", "p", "pre", "select", "b", "span", "strong", "table", - "tbody", "td", "textarea", "tfoot", "th", "thead", "tr", "tt", "ul", "iframe", - "code" -); - -/////////////////////////////////////////////////////////////////////////// -//// HTMLTemplates - -let HTMLTemplates = { - showTextNodesWithWhitespace: false -}; - -let BaseTemplates = { - showTextNodesWithWhitespace: false -}; - -/////////////////////////////////////////////////////////////////////////// -//// HTMLTemplates.Reps - -BaseTemplates.OBJECTLINK = domplate.A({ - "class": "objectLink objectLink-$className a11yFocus", - _repObject: "$object" -}); - -BaseTemplates.Rep = domplate( -{ - className: "", - inspectable: true, - - supportsObject: function(object, type) - { - return false; - }, - - inspectObject: function(object, context) - { - // Firebug.chrome.select(object); // todo - }, - - browseObject: function(object, context) - { - }, - - persistObject: function(object, context) - { - }, - - getRealObject: function(object, context) - { - return object; - }, - - /** - * Return a sensible string title for the given object, removing any wrapper - * information from it. - * @param aObject - * The object to get the title of. - * @returns string - */ - - getTitle: function(aObject) - { - // e.g., [object XPCWrappedNative [object foo]] - let label = safeToString(aObject); - - const re =/\[object ([^\]]*)/; - let objectMatch = re.exec(label); - let secondObjectMatch = null; - if (objectMatch) { - // e.g., XPCWrappedNative [object foo - secondObjectMatch = re.exec(objectMatch[1]); - } - - if (secondObjectMatch) - return secondObjectMatch[1]; // eg foo - else - return objectMatch ? objectMatch[1] : label; - }, - - getTooltip: function(object) - { - return null; - }, - - /** - * Called by chrome.onContextMenu to build the context menu when the - * underlying object has this rep. - * See also Panel for a similar function also called by onContextMenu. - * Extensions may monkey patch and chain off this call. - * @param object: the 'realObject', a model value, eg a DOM property - * @param target: the HTML element clicked on. - * @param context: the context, probably FirebugContext - * @returns an array of menu items. - */ - getContextMenuItems: function(object, target, context) - { - return []; - }, - - ///////////////////////////////////////////////////////////////////////// - // Convenience for domplates - - STR: function(name) - { - return name; // todo getproperty? - }, - - cropString: function(text) - { - return cropString(text); - }, - - cropMultipleLines: function(text, limit) - { - return cropMultipleLines(text, limit); - }, - - toLowerCase: function(text) - { - return text ? text.toLowerCase() : text; - }, - - plural: function(n) - { - return n == 1 ? "" : "s"; - } -}); - -BaseTemplates.Element = domplate(BaseTemplates.Rep, -{ - tag: - BaseTemplates.OBJECTLINK( - "<", - domplate.SPAN({"class": "nodeTag"}, - "$object.localName|toLowerCase"), - domplate.FOR("attr", "$object|attrIterator", - " $attr.localName="", - domplate.SPAN({"class": "nodeValue"}, - "$attr.nodeValue"), - """ - ), - ">" - ), - - shortTag: - BaseTemplates.OBJECTLINK( - domplate.SPAN({"class": "$object|getVisible"}, - domplate.SPAN({"class": "selectorTag"}, - "$object|getSelectorTag"), - domplate.SPAN({"class": "selectorId"}, - "$object|getSelectorId"), - domplate.SPAN({"class": "selectorClass"}, - "$object|getSelectorClass"), - domplate.SPAN({"class": "selectorValue"}, - "$object|getValue") - ) - ), - - getVisible: function(elt) - { - return isVisible(elt) ? "" : "selectorHidden"; - }, - - getSelectorTag: function(elt) - { - return elt.localName.toLowerCase(); - }, - - getSelectorId: function(elt) - { - return elt.id ? ("#" + elt.id) : ""; - }, - - getSelectorClass: function(elt) - { - return elt.getAttribute("class") - ? ("." + elt.getAttribute("class").split(" ")[0]) - : ""; - }, - - getValue: function(elt) - { // todo getFileName - let value; -/* - if (elt instanceof HTMLImageElement) - value = getFileName(elt.getAttribute("src")); - else if (elt instanceof HTMLAnchorElement) - value = getFileName(elt.getAttribute("href")); - else if (elt instanceof HTMLInputElement) - value = elt.getAttribute("value"); - else if (elt instanceof HTMLFormElement) - value = getFileName(elt.getAttribute("action")); - else if (elt instanceof HTMLScriptElement) - value = getFileName(elt.getAttribute("src")); - - return value ? " " + cropMultipleLines(value, 20) : ""; */ - // trying a simplified version from above commented section - // todo - if (elt instanceof DOM.HTMLImageElement) - value = elt.getAttribute("src"); - else if (elt instanceof DOM.HTMLAnchorElement) - value = elt.getAttribute("href"); - else if (elt instanceof DOM.HTMLInputElement) - value = elt.getAttribute("value"); - else if (elt instanceof DOM.HTMLFormElement) - value = elt.getAttribute("action"); - else if (elt instanceof DOM.HTMLScriptElement) - value = elt.getAttribute("src"); - - return value ? " " + cropMultipleLines(value, 20) : ""; - }, - - attrIterator: function(elt) - { - let attrs = []; - let idAttr, classAttr; - if (elt.attributes) { - for (let i = 0; i < elt.attributes.length; ++i) { - var attr = elt.attributes[i]; - if (attr.localName.indexOf("-moz-math") != -1) - continue; - else if (attr.localName == "id") - idAttr = attr; - else if (attr.localName == "class") - classAttr = attr; - else - attrs.push(attr); - } - } - if (classAttr) - attrs.unshift(classAttr); - if (idAttr) - attrs.unshift(idAttr); - return attrs; - }, - - shortAttrIterator: function(elt) - { - let attrs = []; - if (elt.attributes) { - for (let i = 0; i < elt.attributes.length; ++i) { - let attr = elt.attributes[i]; - if (attr.localName == "id" || attr.localName == "class") - attrs.push(attr); - } - } - - return attrs; - }, - - getHidden: function(elt) - { - return isVisible(elt) ? "" : "nodeHidden"; - }, - -/* getXPath: function(elt) - { - return getElementTreeXPath(elt); // todo - }, */ - - getNodeTextGroups: function(element) - { - let text = element.textContent; - return [{str: text, 'class': '', extra: ''}]; - }, - - className: "element", - - supportsObject: function(object, type) - { - return object instanceof DOM.Element; - }, - - browseObject: function(elt, context) - { - let tag = elt.localName.toLowerCase(); - return true; - }, -}); - - -/////////////////////////////////////////////////////////////////////////// -//// HTMLTemplates.tags - -BaseTemplates.AttrTag = - domplate.SPAN({"class": "nodeAttr editGroup"}, - " ", - domplate.SPAN({"class": "nodeName editable"}, "$attr.nodeName"), - "="", - domplate.SPAN({"class": "nodeValue editable", "data-attributeName": "$attr.nodeName"}, "$attr.nodeValue"), - """); - -BaseTemplates.TextTag = - domplate.SPAN({"class": "nodeText editable"}, - domplate.FOR("chr", "$object|getNodeTextGroups", - domplate.SPAN({"class": "$chr.class $chr.extra"}, - "$chr.str"))); - -/////////////////////////////////////////////////////////////////////////// -//// HTMLTemplates - - - -HTMLTemplates.CompleteElement = domplate(BaseTemplates.Element, -{ - tag: - domplate.DIV({"class": - "nodeBox open $object|getHidden repIgnore", - _repObject: "$object", role : 'presentation'}, - domplate.DIV({"class": "nodeLabel", role: "presentation"}, - domplate.SPAN({"class": "nodeLabelBox repTarget repTarget", - role : 'treeitem', 'aria-expanded' : 'false'}, - "<", - domplate.SPAN({"class": "nodeTag"}, - "$object.nodeName|toLowerCase"), - domplate.FOR("attr", "$object|attrIterator", BaseTemplates.AttrTag), - domplate.SPAN({"class": "nodeBracket"}, ">") - ) - ), - domplate.DIV({"class": "nodeChildBox", role :"group"}, - domplate.FOR("child", "$object|childIterator", - domplate.TAG("$child|getNodeTag", {object: "$child"}) - ) - ), - domplate.DIV({"class": "nodeCloseLabel", role:"presentation"}, - "</", - domplate.SPAN({"class": "nodeTag"}, - "$object.nodeName|toLowerCase"), - ">" - ) - ), - - getNodeTag: function(node) - { - return domplateUtils.getNodeTag(node, true); - }, - - childIterator: function(node) - { - if (node.contentDocument) - return [node.contentDocument.documentElement]; - - if (this.showTextNodesWithWhitespace) - return cloneArray(node.childNodes); - else { - let nodes = []; - for (let child = node.firstChild; child; child = child.nextSibling) { - if (child.nodeType != DOM.Node.TEXT_NODE || !domplateUtils.isWhitespaceText(child)) - nodes.push(child); - } - return nodes; - } - } -}); - -HTMLTemplates.SoloElement = domplate(HTMLTemplates.CompleteElement, -{ - tag: - domplate.DIV({"class": "soloElement", - onmousedown: "$onMouseDown"}, - HTMLTemplates.CompleteElement.tag), - - onMouseDown: function(event) - { - for (let child = event.target; child; child = child.parentNode) { - if (child.repObject) { // todo - // let panel = Firebug.getElementPanel(child); - // Firebug.chrome.select(child.repObject); - break; - } - } - } -}); - -HTMLTemplates.Element = domplate(BaseTemplates.Element, -{ - tag: - domplate.DIV({"class": "nodeBox containerNodeBox $object|getHidden repIgnore", - _repObject: "$object", role: "presentation"}, - domplate.DIV({"class": "nodeLabel", role: "presentation"}, - domplate.IMG({"class": "twisty", role: "presentation"}), - domplate.SPAN({"class": "nodeLabelBox repTarget", - role: 'treeitem', 'aria-expanded': 'false'}, - "<", - domplate.SPAN({"class": "nodeTag"}, - "$object.nodeName|toLowerCase"), - domplate.FOR("attr", "$object|attrIterator", BaseTemplates.AttrTag), - domplate.SPAN({"class": "nodeBracket editable insertBefore"}, - ">") - ) - ), - domplate.DIV({"class": "nodeChildBox", role: "group"}), /* nodeChildBox is special signal in insideOutBox */ - domplate.DIV({"class": "nodeCloseLabel", role: "presentation"}, - domplate.SPAN({"class": "nodeCloseLabelBox repTarget"}, - "</", - domplate.SPAN({"class": "nodeTag"}, "$object.nodeName|toLowerCase"), - ">" - ) - ) - ) -}); - -HTMLTemplates.HTMLHtmlElement = domplate(BaseTemplates.Element, -{ - tag: - domplate.DIV({"class": - "nodeBox htmlNodeBox containerNodeBox $object|getHidden repIgnore", - _repObject: "$object", role: "presentation"}, - domplate.DIV({"class": "docType"}, - "$object|getDocType"), - domplate.DIV({"class": "nodeLabel", role: "presentation"}, - domplate.IMG({"class": "twisty", role: "presentation"}), - domplate.SPAN({"class": "nodeLabelBox repTarget", - role: 'treeitem', 'aria-expanded' : 'false'}, - "<", - domplate.SPAN({"class": "nodeTag"}, - "$object.nodeName|toLowerCase"), - domplate.FOR("attr", "$object|attrIterator", BaseTemplates.AttrTag), - domplate.SPAN({"class": - "nodeBracket editable insertBefore"}, ">") - ) - ), /* nodeChildBox is special signal in insideOutBox */ - domplate.DIV({"class": "nodeChildBox", role: "group"}), - domplate.DIV({"class": "nodeCloseLabel", role: "presentation"}, - domplate.SPAN({"class": "nodeCloseLabelBox repTarget"}, - "</", - domplate.SPAN({"class": "nodeTag"}, - "$object.nodeName|toLowerCase"), - ">" - ) - ) - ), - - getDocType: function(obj) - { - let doctype = obj.ownerDocument.doctype; - return ''; - } -}); - -HTMLTemplates.TextElement = domplate(BaseTemplates.Element, -{ - tag: - domplate.DIV({"class": - "nodeBox textNodeBox $object|getHidden repIgnore", - _repObject: "$object", role: 'presentation'}, - domplate.DIV({"class": "nodeLabel", role: "presentation"}, - domplate.SPAN({"class": "nodeLabelBox repTarget", - role: 'treeitem'}, - "<", - domplate.SPAN({"class": "nodeTag"}, - "$object.nodeName|toLowerCase"), - domplate.FOR("attr", "$object|attrIterator", BaseTemplates.AttrTag), - domplate.SPAN({"class": - "nodeBracket editable insertBefore"}, ">"), - BaseTemplates.TextTag, - "</", - domplate.SPAN({"class": "nodeTag"}, - "$object.nodeName|toLowerCase"), - ">" - ) - ) - ) -}); - -HTMLTemplates.EmptyElement = domplate(BaseTemplates.Element, -{ - tag: - domplate.DIV({"class": - "nodeBox emptyNodeBox $object|getHidden repIgnore", - _repObject: "$object", role: 'presentation'}, - domplate.DIV({"class": "nodeLabel", role: "presentation"}, - domplate.SPAN({"class": "nodeLabelBox repTarget", - role: 'treeitem'}, - "<", - domplate.SPAN({"class": "nodeTag"}, - "$object.nodeName|toLowerCase"), - domplate.FOR("attr", "$object|attrIterator", BaseTemplates.AttrTag), - domplate.SPAN({"class": - "nodeBracket editable insertBefore"}, ">") - ) - ) - ) -}); - -HTMLTemplates.XEmptyElement = domplate(BaseTemplates.Element, -{ - tag: - domplate.DIV({"class": - "nodeBox emptyNodeBox $object|getHidden repIgnore", - _repObject: "$object", role: 'presentation'}, - domplate.DIV({"class": "nodeLabel", role: "presentation"}, - domplate.SPAN({"class": "nodeLabelBox repTarget", - role : 'treeitem'}, - "<", - domplate.SPAN({"class": "nodeTag"}, - "$object.nodeName|toLowerCase"), - domplate.FOR("attr", "$object|attrIterator", BaseTemplates.AttrTag), - domplate.SPAN({"class": - "nodeBracket editable insertBefore"}, "/>") - ) - ) - ) -}); - -HTMLTemplates.AttrNode = domplate(BaseTemplates.Element, -{ - tag: BaseTemplates.AttrTag -}); - -HTMLTemplates.TextNode = domplate(BaseTemplates.Element, -{ - tag: - domplate.DIV({"class": "nodeBox", _repObject: "$object", - role: 'presentation'}, BaseTemplates.TextTag) -}); - -HTMLTemplates.CDATANode = domplate(BaseTemplates.Element, -{ - tag: - domplate.DIV({"class": "nodeBox", _repObject: "$object", - role: 'presentation'}, - "<![CDATA[", - domplate.SPAN({"class": "nodeText nodeCDATA editable"}, - "$object.nodeValue"), - "]]>") -}); - -HTMLTemplates.CommentNode = domplate(BaseTemplates.Element, -{ - tag: - domplate.DIV({"class": "nodeBox nodeComment", - _repObject: "$object", role : 'presentation'}, - "<!--", - domplate.SPAN({"class": "nodeComment editable"}, - "$object.nodeValue"), - "-->") -}); - -HTMLTemplates.Nada = domplate(BaseTemplates.Rep, -{ - tag: domplate.SPAN(""), - className: "nada" -}); - diff --git a/browser/devtools/highlighter/inspector.html b/browser/devtools/highlighter/inspector.html deleted file mode 100644 index 88a351c5d950..000000000000 --- a/browser/devtools/highlighter/inspector.html +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - -
- -
- - diff --git a/browser/devtools/highlighter/inspector.jsm b/browser/devtools/highlighter/inspector.jsm index c8d2e44702fd..02de2ac5a293 100644 --- a/browser/devtools/highlighter/inspector.jsm +++ b/browser/devtools/highlighter/inspector.jsm @@ -13,12 +13,12 @@ var EXPORTED_SYMBOLS = ["InspectorUI"]; Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/XPCOMUtils.jsm"); -Cu.import("resource:///modules/TreePanel.jsm"); Cu.import("resource:///modules/devtools/MarkupView.jsm"); Cu.import("resource:///modules/highlighter.jsm"); Cu.import("resource:///modules/devtools/LayoutView.jsm"); Cu.import("resource:///modules/devtools/LayoutHelpers.jsm"); Cu.import("resource:///modules/devtools/EventEmitter.jsm"); +Cu.import("resource:///modules/devtools/DOMHelpers.jsm"); // Inspector notifications dispatched through the nsIObserverService. const INSPECTOR_NOTIFICATIONS = { diff --git a/browser/devtools/jar.mn b/browser/devtools/jar.mn index 760329c7e708..1a509ef2927c 100644 --- a/browser/devtools/jar.mn +++ b/browser/devtools/jar.mn @@ -3,7 +3,6 @@ # file, You can obtain one at http://mozilla.org/MPL/2.0/. browser.jar: - content/browser/inspector.html (highlighter/inspector.html) content/browser/devtools/markup-view.xhtml (markupview/markup-view.xhtml) content/browser/devtools/markup-view.css (markupview/markup-view.css) content/browser/NetworkPanel.xhtml (webconsole/NetworkPanel.xhtml) diff --git a/browser/devtools/shared/DOMHelpers.jsm b/browser/devtools/shared/DOMHelpers.jsm new file mode 100644 index 000000000000..489db50bef7c --- /dev/null +++ b/browser/devtools/shared/DOMHelpers.jsm @@ -0,0 +1,124 @@ +/* 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/. */ + +const EXPORTED_SYMBOLS = ["DOMHelpers"]; + +/** + * DOMHelpers + * Makes DOM traversal easier. Goes through iframes. + * + * @constructor + * @param nsIDOMWindow aWindow + * The content window, owning the document to traverse. + */ +function DOMHelpers(aWindow) { + this.window = aWindow; +}; + +DOMHelpers.prototype = { + getParentObject: function Helpers_getParentObject(node) + { + let parentNode = node ? node.parentNode : null; + + if (!parentNode) { + // Documents have no parentNode; Attr, Document, DocumentFragment, Entity, + // and Notation. top level windows have no parentNode + if (node && node == this.window.Node.DOCUMENT_NODE) { + // document type + if (node.defaultView) { + let embeddingFrame = node.defaultView.frameElement; + if (embeddingFrame) + return embeddingFrame.parentNode; + } + } + // a Document object without a parentNode or window + return null; // top level has no parent + } + + if (parentNode.nodeType == this.window.Node.DOCUMENT_NODE) { + if (parentNode.defaultView) { + return parentNode.defaultView.frameElement; + } + // parent is document element, but no window at defaultView. + return null; + } + + if (!parentNode.localName) + return null; + + return parentNode; + }, + + getChildObject: function Helpers_getChildObject(node, index, previousSibling, + showTextNodesWithWhitespace) + { + if (!node) + return null; + + if (node.contentDocument) { + // then the node is a frame + if (index == 0) { + return node.contentDocument.documentElement; // the node's HTMLElement + } + return null; + } + + if (node instanceof this.window.GetSVGDocument) { + let svgDocument = node.getSVGDocument(); + if (svgDocument) { + // then the node is a frame + if (index == 0) { + return svgDocument.documentElement; // the node's SVGElement + } + return null; + } + } + + let child = null; + if (previousSibling) // then we are walking + child = this.getNextSibling(previousSibling); + else + child = this.getFirstChild(node); + + if (showTextNodesWithWhitespace) + return child; + + for (; child; child = this.getNextSibling(child)) { + if (!this.isWhitespaceText(child)) + return child; + } + + return null; // we have no children worth showing. + }, + + getFirstChild: function Helpers_getFirstChild(node) + { + let SHOW_ALL = Components.interfaces.nsIDOMNodeFilter.SHOW_ALL; + this.treeWalker = node.ownerDocument.createTreeWalker(node, + SHOW_ALL, null, false); + return this.treeWalker.firstChild(); + }, + + getNextSibling: function Helpers_getNextSibling(node) + { + let next = this.treeWalker.nextSibling(); + + if (!next) + delete this.treeWalker; + + return next; + }, + + isWhitespaceText: function Helpers_isWhitespaceText(node) + { + return node.nodeType == this.window.Node.TEXT_NODE && + !/[^\s]/.exec(node.nodeValue); + }, + + destroy: function Helpers_destroy() + { + delete this.window; + delete this.treeWalker; + } +}; diff --git a/browser/themes/gnomestripe/devtools/htmlpanel.css b/browser/themes/gnomestripe/devtools/htmlpanel.css deleted file mode 100644 index 399b70772c66..000000000000 --- a/browser/themes/gnomestripe/devtools/htmlpanel.css +++ /dev/null @@ -1,402 +0,0 @@ -/* - * Software License Agreement (BSD License) - * - * Copyright (c) 2007, Parakey Inc. - * All rights reserved. - * - * Redistribution and use of this software in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * - * * Redistributions of source code must retain the above - * copyright notice, this list of conditions and the - * following disclaimer. - * - * * Redistributions in binary form must reproduce the above - * copyright notice, this list of conditions and the - * following disclaimer in the documentation and/or other - * materials provided with the distribution. - * - * * Neither the name of Parakey Inc. nor the names of its - * contributors may be used to endorse or promote products - * derived from this software without specific prior - * written permission of Parakey Inc. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR - * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND - * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR - * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL - * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER - * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT - * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -/* - * Creator: - * Joe Hewitt - * Contributors - * John J. Barton (IBM Almaden) - * Jan Odvarko (Mozilla Corp.) - * Max Stepanov (Aptana Inc.) - * Rob Campbell (Mozilla Corp.) - * Hans Hillen (Paciello Group, Mozilla) - * Curtis Bartley (Mozilla Corp.) - * Mike Collins (IBM Almaden) - * Kevin Decker - * Mike Ratcliffe (Comartis AG) - * Hernan Rodríguez Colmeiro - * Austin Andrews - * Christoph Dorn - * Steven Roussey (AppCenter Inc, Network54) - */ - -html { - background-color: -moz-dialog; -} - -body { - margin: 0; - overflow: auto; - font-family: Lucida Grande, sans-serif; - font-size: 11px; - padding-top: 5px; -} - -h1 { - font-size: 17px; - border-bottom: 1px solid threedlightshadow; -} - -a { - color: #0000ff; -} - -pre { - margin: 0; - font: inherit; -} - -code { - display: block; - white-space: pre; -} - -/* DOMPlate */ - -.objectLink-element, -.objectLink-textNode, -.objectLink-function, -.objectBox-stackTrace, -.objectLink-profile { - font-family: Menlo, Andale Mono, monospace; -} - -.objectLink-textNode { - white-space: pre-wrap; -} - -.objectLink-styleRule, -.objectLink-element, -.objectLink-textNode { - color: #000088; -} - -.selectorTag, -.selectorId, -.selectorClass { - font-family: Menlo, Andale Mono, monospace; - font-weight: normal; -} - -.selectorTag { - color: #0000FF; -} - -.selectorId { - color: DarkBlue; -} - -.selectorClass { - color: red; -} - -.selectorHidden > .selectorTag { - color: #5F82D9; -} - -.selectorHidden > .selectorId { - color: #888888; -} - -.selectorHidden > .selectorClass { - color: #D86060; -} - -.selectorValue { - font-family: Menlo, Andale Mono, monospace; - font-style: italic; - color: #555555; -} - -.panelNode-html { - -moz-box-sizing: padding-box; - padding: 4px 0 0 2px; -} - -.nodeBox { - position: relative; - font-family: Menlo, Andale Mono, monospace; - padding-left: 13px; - -moz-user-select: -moz-none; -} - -.nodeBox.search-selection { - -moz-user-select: text; -} - -.twisty { - position: absolute; - left: 0px; - padding: 8px; -} - -.nodeChildBox { - margin-left: 12px; - display: none; -} - -.nodeLabel, -.nodeCloseLabel { - margin: -2px 2px 0 2px; - border: 2px solid transparent; - border-radius: 3px; - padding: 0 2px; - color: #000088; -} - -.nodeCloseLabel { - display: none; -} - -.nodeTag { - cursor: pointer; - color: blue; -} - -.nodeValue { - color: #FF0000; - font-weight: normal; -} - -.nodeText, -.nodeComment { - margin: 0 2px; - vertical-align: top; -} - -.nodeText { - color: #333333; -} - -.docType { - position: absolute; - /* position DOCTYPE element above/outside the "nodeBox" that contains it */ - /* Note: to be fixed in Bug #688439 */ - top: -16px; - font-family: Menlo, Andale Mono, monospace; - padding-left: 8px; - color: #999; - white-space: nowrap; - font-style: italic; -} - -.htmlNodeBox { - /* make room for DOCTYPE element to be rendered above/outside "nodeBox" */ - /* Note: to be fixed in Bug #688439 */ - margin-top: 16px; -} - -.nodeWhiteSpace { - border: 1px solid LightGray; - white-space: pre; /* otherwise the border will be collapsed around zero pixels */ - margin-left: 1px; - color: gray; -} - -.nodeWhiteSpace_Space { - border: 1px solid #ddd; -} - -.nodeTextEntity { - border: 1px solid gray; - white-space: pre; /* otherwise the border will be collapsed around zero pixels */ - margin-left: 1px; -} - -.nodeComment { - color: DarkGreen; -} - -.nodeBox.highlightOpen > .nodeLabel { - background-color: #EEEEEE; -} - -.nodeBox.highlightOpen > .nodeCloseLabel, -.nodeBox.highlightOpen > .nodeChildBox, -.nodeBox.open > .nodeCloseLabel, -.nodeBox.open > .nodeChildBox { - display: block; -} - -.nodeBox.selected > .nodeLabel > .nodeLabelBox, -.nodeBox.selected > .nodeLabel { - border-color: Highlight; - background-color: Highlight; - color: HighlightText !important; -} - -.nodeBox.selected > .nodeLabel > .nodeLabelBox, -.nodeBox.selected > .nodeLabel > .nodeLabelBox > .nodeTag, -.nodeBox.selected > .nodeLabel > .nodeLabelBox > .nodeAttr > .nodeValue, -.nodeBox.selected > .nodeLabel > .nodeLabelBox > .nodeText { - color: inherit !important; -} - -.nodeBox.highlighted > .nodeLabel { - border-color: Highlight !important; - background-color: cyan !important; - color: #000000 !important; -} - -.nodeBox.highlighted > .nodeLabel > .nodeLabelBox, -.nodeBox.highlighted > .nodeLabel > .nodeLabelBox > .nodeTag, -.nodeBox.highlighted > .nodeLabel > .nodeLabelBox > .nodeAttr > .nodeValue, -.nodeBox.highlighted > .nodeLabel > .nodeLabelBox > .nodeText { - color: #000000 !important; -} - -.nodeBox.nodeHidden .nodeLabel > .nodeLabelBox, -.nodeBox.nodeHidden .nodeCloseLabel, -.nodeBox.nodeHidden .nodeLabel > .nodeLabelBox > .nodeText, -.nodeBox.nodeHidden .nodeText { - color: #888888; -} - -.nodeBox.nodeHidden .nodeLabel > .nodeLabelBox > .nodeTag, -.nodeBox.nodeHidden .nodeCloseLabel > .nodeCloseLabelBox > .nodeTag { - color: #5F82D9; -} - -.nodeBox.nodeHidden .nodeLabel > .nodeLabelBox > .nodeAttr > .nodeValue { - color: #D86060; -} - -.nodeBox.nodeHidden.selected > .nodeLabel > .nodeLabelBox, -.nodeBox.nodeHidden.selected > .nodeLabel > .nodeLabelBox > .nodeTag, -.nodeBox.nodeHidden.selected > .nodeLabel > .nodeLabelBox > .nodeAttr > .nodeValue, -.nodeBox.nodeHidden.selected > .nodeLabel > .nodeLabelBox > .nodeText { - color: SkyBlue !important; -} - -.nodeBox.mutated > .nodeLabel, -.nodeAttr.mutated, -.nodeValue.mutated, -.nodeText.mutated, -.nodeBox.mutated > .nodeText { - background-color: #EFFF79; - color: #FF0000 !important; -} - -.nodeBox.selected.mutated > .nodeLabel, -.nodeBox.selected.mutated > .nodeLabel > .nodeLabelBox, -.nodeBox.selected > .nodeLabel > .nodeLabelBox > .nodeAttr.mutated > .nodeValue, -.nodeBox.selected > .nodeLabel > .nodeLabelBox > .nodeAttr > .nodeValue.mutated, -.nodeBox.selected > .nodeLabel > .nodeLabelBox > .nodeText.mutated { - background-color: #EFFF79; - border-color: #EFFF79; - color: #FF0000 !important; -} - -.logRow-dirxml { - padding-left: 0; -} - -.soloElement > .nodeBox { - padding-left: 0; -} - -.useA11y .nodeLabel.focused { - outline: 2px solid #FF9933; - -moz-outline-radius: 3px; - outline-offset: -2px; -} - -.useA11y .nodeLabelBox:focus { - outline: none; -} - -/* from panel.css */ - -/* HTML panel */ - -.nodeBox.selected > .nodeLabel > .nodeLabelBox, -.nodeBox.selected > .nodeLabel { - border-color: #3875d7; - background-color: #3875d7; - color: #FFFFFF !important; -} - -.nodeBox.highlighted > .nodeLabel { - border-color: #3875d7 !important; -} - -/************************************************************************************************/ -/* Twisties */ - -.twisty -{ - -moz-appearance: treetwisty; -} - -.nodeBox.highlightOpen > .nodeLabel > .twisty, -.nodeBox.open > .nodeLabel > .twisty -{ - -moz-appearance: treetwistyopen; -} - -/************************************************************************************************/ -/* HTML panel */ - -.nodeBox.selected > .nodeLabel > .nodeLabelBox, -.nodeBox.selected > .nodeLabel { - border-color: #3875d7; - background-color: #3875d7; - color: #FFFFFF !important; -} - -.nodeBox.highlighted > .nodeLabel { - border-color: #3875d7 !important; -} - -.editingAttributeValue { - background-color: #492; -} - -#attribute-editor { - visibility: hidden; - position: absolute; - z-index: 5000; - background-color: #fff; - border: 1px solid #000; -} - -#attribute-editor.editing { - visibility: visible; -} - -#attribute-editor-input { - border: none; - padding: 2px 5px; - font-family: Menlo, Andale Mono, monospace; - font-size: 11px; -} diff --git a/browser/themes/gnomestripe/jar.mn b/browser/themes/gnomestripe/jar.mn index df1daa3c2be5..94c87604abe4 100644 --- a/browser/themes/gnomestripe/jar.mn +++ b/browser/themes/gnomestripe/jar.mn @@ -108,7 +108,6 @@ browser.jar: skin/classic/browser/devtools/webconsole_networkpanel.css (devtools/webconsole_networkpanel.css) skin/classic/browser/devtools/webconsole.png (devtools/webconsole.png) skin/classic/browser/devtools/commandline.css (devtools/commandline.css) - skin/classic/browser/devtools/htmlpanel.css (devtools/htmlpanel.css) skin/classic/browser/devtools/markup-view.css (devtools/markup-view.css) skin/classic/browser/devtools/orion.css (devtools/orion.css) skin/classic/browser/devtools/orion-container.css (devtools/orion-container.css) diff --git a/browser/themes/pinstripe/devtools/htmlpanel.css b/browser/themes/pinstripe/devtools/htmlpanel.css deleted file mode 100644 index 61aa39cd867d..000000000000 --- a/browser/themes/pinstripe/devtools/htmlpanel.css +++ /dev/null @@ -1,392 +0,0 @@ -/* - * Software License Agreement (BSD License) - * - * Copyright (c) 2007, Parakey Inc. - * All rights reserved. - * - * Redistribution and use of this software in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * - * * Redistributions of source code must retain the above - * copyright notice, this list of conditions and the - * following disclaimer. - * - * * Redistributions in binary form must reproduce the above - * copyright notice, this list of conditions and the - * following disclaimer in the documentation and/or other - * materials provided with the distribution. - * - * * Neither the name of Parakey Inc. nor the names of its - * contributors may be used to endorse or promote products - * derived from this software without specific prior - * written permission of Parakey Inc. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR - * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND - * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR - * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL - * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER - * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT - * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -/* - * Creator: - * Joe Hewitt - * Contributors - * John J. Barton (IBM Almaden) - * Jan Odvarko (Mozilla Corp.) - * Max Stepanov (Aptana Inc.) - * Rob Campbell (Mozilla Corp.) - * Hans Hillen (Paciello Group, Mozilla) - * Curtis Bartley (Mozilla Corp.) - * Mike Collins (IBM Almaden) - * Kevin Decker - * Mike Ratcliffe (Comartis AG) - * Hernan Rodríguez Colmeiro - * Austin Andrews - * Christoph Dorn - * Steven Roussey (AppCenter Inc, Network54) - */ - -html { - background-color: -moz-dialog; -} - -body { - margin: 0; - overflow: auto; - font-family: Lucida Grande, sans-serif; - font-size: 11px; - padding-top: 5px; -} - -h1 { - font-size: 17px; - border-bottom: 1px solid threedlightshadow; -} - -a { - color: #0000ff; -} - -pre { - margin: 0; - font: inherit; -} - -code { - display: block; - white-space: pre; -} - -/* DOMPlate */ - -.objectLink-element, -.objectLink-textNode, -.objectLink-function, -.objectBox-stackTrace, -.objectLink-profile { - font-family: Menlo, Andale Mono, monospace; -} - -.objectLink-textNode { - white-space: pre-wrap; -} - -.objectLink-styleRule, -.objectLink-element, -.objectLink-textNode { - color: #000088; -} - -.selectorTag, -.selectorId, -.selectorClass { - font-family: Menlo, Andale Mono, monospace; - font-weight: normal; -} - -.selectorTag { - color: #0000FF; -} - -.selectorId { - color: DarkBlue; -} - -.selectorClass { - color: red; -} - -.selectorHidden > .selectorTag { - color: #5F82D9; -} - -.selectorHidden > .selectorId { - color: #888888; -} - -.selectorHidden > .selectorClass { - color: #D86060; -} - -.selectorValue { - font-family: Menlo, Andale Mono, monospace; - font-style: italic; - color: #555555; -} - -.panelNode-html { - -moz-box-sizing: padding-box; - padding: 4px 0 0 2px; -} - -.nodeBox { - position: relative; - font-family: Menlo, Andale Mono, monospace; - padding-left: 13px; - -moz-user-select: -moz-none; -} - -.nodeBox.search-selection { - -moz-user-select: text; -} - -.twisty { - position: absolute; - left: 0px; - top: 0px; - width: 14px; - height: 14px; -} - -.nodeChildBox { - margin-left: 12px; - display: none; -} - -.nodeLabel, -.nodeCloseLabel { - margin: -2px 2px 0 2px; - border: 2px solid transparent; - border-radius: 3px; - padding: 0 2px; - color: #000088; -} - -.nodeCloseLabel { - display: none; -} - -.nodeTag { - cursor: pointer; - color: blue; -} - -.nodeValue { - color: #FF0000; - font-weight: normal; -} - -.nodeText, -.nodeComment { - margin: 0 2px; - vertical-align: top; -} - -.nodeText { - color: #333333; -} - -.docType { - position: absolute; - /* position DOCTYPE element above/outside the "nodeBox" that contains it */ - /* Note: to be fixed in Bug #688439 */ - top: -16px; - font-family: Menlo, Andale Mono, monospace; - padding-left: 8px; - color: #999; - white-space: nowrap; - font-style: italic; -} - -.htmlNodeBox { - /* make room for DOCTYPE element to be rendered above/outside "nodeBox" */ - /* Note: to be fixed in Bug #688439 */ - margin-top: 16px; -} - -.nodeWhiteSpace { - border: 1px solid LightGray; - white-space: pre; /* otherwise the border will be collapsed around zero pixels */ - margin-left: 1px; - color: gray; -} - -.nodeWhiteSpace_Space { - border: 1px solid #ddd; -} - -.nodeTextEntity { - border: 1px solid gray; - white-space: pre; /* otherwise the border will be collapsed around zero pixels */ - margin-left: 1px; -} - -.nodeComment { - color: DarkGreen; -} - -.nodeBox.highlightOpen > .nodeLabel { - background-color: #EEEEEE; -} - -.nodeBox.highlightOpen > .nodeCloseLabel, -.nodeBox.highlightOpen > .nodeChildBox, -.nodeBox.open > .nodeCloseLabel, -.nodeBox.open > .nodeChildBox { - display: block; -} - -.nodeBox.selected > .nodeLabel > .nodeLabelBox, -.nodeBox.selected > .nodeLabel { - border-color: Highlight; - background-color: Highlight; - color: HighlightText !important; -} - -.nodeBox.selected > .nodeLabel > .nodeLabelBox, -.nodeBox.selected > .nodeLabel > .nodeLabelBox > .nodeTag, -.nodeBox.selected > .nodeLabel > .nodeLabelBox > .nodeAttr > .nodeValue, -.nodeBox.selected > .nodeLabel > .nodeLabelBox > .nodeText { - color: inherit !important; -} - -.nodeBox.highlighted > .nodeLabel { - border-color: Highlight !important; - background-color: cyan !important; - color: #000000 !important; -} - -.nodeBox.highlighted > .nodeLabel > .nodeLabelBox, -.nodeBox.highlighted > .nodeLabel > .nodeLabelBox > .nodeTag, -.nodeBox.highlighted > .nodeLabel > .nodeLabelBox > .nodeAttr > .nodeValue, -.nodeBox.highlighted > .nodeLabel > .nodeLabelBox > .nodeText { - color: #000000 !important; -} - -.nodeBox.nodeHidden .nodeLabel > .nodeLabelBox, -.nodeBox.nodeHidden .nodeCloseLabel, -.nodeBox.nodeHidden .nodeLabel > .nodeLabelBox > .nodeText, -.nodeBox.nodeHidden .nodeText { - color: #888888; -} - -.nodeBox.nodeHidden .nodeLabel > .nodeLabelBox > .nodeTag, -.nodeBox.nodeHidden .nodeCloseLabel > .nodeCloseLabelBox > .nodeTag { - color: #5F82D9; -} - -.nodeBox.nodeHidden .nodeLabel > .nodeLabelBox > .nodeAttr > .nodeValue { - color: #D86060; -} - -.nodeBox.nodeHidden.selected > .nodeLabel > .nodeLabelBox, -.nodeBox.nodeHidden.selected > .nodeLabel > .nodeLabelBox > .nodeTag, -.nodeBox.nodeHidden.selected > .nodeLabel > .nodeLabelBox > .nodeAttr > .nodeValue, -.nodeBox.nodeHidden.selected > .nodeLabel > .nodeLabelBox > .nodeText { - color: SkyBlue !important; -} - -.nodeBox.mutated > .nodeLabel, -.nodeAttr.mutated, -.nodeValue.mutated, -.nodeText.mutated, -.nodeBox.mutated > .nodeText { - background-color: #EFFF79; - color: #FF0000 !important; -} - -.nodeBox.selected.mutated > .nodeLabel, -.nodeBox.selected.mutated > .nodeLabel > .nodeLabelBox, -.nodeBox.selected > .nodeLabel > .nodeLabelBox > .nodeAttr.mutated > .nodeValue, -.nodeBox.selected > .nodeLabel > .nodeLabelBox > .nodeAttr > .nodeValue.mutated, -.nodeBox.selected > .nodeLabel > .nodeLabelBox > .nodeText.mutated { - background-color: #EFFF79; - border-color: #EFFF79; - color: #FF0000 !important; -} - -.logRow-dirxml { - padding-left: 0; -} - -.soloElement > .nodeBox { - padding-left: 0; -} - -.nodeBox.selected > .nodeLabel > .nodeLabelBox, -.nodeBox.selected > .nodeLabel { - border-color: #3875d7; - background-color: #3875d7; - color: #FFFFFF !important; -} - -.nodeBox.highlighted > .nodeLabel { - border-color: #3875d7 !important; -} - -/************************************************************************************************/ -/* Twisties */ - -.twisty -{ - -moz-appearance: treetwisty; -} - -.nodeBox.highlightOpen > .nodeLabel > .twisty, -.nodeBox.open > .nodeLabel > .twisty -{ - -moz-appearance: treetwistyopen; -} - -.memberRow.hasChildren > .memberLabelCell > .memberLabel, -.hasHeaders .netHrefLabel { - background-position: 2px 2px; -} - -.nodeBox.selected > .nodeLabel > .nodeLabelBox, -.nodeBox.selected > .nodeLabel { - border-color: #3875d7; - background-color: #3875d7; - color: #FFFFFF !important; -} - -.nodeBox.highlighted > .nodeLabel { - border-color: #3875d7 !important; -} - -.editingAttributeValue { - background-color: #492; -} - -#attribute-editor { - visibility: hidden; - position: absolute; - z-index: 5000; - background-color: #fff; - border: 1px solid #000; -} - -#attribute-editor.editing { - visibility: visible; -} - -#attribute-editor-input { - border: none; - padding: 2px 5px; - font-family: Menlo, Andale Mono, monospace; - font-size: 11px; -} diff --git a/browser/themes/pinstripe/jar.mn b/browser/themes/pinstripe/jar.mn index 1fff34f6ee05..94715bc62772 100644 --- a/browser/themes/pinstripe/jar.mn +++ b/browser/themes/pinstripe/jar.mn @@ -173,7 +173,6 @@ browser.jar: skin/classic/browser/devtools/goto-mdn.png (devtools/goto-mdn.png) skin/classic/browser/devtools/csshtmltree.css (devtools/csshtmltree.css) skin/classic/browser/devtools/commandline.css (devtools/commandline.css) - skin/classic/browser/devtools/htmlpanel.css (devtools/htmlpanel.css) skin/classic/browser/devtools/markup-view.css (devtools/markup-view.css) skin/classic/browser/devtools/orion.css (devtools/orion.css) skin/classic/browser/devtools/orion-container.css (devtools/orion-container.css) diff --git a/browser/themes/winstripe/devtools/htmlpanel.css b/browser/themes/winstripe/devtools/htmlpanel.css deleted file mode 100644 index 44927488055c..000000000000 --- a/browser/themes/winstripe/devtools/htmlpanel.css +++ /dev/null @@ -1,377 +0,0 @@ -/* - * Software License Agreement (BSD License) - * - * Copyright (c) 2007, Parakey Inc. - * All rights reserved. - * - * Redistribution and use of this software in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * - * * Redistributions of source code must retain the above - * copyright notice, this list of conditions and the - * following disclaimer. - * - * * Redistributions in binary form must reproduce the above - * copyright notice, this list of conditions and the - * following disclaimer in the documentation and/or other - * materials provided with the distribution. - * - * * Neither the name of Parakey Inc. nor the names of its - * contributors may be used to endorse or promote products - * derived from this software without specific prior - * written permission of Parakey Inc. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR - * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND - * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR - * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL - * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER - * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT - * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -/* - * Creator: - * Joe Hewitt - * Contributors - * John J. Barton (IBM Almaden) - * Jan Odvarko (Mozilla Corp.) - * Max Stepanov (Aptana Inc.) - * Rob Campbell (Mozilla Corp.) - * Hans Hillen (Paciello Group, Mozilla) - * Curtis Bartley (Mozilla Corp.) - * Mike Collins (IBM Almaden) - * Kevin Decker - * Mike Ratcliffe (Comartis AG) - * Hernan Rodríguez Colmeiro - * Austin Andrews - * Christoph Dorn - * Steven Roussey (AppCenter Inc, Network54) - */ - -html { - background-color: -moz-dialog; -} - -body { - margin: 0; - overflow: auto; - font-family: Lucida Grande, sans-serif; - font-size: 11px; - padding-top: 5px; -} - -h1 { - font-size: 17px; - border-bottom: 1px solid threedlightshadow; -} - -a { - color: #0000ff; -} - -pre { - margin: 0; - font: inherit; -} - -code { - display: block; - white-space: pre; -} - -/* DOMPlate */ - -.objectLink-element, -.objectLink-textNode, -.objectLink-function, -.objectBox-stackTrace, -.objectLink-profile { - font-family: Menlo, Andale Mono, monospace; -} - -.objectLink-textNode { - white-space: pre-wrap; -} - -.objectLink-styleRule, -.objectLink-element, -.objectLink-textNode { - color: #000088; -} - -.selectorTag, -.selectorId, -.selectorClass { - font-family: Menlo, Andale Mono, monospace; - font-weight: normal; -} - -.selectorTag { - color: #0000FF; -} - -.selectorId { - color: DarkBlue; -} - -.selectorClass { - color: red; -} - -.selectorHidden > .selectorTag { - color: #5F82D9; -} - -.selectorHidden > .selectorId { - color: #888888; -} - -.selectorHidden > .selectorClass { - color: #D86060; -} - -.selectorValue { - font-family: Menlo, Andale Mono, monospace; - font-style: italic; - color: #555555; -} - -.panelNode-html { - -moz-box-sizing: padding-box; - padding: 4px 0 0 2px; -} - -.nodeBox { - position: relative; - font-family: Menlo, Andale Mono, monospace; - padding-left: 13px; - -moz-user-select: -moz-none; -} - -.nodeBox.search-selection { - -moz-user-select: text; -} - -.twisty { - position: absolute; - left: 0px; - top: 0px; - width: 14px; - height: 14px; -} - -.nodeChildBox { - margin-left: 12px; - display: none; -} - -.nodeLabel, -.nodeCloseLabel { - margin: -2px 2px 0 2px; - border: 2px solid transparent; - border-radius: 3px; - padding: 0 2px; - color: #000088; -} - -.nodeCloseLabel { - display: none; -} - -.nodeTag { - cursor: pointer; - color: blue; -} - -.nodeValue { - color: #FF0000; - font-weight: normal; -} - -.nodeText, -.nodeComment { - margin: 0 2px; - vertical-align: top; -} - -.nodeText { - color: #333333; -} - -.docType { - position: absolute; - /* position DOCTYPE element above/outside the "nodeBox" that contains it */ - /* Note: to be fixed in Bug #688439 */ - top: -16px; - font-family: Menlo, Andale Mono, monospace; - padding-left: 8px; - color: #999; - white-space: nowrap; - font-style: italic; -} - -.htmlNodeBox { - /* make room for DOCTYPE element to be rendered above/outside "nodeBox" */ - /* Note: to be fixed in Bug #688439 */ - margin-top: 16px; -} - -.nodeWhiteSpace { - border: 1px solid LightGray; - white-space: pre; - margin-left: 1px; - color: gray; -} - -.nodeWhiteSpace_Space { - border: 1px solid #ddd; -} - -.nodeTextEntity { - border: 1px solid gray; - white-space: pre; - margin-left: 1px; -} - -.nodeComment { - color: DarkGreen; -} - -.nodeBox.highlightOpen > .nodeLabel { - background-color: #EEEEEE; -} - -.nodeBox.highlightOpen > .nodeCloseLabel, -.nodeBox.highlightOpen > .nodeChildBox, -.nodeBox.open > .nodeCloseLabel, -.nodeBox.open > .nodeChildBox { - display: block; -} - -.nodeBox.selected > .nodeLabel > .nodeLabelBox, -.nodeBox.selected > .nodeLabel { - border-color: Highlight; - background-color: Highlight; - color: HighlightText !important; -} - -.nodeBox.selected > .nodeLabel > .nodeLabelBox, -.nodeBox.selected > .nodeLabel > .nodeLabelBox > .nodeTag, -.nodeBox.selected > .nodeLabel > .nodeLabelBox > .nodeAttr > .nodeValue, -.nodeBox.selected > .nodeLabel > .nodeLabelBox > .nodeText { - color: inherit !important; -} - -.nodeBox.highlighted > .nodeLabel { - border-color: Highlight !important; - background-color: cyan !important; - color: #000000 !important; -} - -.nodeBox.highlighted > .nodeLabel > .nodeLabelBox, -.nodeBox.highlighted > .nodeLabel > .nodeLabelBox > .nodeTag, -.nodeBox.highlighted > .nodeLabel > .nodeLabelBox > .nodeAttr > .nodeValue, -.nodeBox.highlighted > .nodeLabel > .nodeLabelBox > .nodeText { - color: #000000 !important; -} - -.nodeBox.nodeHidden .nodeLabel > .nodeLabelBox, -.nodeBox.nodeHidden .nodeCloseLabel, -.nodeBox.nodeHidden .nodeLabel > .nodeLabelBox > .nodeText, -.nodeBox.nodeHidden .nodeText { - color: #888888; -} - -.nodeBox.nodeHidden .nodeLabel > .nodeLabelBox > .nodeTag, -.nodeBox.nodeHidden .nodeCloseLabel > .nodeCloseLabelBox > .nodeTag { - color: #5F82D9; -} - -.nodeBox.nodeHidden .nodeLabel > .nodeLabelBox > .nodeAttr > .nodeValue { - color: #D86060; -} - -.nodeBox.nodeHidden.selected > .nodeLabel > .nodeLabelBox, -.nodeBox.nodeHidden.selected > .nodeLabel > .nodeLabelBox > .nodeTag, -.nodeBox.nodeHidden.selected > .nodeLabel > .nodeLabelBox > .nodeAttr > .nodeValue, -.nodeBox.nodeHidden.selected > .nodeLabel > .nodeLabelBox > .nodeText { - color: SkyBlue !important; -} - -.nodeBox.mutated > .nodeLabel, -.nodeAttr.mutated, -.nodeValue.mutated, -.nodeText.mutated, -.nodeBox.mutated > .nodeText { - background-color: #EFFF79; - color: #FF0000 !important; -} - -.nodeBox.selected.mutated > .nodeLabel, -.nodeBox.selected.mutated > .nodeLabel > .nodeLabelBox, -.nodeBox.selected > .nodeLabel > .nodeLabelBox > .nodeAttr.mutated > .nodeValue, -.nodeBox.selected > .nodeLabel > .nodeLabelBox > .nodeAttr > .nodeValue.mutated, -.nodeBox.selected > .nodeLabel > .nodeLabelBox > .nodeText.mutated { - background-color: #EFFF79; - border-color: #EFFF79; - color: #FF0000 !important; -} - -.logRow-dirxml { - padding-left: 0; -} - -.soloElement > .nodeBox { - padding-left: 0; -} - -.nodeBox.selected > .nodeLabel > .nodeLabelBox, -.nodeBox.selected > .nodeLabel { - border-color: #3875d7; - background-color: #3875d7; - color: #FFFFFF !important; -} - -.nodeBox.highlighted > .nodeLabel { - border-color: #3875d7 !important; -} - -/* Twisties */ - -.twisty -{ - background-repeat: no-repeat; - background-position: center; - background-image: url("chrome://global/skin/tree/twisty-clsd.png") !important; -} - -.nodeBox.highlightOpen > .nodeLabel > .twisty, -.nodeBox.open > .nodeLabel > .twisty -{ - background-image: url("chrome://global/skin/tree/twisty-open.png") !important; -} - -.editingAttributeValue { - background-color: #492; -} - -#attribute-editor { - visibility: hidden; - position: absolute; - z-index: 5000; - background-color: #fff; - border: 1px solid #000; -} - -#attribute-editor.editing { - visibility: visible; -} - -#attribute-editor-input { - border: none; - padding: 2px 5px; - font-family: Menlo, Andale Mono, monospace; - font-size: 11px; -} diff --git a/browser/themes/winstripe/jar.mn b/browser/themes/winstripe/jar.mn index a82b5cfae819..0d9efa0d8c77 100644 --- a/browser/themes/winstripe/jar.mn +++ b/browser/themes/winstripe/jar.mn @@ -131,7 +131,6 @@ browser.jar: skin/classic/browser/devtools/goto-mdn.png (devtools/goto-mdn.png) skin/classic/browser/devtools/csshtmltree.css (devtools/csshtmltree.css) skin/classic/browser/devtools/commandline.css (devtools/commandline.css) - skin/classic/browser/devtools/htmlpanel.css (devtools/htmlpanel.css) skin/classic/browser/devtools/markup-view.css (devtools/markup-view.css) skin/classic/browser/devtools/orion.css (devtools/orion.css) skin/classic/browser/devtools/orion-container.css (devtools/orion-container.css) @@ -335,7 +334,6 @@ browser.jar: skin/classic/aero/browser/devtools/goto-mdn.png (devtools/goto-mdn.png) skin/classic/aero/browser/devtools/csshtmltree.css (devtools/csshtmltree.css) skin/classic/aero/browser/devtools/commandline.css (devtools/commandline.css) - skin/classic/aero/browser/devtools/htmlpanel.css (devtools/htmlpanel.css) skin/classic/aero/browser/devtools/markup-view.css (devtools/markup-view.css) skin/classic/aero/browser/devtools/orion.css (devtools/orion.css) skin/classic/aero/browser/devtools/orion-container.css (devtools/orion-container.css) From 85b551d51c5e74be815f28cccb5fd2c79804cb30 Mon Sep 17 00:00:00 2001 From: Michael Ratcliffe Date: Thu, 4 Oct 2012 10:32:36 +0100 Subject: [PATCH 02/18] Bug 786803 - GCLI help command panel is too big; r=jwalker --- browser/devtools/shared/DeveloperToolbar.jsm | 75 +++++++++++++++++--- 1 file changed, 65 insertions(+), 10 deletions(-) diff --git a/browser/devtools/shared/DeveloperToolbar.jsm b/browser/devtools/shared/DeveloperToolbar.jsm index 7873890a28ae..53d7756c5026 100644 --- a/browser/devtools/shared/DeveloperToolbar.jsm +++ b/browser/devtools/shared/DeveloperToolbar.jsm @@ -7,6 +7,7 @@ const EXPORTED_SYMBOLS = [ "DeveloperToolbar" ]; const NS_XHTML = "http://www.w3.org/1999/xhtml"; +const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; const WEBCONSOLE_CONTENT_SCRIPT_URL = "chrome://browser/content/devtools/HUDService-content.js"; @@ -638,7 +639,6 @@ function OutputPanel(aChromeDoc, aInput, aLoadCallback) this._frame = aChromeDoc.createElementNS(NS_XHTML, "iframe"); this._frame.id = "gcli-output-frame"; this._frame.setAttribute("src", "chrome://browser/content/devtools/commandlineoutput.xhtml"); - this._frame.setAttribute("flex", "1"); this._panel.appendChild(this._frame); this.displayedOutput = undefined; @@ -674,6 +674,33 @@ OutputPanel.prototype._onload = function OP_onload() } }; +/** + * Determine the scrollbar width in the current document. + * + * @private + */ +Object.defineProperty(OutputPanel.prototype, 'scrollbarWidth', { + get: function() { + if (this.__scrollbarWidth) { + return this.__scrollbarWidth; + } + + let hbox = this.document.createElementNS(XUL_NS, "hbox"); + hbox.setAttribute("style", "height: 0%; overflow: hidden"); + + let scrollbar = this.document.createElementNS(XUL_NS, "scrollbar"); + scrollbar.setAttribute("orient", "vertical"); + hbox.appendChild(scrollbar); + + this.document.documentElement.appendChild(hbox); + this.__scrollbarWidth = scrollbar.clientWidth; + this.document.documentElement.removeChild(hbox); + + return this.__scrollbarWidth; + }, + enumerable: true +}; + /** * Prevent the popup from hiding if it is not permitted via this.canHide. */ @@ -691,17 +718,15 @@ OutputPanel.prototype._onpopuphiding = function OP_onpopuphiding(aEvent) */ OutputPanel.prototype.show = function OP_show() { - // This is nasty, but displaying the panel causes it to re-flow, which can - // change the size it should be, so we need to resize the iframe after the - // panel has displayed - this._panel.ownerDocument.defaultView.setTimeout(function() { - this._resize(); - }.bind(this), 0); - if (isLinux) { this.canHide = false; } + // We need to reset the iframe size in order for future size calculations to + // be correct + this._frame.style.minHeight = this._frame.style.maxHeight = 0; + this._frame.style.minWidth = 0; + this._panel.openPopup(this._input, "before_start", 0, 0, false, false, null); this._resize(); @@ -718,8 +743,38 @@ OutputPanel.prototype._resize = function CLP_resize() return } - this._frame.height = this.document.body.scrollHeight; - this._frame.width = this._input.clientWidth + 2; + // Set max panel width to match any content with a max of the width of the + // browser window. + let maxWidth = this._panel.ownerDocument.documentElement.clientWidth; + let width = Math.min(maxWidth, this.document.documentElement.scrollWidth); + + // Add scrollbar width to content size in case a scrollbar is needed. + width += this.scrollbarWidth; + + // Set the width of the iframe. + this._frame.style.minWidth = width + "px"; + + // browserAdjustment is used to correct the panel height according to the + // browsers borders etc. + const browserAdjustment = 15; + + // Set max panel height to match any content with a max of the height of the + // browser window. + let maxHeight = + this._panel.ownerDocument.documentElement.clientHeight - browserAdjustment; + let height = Math.min(maxHeight, this.document.documentElement.scrollHeight); + + // Set the height of the iframe. Setting iframe.height does not work. + this._frame.style.minHeight = this._frame.style.maxHeight = height + "px"; + + // Set the height and width of the panel to match the iframe. + this._panel.sizeTo(width, height); + + // Move the panel to the correct position in the case that it has been + // positioned incorrectly. + let screenX = this._input.boxObject.screenX; + let screenY = this._toolbar.boxObject.screenY; + this._panel.moveTo(screenX, screenY - height); }; /** From eaaa95f78280b2f709af43df5b8c8c96e25f603e Mon Sep 17 00:00:00 2001 From: Michael Ratcliffe Date: Wed, 3 Oct 2012 19:10:07 +0100 Subject: [PATCH 03/18] Bug 785134 - Update jsb to fix various bugs; r=jwalker --- browser/devtools/commandline/CmdJsb.jsm | 139 +- browser/devtools/commandline/commandline.css | 3 + .../commandline/test/browser_cmd_jsb.js | 78 +- .../test/browser_cmd_jsb_script.jsi | 3 +- browser/devtools/shared/Jsbeautify.jsm | 2432 ++++++++--------- .../browser/devtools/gclicommands.properties | 32 +- .../gnomestripe/devtools/commandline.css | 7 +- .../themes/pinstripe/devtools/commandline.css | 7 +- .../themes/winstripe/devtools/commandline.css | 7 +- 9 files changed, 1365 insertions(+), 1343 deletions(-) diff --git a/browser/devtools/commandline/CmdJsb.jsm b/browser/devtools/commandline/CmdJsb.jsm index 08bd7b107766..f19c32f86edb 100644 --- a/browser/devtools/commandline/CmdJsb.jsm +++ b/browser/devtools/commandline/CmdJsb.jsm @@ -28,79 +28,82 @@ gcli.addCommand({ description: gcli.lookup('jsbUrlDesc') }, { - name: 'indentSize', - type: 'number', - description: gcli.lookup('jsbIndentSizeDesc'), - manual: gcli.lookup('jsbIndentSizeManual'), - defaultValue: 2 - }, - { - name: 'indentChar', - type: { - name: 'selection', - lookup: [ - { name: "space", value: " " }, - { name: "tab", value: "\t" } - ] - }, - description: gcli.lookup('jsbIndentCharDesc'), - manual: gcli.lookup('jsbIndentCharManual'), - defaultValue: ' ', - }, - { - name: 'preserveNewlines', - type: 'boolean', - description: gcli.lookup('jsbPreserveNewlinesDesc'), - manual: gcli.lookup('jsbPreserveNewlinesManual') - }, - { - name: 'preserveMaxNewlines', - type: 'number', - description: gcli.lookup('jsbPreserveMaxNewlinesDesc'), - manual: gcli.lookup('jsbPreserveMaxNewlinesManual'), - defaultValue: -1 - }, - { - name: 'jslintHappy', - type: 'boolean', - description: gcli.lookup('jsbJslintHappyDesc'), - manual: gcli.lookup('jsbJslintHappyManual') - }, - { - name: 'braceStyle', - type: { - name: 'selection', - data: ['collapse', 'expand', 'end-expand', 'expand-strict'] - }, - description: gcli.lookup('jsbBraceStyleDesc'), - manual: gcli.lookup('jsbBraceStyleManual'), - defaultValue: "collapse" - }, - { - name: 'spaceBeforeConditional', - type: 'boolean', - description: gcli.lookup('jsbSpaceBeforeConditionalDesc'), - manual: gcli.lookup('jsbSpaceBeforeConditionalManual') - }, - { - name: 'unescapeStrings', - type: 'boolean', - description: gcli.lookup('jsbUnescapeStringsDesc'), - manual: gcli.lookup('jsbUnescapeStringsManual') + group: gcli.lookup("jsbOptionsDesc"), + params: [ + { + name: 'indentSize', + type: 'number', + description: gcli.lookup('jsbIndentSizeDesc'), + manual: gcli.lookup('jsbIndentSizeManual'), + defaultValue: 2 + }, + { + name: 'indentChar', + type: { + name: 'selection', + lookup: [ + { name: "space", value: " " }, + { name: "tab", value: "\t" } + ] + }, + description: gcli.lookup('jsbIndentCharDesc'), + manual: gcli.lookup('jsbIndentCharManual'), + defaultValue: ' ', + }, + { + name: 'doNotPreserveNewlines', + type: 'boolean', + description: gcli.lookup('jsbDoNotPreserveNewlinesDesc') + }, + { + name: 'preserveMaxNewlines', + type: 'number', + description: gcli.lookup('jsbPreserveMaxNewlinesDesc'), + manual: gcli.lookup('jsbPreserveMaxNewlinesManual'), + defaultValue: -1 + }, + { + name: 'jslintHappy', + type: 'boolean', + description: gcli.lookup('jsbJslintHappyDesc'), + manual: gcli.lookup('jsbJslintHappyManual') + }, + { + name: 'braceStyle', + type: { + name: 'selection', + data: ['collapse', 'expand', 'end-expand', 'expand-strict'] + }, + description: gcli.lookup('jsbBraceStyleDesc'), + manual: gcli.lookup('jsbBraceStyleManual'), + defaultValue: "collapse" + }, + { + name: 'noSpaceBeforeConditional', + type: 'boolean', + description: gcli.lookup('jsbNoSpaceBeforeConditionalDesc') + }, + { + name: 'unescapeStrings', + type: 'boolean', + description: gcli.lookup('jsbUnescapeStringsDesc'), + manual: gcli.lookup('jsbUnescapeStringsManual') + } + ] } ], exec: function(args, context) { let opts = { indent_size: args.indentSize, indent_char: args.indentChar, - preserve_newlines: args.preserveNewlines, + preserve_newlines: !args.doNotPreserveNewlines, max_preserve_newlines: args.preserveMaxNewlines == -1 ? undefined : args.preserveMaxNewlines, jslint_happy: args.jslintHappy, brace_style: args.braceStyle, - space_before_conditional: args.spaceBeforeConditional, + space_before_conditional: !args.noSpaceBeforeConditional, unescape_strings: args.unescapeStrings - } + }; let xhr = new XMLHttpRequest(); @@ -117,13 +120,13 @@ gcli.addCommand({ if (xhr.status == 200 || xhr.status == 0) { let browserDoc = context.environment.chromeDocument; let browserWindow = browserDoc.defaultView; - let browser = browserWindow.gBrowser; - - browser.selectedTab = browser.addTab("data:text/plain;base64," + - browserWindow.btoa(js_beautify(xhr.responseText, opts))); + let gBrowser = browserWindow.gBrowser; + let result = js_beautify(xhr.responseText, opts); + + browserWindow.Scratchpad.ScratchpadManager.openScratchpad({text: result}); + promise.resolve(); - } - else { + } else { promise.resolve("Unable to load page to beautify: " + args.url + " " + xhr.status + " " + xhr.statusText); } diff --git a/browser/devtools/commandline/commandline.css b/browser/devtools/commandline/commandline.css index a088c5bbfd8f..93ca1854e58d 100644 --- a/browser/devtools/commandline/commandline.css +++ b/browser/devtools/commandline/commandline.css @@ -34,3 +34,6 @@ direction: ltr; } +.gcli-row-out .nowrap { + white-space: nowrap; +} diff --git a/browser/devtools/commandline/test/browser_cmd_jsb.js b/browser/devtools/commandline/test/browser_cmd_jsb.js index f1d4183fca9d..301ee5fc848d 100644 --- a/browser/devtools/commandline/test/browser_cmd_jsb.js +++ b/browser/devtools/commandline/test/browser_cmd_jsb.js @@ -6,52 +6,68 @@ const TEST_URI = "http://example.com/browser/browser/devtools/commandline/" + "test/browser_cmd_jsb_script.jsi"; +let scratchpadWin = null; +let Scratchpad = null; + function test() { - DeveloperToolbarTest.test("about:blank", [ /*GJT_test*/ ]); + DeveloperToolbarTest.test("about:blank", [ GJT_test ]); } function GJT_test() { helpers.setInput('jsb'); helpers.check({ input: 'jsb', - hints: ' [indentSize] [indentChar] [preserveNewlines] [preserveMaxNewlines] [jslintHappy] [braceStyle] [spaceBeforeConditional] [unescapeStrings]', + hints: ' [options]', markup: 'VVV', status: 'ERROR' }); + DeveloperToolbarTest.exec({ completed: false }); - gBrowser.addTabsProgressListener({ - onProgressChange: DeveloperToolbarTest.checkCalled(function GJT_onProgressChange(aBrowser) { - gBrowser.removeTabsProgressListener(this); + Services.ww.registerNotification(function(aSubject, aTopic, aData) { + if (aTopic == "domwindowopened") { + Services.ww.unregisterNotification(arguments.callee); - let win = aBrowser._contentWindow; - let uri = win.document.location.href; - let result = win.atob(uri.replace(/.*,/, "")); + scratchpadWin = aSubject.QueryInterface(Ci.nsIDOMWindow); + scratchpadWin.addEventListener("load", function GDT_onLoad() { + scratchpadWin.removeEventListener("load", GDT_onLoad, false); + Scratchpad = scratchpadWin.Scratchpad; - result = result.replace(/[\r\n]]/g, "\n"); + let observer = { + onReady: function GJT_onReady() { + Scratchpad.removeObserver(observer); - let correct = "function somefunc() {\n" + - " for (let n = 0; n < 500; n++) {\n" + - " if (n % 2 == 1) {\n" + - " console.log(n);\n" + - " console.log(n + 1);\n" + - " }\n" + - " }\n" + - "}"; - is(result, correct, "JS has been correctly prettified"); - }) - }); + let result = Scratchpad.getText(); + result = result.replace(/[\r\n]]*/g, "\n"); + let correct = "function somefunc() {\n" + + " if (true) // Some comment\n" + + " doSomething();\n" + + " for (let n = 0; n < 500; n++) {\n" + + " if (n % 2 == 1) {\n" + + " console.log(n);\n" + + " console.log(n + 1);\n" + + " }\n" + + " }\n" + + "}"; + is(result, correct, "JS has been correctly prettified"); + + finishUp(); + }, + }; + Scratchpad.addObserver(observer); + }, false); + } + }); info("Checking beautification"); - - helpers.setInput('jsb ' + TEST_URI); - /* - helpers.check({ - input: 'jsb', - hints: ' [options]', - markup: 'VVV', - status: 'VALID' + DeveloperToolbarTest.exec({ + typed: "jsb " + TEST_URI, + completed: false }); - */ - - DeveloperToolbarTest.exec({ completed: false }); } + +let finishUp = DeveloperToolbarTest.checkCalled(function GJT_finishUp() { + if (scratchpadWin) { + scratchpadWin.close(); + scratchpadWin = null; + } +}); diff --git a/browser/devtools/commandline/test/browser_cmd_jsb_script.jsi b/browser/devtools/commandline/test/browser_cmd_jsb_script.jsi index 81085f3147e8..dcaac807c76a 100644 --- a/browser/devtools/commandline/test/browser_cmd_jsb_script.jsi +++ b/browser/devtools/commandline/test/browser_cmd_jsb_script.jsi @@ -1 +1,2 @@ -function somefunc(){for(let n=0;n<500;n++){if(n%2==1){console.log(n);console.log(n+1);}}} +function somefunc(){if (true) // Some comment +doSomething();for(let n=0;n<500;n++){if(n%2==1){console.log(n);console.log(n+1);}}} diff --git a/browser/devtools/shared/Jsbeautify.jsm b/browser/devtools/shared/Jsbeautify.jsm index 10a1f0889158..325cc36e0edc 100644 --- a/browser/devtools/shared/Jsbeautify.jsm +++ b/browser/devtools/shared/Jsbeautify.jsm @@ -1,1317 +1,1303 @@ -/* Any copyright is dedicated to the Public Domain. -* http://creativecommons.org/publicdomain/zero/1.0/ */ - +/*jslint onevar: false, plusplus: false */ +/*jshint curly:true, eqeqeq:true, laxbreak:true, noempty:false */ /* -JS Beautifier written by Einar Lielmanis, - http://jsbeautifier.org/ -Originally converted to javascript by Vital, -"End braces on own line" added by Chris J. Shull, + JS Beautifier +--------------- -You are free to use this in any way you want, in case you find this useful or -working for you. -Usage: -js_beautify(js_source_text); -js_beautify(js_source_text, options); + Written by Einar Lielmanis, + http://jsbeautifier.org/ -The options are: -indent_size (default 4) — indentation size. -indent_char (default space) — character to indent with. -preserve_newlines (default true) — whether existing line breaks should be - preserved. -max_preserve_newlines (default unlimited) - maximum number of line breaks to be - preserved in one chunk. -jslint_happy (default false) — if true, then jslint-stricter mode is - enforced. + Originally converted to javascript by Vital, + "End braces on own line" added by Chris J. Shull, - jslint_happy !jslint_happy - --------------------------------- - function () function() + You are free to use this in any way you want, in case you find this useful or working for you. -brace_style (default "collapse") - "collapse" | "expand" | "end-expand" | - "expand-strict" - put braces on the same line as control statements (default), or put braces - on own line (Allman / ANSI style), or just put end braces on own line. + Usage: + js_beautify(js_source_text); + js_beautify(js_source_text, options); - expand-strict: put brace on own line even in such cases: + The options are: + indent_size (default 4) - indentation size, + indent_char (default space) - character to indent with, + preserve_newlines (default true) - whether existing line breaks should be preserved, + max_preserve_newlines (default unlimited) - maximum number of line breaks to be preserved in one chunk, - var a = - { - a: 5, - b: 6 - } - This mode may break your scripts - e.g "return { a: 1 }" will be broken into - two lines, so beware. + jslint_happy (default false) - if true, then jslint-stricter mode is enforced. -space_before_conditional (default true) - should the space before conditional -statement be added, "if(true)" vs "if (true)", + jslint_happy !jslint_happy + --------------------------------- + function () function() -unescape_strings (default false) - should printable characters in -strings encoded in \xNN notation be unescaped, "example" vs -"\x65\x78\x61\x6d\x70\x6c\x65" + brace_style (default "collapse") - "collapse" | "expand" | "end-expand" | "expand-strict" + put braces on the same line as control statements (default), or put braces on own line (Allman / ANSI style), or just put end braces on own line. + + expand-strict: put brace on own line even in such cases: + + var a = + { + a: 5, + b: 6 + } + This mode may break your scripts - e.g "return { a: 1 }" will be broken into two lines, so beware. + + space_before_conditional (default true) - should the space before conditional statement be added, "if(true)" vs "if (true)", + + unescape_strings (default false) - should printable characters in strings encoded in \xNN notation be unescaped, "example" vs "\x65\x78\x61\x6d\x70\x6c\x65" + + e.g + + js_beautify(js_source_text, { + 'indent_size': 1, + 'indent_char': '\t' + }); -e.g -js_beautify(js_source_text, { - 'indent_size': 1, - 'indent_char': '\t' -}); */ -"use strict"; +let EXPORTED_SYMBOLS = ["js_beautify"]; -var EXPORTED_SYMBOLS = [ "js_beautify" ]; +function js_beautify(js_source_text, options) { -function js_beautify(js_source_text, options={}) { - let input, output, token_text, last_type, last_text, last_last_text, - last_word, flags, flag_store, indent_string, whitespace, wordchar, - punct, parser_pos, line_starters, digits, prefix, token_type, - do_block_just_closed, wanted_newline, just_added_newline, n_newlines, - opt_brace_style, preindent_string = ''; - - // compatibility - if (options.space_after_anon_function !== undefined && - options.jslint_happy === undefined) { - options.jslint_happy = options.space_after_anon_function; - } - if (options.braces_on_own_line !== undefined) { - opt_brace_style = options.braces_on_own_line ? "expand" : "collapse"; - } - opt_brace_style = options.brace_style ? options.brace_style : - (opt_brace_style ? opt_brace_style : "collapse"); + var input, output, token_text, last_type, last_text, last_last_text, last_word, flags, flag_store, indent_string; + var whitespace, wordchar, punct, parser_pos, line_starters, digits; + var prefix, token_type, do_block_just_closed; + var wanted_newline, just_added_newline, n_newlines; + var preindent_string = ''; - let opt_indent_size = options.indent_size ? options.indent_size : 4; - let opt_indent_char = options.indent_char ? options.indent_char : ' '; - let opt_preserve_newlines = typeof options.preserve_newlines === 'undefined' ? - true : options.preserve_newlines; - let opt_max_preserve_newlines = - typeof options.max_preserve_newlines === 'undefined' ? - false : options.max_preserve_newlines; - let opt_jslint_happy = options.jslint_happy === 'undefined' ? - false : options.jslint_happy; - let opt_keep_array_indentation = - typeof options.keep_array_indentation === 'undefined' ? - false : options.keep_array_indentation; - let opt_space_before_conditional = - typeof options.space_before_conditional === 'undefined' ? - true : options.space_before_conditional; - let opt_indent_case = typeof options.indent_case === 'undefined' ? - false : options.indent_case; - let opt_unescape_strings = typeof options.unescape_strings === 'undefined' ? - false : options.unescape_strings; + // Some interpreters have unexpected results with foo = baz || bar; + options = options ? options : {}; - just_added_newline = false; + var opt_brace_style; - // cache the source's length. - let input_length = js_source_text.length; - - function trim_output(eat_newlines) { - eat_newlines = typeof eat_newlines === 'undefined' ? false : eat_newlines; - while (output.length && (output[output.length - 1] === ' ' || - output[output.length - 1] === indent_string || - output[output.length - 1] === preindent_string || - (eat_newlines && (output[output.length - 1] === '\n' || - output[output.length - 1] === '\r')))) { - output.pop(); + // compatibility + if (options.space_after_anon_function !== undefined && options.jslint_happy === undefined) { + options.jslint_happy = options.space_after_anon_function; } - } - - function trim(s) { - return s.replace(/^\s\s*|\s\s*$/, ''); - } - - function split_newlines(s) - { - return s.split(/\x0d\x0a|\x0a/); - } - - function force_newline() - { - let old_keep_array_indentation = opt_keep_array_indentation; - opt_keep_array_indentation = false; - print_newline(); - opt_keep_array_indentation = old_keep_array_indentation; - } - - function print_newline(ignore_repeated) { - - flags.eat_next_space = false; - if (opt_keep_array_indentation && is_array(flags.mode)) { - return; + if (options.braces_on_own_line !== undefined) { //graceful handling of deprecated option + opt_brace_style = options.braces_on_own_line ? "expand" : "collapse"; } - - ignore_repeated = typeof ignore_repeated === 'undefined' ? true : ignore_repeated; - - flags.if_line = false; - trim_output(); - - if (!output.length) { - return; // no newline on start of file - } - - if (output[output.length - 1] !== "\n" || !ignore_repeated) { - just_added_newline = true; - output.push("\n"); - } - if (preindent_string) { - output.push(preindent_string); - } - for (let i = 0; i < flags.indentation_level; i += 1) { - output.push(indent_string); - } - if (flags.var_line && flags.var_line_reindented) { - output.push(indent_string); // skip space-stuffing, if indenting with a tab - } - if (flags.case_body) { - output.push(indent_string); - } - } - - function print_single_space() { - - if (last_type === 'TK_COMMENT') { - return print_newline(); - } - if (flags.eat_next_space) { - flags.eat_next_space = false; - return; - } - let last_output = ' '; - if (output.length) { - last_output = output[output.length - 1]; - } - if (last_output !== ' ' && last_output !== '\n' && - last_output !== indent_string) { // prevent occassional duplicate space - output.push(' '); - } - } + opt_brace_style = options.brace_style ? options.brace_style : (opt_brace_style ? opt_brace_style : "collapse"); - function print_token() { + var opt_indent_size = options.indent_size ? options.indent_size : 4; + var opt_indent_char = options.indent_char ? options.indent_char : ' '; + var opt_preserve_newlines = typeof options.preserve_newlines === 'undefined' ? true : options.preserve_newlines; + var opt_max_preserve_newlines = typeof options.max_preserve_newlines === 'undefined' ? false : options.max_preserve_newlines; + var opt_jslint_happy = options.jslint_happy === 'undefined' ? false : options.jslint_happy; + var opt_keep_array_indentation = typeof options.keep_array_indentation === 'undefined' ? false : options.keep_array_indentation; + var opt_space_before_conditional = typeof options.space_before_conditional === 'undefined' ? true : options.space_before_conditional; + var opt_indent_case = typeof options.indent_case === 'undefined' ? false : options.indent_case; + var opt_unescape_strings = typeof options.unescape_strings === 'undefined' ? false : options.unescape_strings; + just_added_newline = false; - flags.eat_next_space = false; - output.push(token_text); - } - function indent() { - flags.indentation_level += 1; - } + // cache the source's length. + var input_length = js_source_text.length; - - function remove_indent() { - if (output.length && output[output.length - 1] === indent_string) { - output.pop(); - } - } - - function set_mode(mode) { - if (flags) { - flag_store.push(flags); - } - flags = { - previous_mode: flags ? flags.mode : 'BLOCK', - mode: mode, - var_line: false, - var_line_tainted: false, - var_line_reindented: false, - in_html_comment: false, - if_line: false, - in_case_statement: false, // switch(..){ INSIDE HERE } - in_case: false, // we're on the exact line with "case 0:" - case_body: false, // the indented case-action block - eat_next_space: false, - indentation_baseline: -1, - indentation_level: (flags ? flags.indentation_level + - (flags.case_body ? 1 : 0) + - ((flags.var_line && flags.var_line_reindented) ? 1 : 0) : 0), - ternary_depth: 0 - }; - } - - function is_array(mode) { - return mode === '[EXPRESSION]' || mode === '[INDENTED-EXPRESSION]'; - } - - function is_expression(mode) { - return in_array(mode, ['[EXPRESSION]', '(EXPRESSION)', - '(FOR-EXPRESSION)', '(COND-EXPRESSION)']); - } - - function restore_mode() { - do_block_just_closed = flags.mode === 'DO_BLOCK'; - if (flag_store.length > 0) { - let mode = flags.mode; - flags = flag_store.pop(); - flags.previous_mode = mode; - } - } - - function all_lines_start_with(lines, c) { - for (let line of lines) { - line = trim(line); - if (line.charAt(0) !== c) { - return false; - } - } - return true; - } - - function is_special_word(word) { - return in_array(word, ['case', 'return', 'do', 'if', 'throw', 'else']); - } - - function in_array(what, arr) { - return arr.indexOf(what) != -1; - } - - function look_up(exclude) { - let local_pos = parser_pos; - let c = input.charAt(local_pos); - while (in_array(c, whitespace) && c != exclude) { - local_pos++; - if (local_pos >= input_length) return 0; - c = input.charAt(local_pos); - } - return c; - } - - function get_next_token() { - n_newlines = 0; - - if (parser_pos >= input_length) { - return ['', 'TK_EOF']; + function trim_output(eat_newlines) { + eat_newlines = typeof eat_newlines === 'undefined' ? false : eat_newlines; + while (output.length && (output[output.length - 1] === ' ' + || output[output.length - 1] === indent_string + || output[output.length - 1] === preindent_string + || (eat_newlines && (output[output.length - 1] === '\n' || output[output.length - 1] === '\r')))) { + output.pop(); + } } - wanted_newline = false; + function trim(s) { + return s.replace(/^\s\s*|\s\s*$/, ''); + } - let c = input.charAt(parser_pos); - parser_pos += 1; + // we could use just string.split, but + // IE doesn't like returning empty strings + function split_newlines(s) { + return s.split(/\x0d\x0a|\x0a/); + } - let keep_whitespace = opt_keep_array_indentation && is_array(flags.mode); + function force_newline() { + var old_keep_array_indentation = opt_keep_array_indentation; + opt_keep_array_indentation = false; + print_newline(); + opt_keep_array_indentation = old_keep_array_indentation; + } - if (keep_whitespace) { + function print_newline(ignore_repeated) { - // - // slight mess to allow nice preservation of array indentation and - // reindent that correctly first time when we get to the arrays: - // let a = [ - // ....'something' - // we make note of whitespace_count = 4 into flags.indentation_baseline so - // we know that 4 whitespaces in original source match indent_level of - // reindented source and afterwards, when we get to - // 'something, - // .......'something else' - // we know that this should be indented to indent_level + - // (7 - indentation_baseline) spaces - // - let whitespace_count = 0; - - while (in_array(c, whitespace)) { - if (c === "\n") { - trim_output(); - output.push("\n"); - just_added_newline = true; - whitespace_count = 0; - } else { - if (c === '\t') { - whitespace_count += 4; - } else if (c === '\r') { - // nothing - } else { - whitespace_count += 1; - } + flags.eat_next_space = false; + if (opt_keep_array_indentation && is_array(flags.mode)) { + return; } - if (parser_pos >= input_length) { - return ['', 'TK_EOF']; + ignore_repeated = typeof ignore_repeated === 'undefined' ? true : ignore_repeated; + + flags.if_line = false; + trim_output(); + + if (!output.length) { + return; // no newline on start of file } - c = input.charAt(parser_pos); - parser_pos += 1; - - } - if (flags.indentation_baseline === -1) { - flags.indentation_baseline = whitespace_count; - } - - if (just_added_newline) { - for (let i = 0; i < flags.indentation_level + 1; i += 1) { - output.push(indent_string); - } - if (flags.indentation_baseline !== -1) { - for (let i = 0; i < whitespace_count - flags.indentation_baseline; i++) { - output.push(' '); - } - } - } - } else { - while (in_array(c, whitespace)) { - - if (c === "\n") { - n_newlines += ( (opt_max_preserve_newlines) ? - (n_newlines <= opt_max_preserve_newlines) ? 1: 0: 1 ); - } - - - if (parser_pos >= input_length) { - return ['', 'TK_EOF']; - } - - c = input.charAt(parser_pos); - parser_pos += 1; - } - - if (opt_preserve_newlines) { - if (n_newlines > 1) { - for (i = 0; i < n_newlines; i += 1) { - print_newline(i === 0); + if (output[output.length - 1] !== "\n" || !ignore_repeated) { just_added_newline = true; - } + output.push("\n"); } - } - wanted_newline = n_newlines > 0; - } - - if (in_array(c, wordchar)) { - if (parser_pos < input_length) { - while (in_array(input.charAt(parser_pos), wordchar)) { - c += input.charAt(parser_pos); - parser_pos += 1; - if (parser_pos === input_length) { - break; - } + if (preindent_string) { + output.push(preindent_string); + } + for (var i = 0; i < flags.indentation_level; i += 1) { + output.push(indent_string); + } + if (flags.var_line && flags.var_line_reindented) { + output.push(indent_string); // skip space-stuffing, if indenting with a tab + } + if (flags.case_body) { + output.push(indent_string); } - } - - // small and surprisingly unugly hack for 1E-10 representation - if (parser_pos !== input_length && c.match(/^[0-9]+[Ee]$/) && - (input.charAt(parser_pos) === '-' || - input.charAt(parser_pos) === '+')) { - let sign = input.charAt(parser_pos); - parser_pos += 1; - - let t = get_next_token(parser_pos); - c += sign + t[0]; - return [c, 'TK_WORD']; - } - - if (c === 'in') { // hack for 'in' operator - return [c, 'TK_OPERATOR']; - } - if (wanted_newline && last_type !== 'TK_OPERATOR' - && last_type !== 'TK_EQUALS' - && !flags.if_line && (opt_preserve_newlines || last_text !== 'var')) { - print_newline(); - } - return [c, 'TK_WORD']; } - if (c === '(' || c === '[') { - return [c, 'TK_START_EXPR']; + + + function print_single_space() { + + if (last_type === 'TK_COMMENT') { + return print_newline(); + } + if (flags.eat_next_space) { + flags.eat_next_space = false; + return; + } + var last_output = ' '; + if (output.length) { + last_output = output[output.length - 1]; + } + if (last_output !== ' ' && last_output !== '\n' && last_output !== indent_string) { // prevent occassional duplicate space + output.push(' '); + } } - if (c === ')' || c === ']') { - return [c, 'TK_END_EXPR']; + + function print_token() { + just_added_newline = false; + flags.eat_next_space = false; + output.push(token_text); } - if (c === '{') { - return [c, 'TK_START_BLOCK']; + function indent() { + flags.indentation_level += 1; } - if (c === '}') { - return [c, 'TK_END_BLOCK']; + + function remove_indent() { + if (output.length && output[output.length - 1] === indent_string) { + output.pop(); + } } - if (c === ';') { - return [c, 'TK_SEMICOLON']; + function set_mode(mode) { + if (flags) { + flag_store.push(flags); + } + flags = { + previous_mode: flags ? flags.mode : 'BLOCK', + mode: mode, + var_line: false, + var_line_tainted: false, + var_line_reindented: false, + in_html_comment: false, + if_line: false, + in_case_statement: false, // switch(..){ INSIDE HERE } + in_case: false, // we're on the exact line with "case 0:" + case_body: false, // the indented case-action block + eat_next_space: false, + indentation_baseline: -1, + indentation_level: (flags ? flags.indentation_level + (flags.case_body ? 1 : 0) + ((flags.var_line && flags.var_line_reindented) ? 1 : 0) : 0), + ternary_depth: 0 + }; } - if (c === '/') { - let comment = ''; - // peek for comment /* ... */ - let inline_comment = true; - if (input.charAt(parser_pos) === '*') { - parser_pos += 1; - if (parser_pos < input_length) { - while (parser_pos < input_length && ! (input.charAt(parser_pos) === '*' && - input.charAt(parser_pos + 1) && input.charAt(parser_pos + 1) === '/')) { + function is_array(mode) { + return mode === '[EXPRESSION]' || mode === '[INDENTED-EXPRESSION]'; + } - c = input.charAt(parser_pos); - comment += c; - if (c === "\n" || c === "\r") { - inline_comment = false; + function is_expression(mode) { + return in_array(mode, ['[EXPRESSION]', '(EXPRESSION)', '(FOR-EXPRESSION)', '(COND-EXPRESSION)']); + } + + function restore_mode() { + do_block_just_closed = flags.mode === 'DO_BLOCK'; + if (flag_store.length > 0) { + var mode = flags.mode; + flags = flag_store.pop(); + flags.previous_mode = mode; + } + } + + function all_lines_start_with(lines, c) { + for (var i = 0; i < lines.length; i++) { + var line = trim(lines[i]); + if (line.charAt(0) !== c) { + return false; } - parser_pos += 1; - if (parser_pos >= input_length) { - break; - } - } } - parser_pos += 2; - if (inline_comment && n_newlines == 0) { - return ['/*' + comment + '*/', 'TK_INLINE_COMMENT']; - } else { - return ['/*' + comment + '*/', 'TK_BLOCK_COMMENT']; - } - } - // peek for comment // ... - if (input.charAt(parser_pos) === '/') { - comment = c; - while (input.charAt(parser_pos) !== '\r' && - input.charAt(parser_pos) !== '\n') { - comment += input.charAt(parser_pos); - parser_pos += 1; - if (parser_pos >= input_length) { - break; - } - } - if (wanted_newline) { - print_newline(); - } - return [comment, 'TK_COMMENT']; - } - + return true; } - if (c === "'" || // string - c === '"' || // string - (c === '/' && - ((last_type === 'TK_WORD' && is_special_word(last_text)) || - (last_text === ')' && in_array(flags.previous_mode, - ['(COND-EXPRESSION)', '(FOR-EXPRESSION)'])) || - (last_type === 'TK_COMMENT' || last_type === 'TK_START_EXPR' || - last_type === 'TK_START_BLOCK' || last_type === 'TK_END_BLOCK' || - last_type === 'TK_OPERATOR' || last_type === 'TK_EQUALS' || - last_type === 'TK_EOF' || last_type === 'TK_SEMICOLON')))) { // regexp - - let sep = c; - let esc = false; - let esc1 = 0; - let esc2 = 0; - let resulting_string = c; - - if (parser_pos < input_length) { - if (sep === '/') { - // - // handle regexp separately... - // - let in_char_class = false; - while (esc || in_char_class || input.charAt(parser_pos) !== sep) { - resulting_string += input.charAt(parser_pos); - if (!esc) { - esc = input.charAt(parser_pos) === '\\'; - if (input.charAt(parser_pos) === '[') { - in_char_class = true; - } else if (input.charAt(parser_pos) === ']') { - in_char_class = false; - } - } else { - esc = false; - } - parser_pos += 1; - if (parser_pos >= input_length) { - // incomplete string/rexp when end-of-file reached. - // bail out with what had been received so far. - return [resulting_string, 'TK_STRING']; - } - } - - } else { - // - // and handle string also separately - // - while (esc || input.charAt(parser_pos) !== sep) { - resulting_string += input.charAt(parser_pos); - if (esc1 && esc1 >= esc2) { - esc1 = parseInt(resulting_string.substr(-esc2), 16); - if (esc1 && esc1 >= 0x20 && esc1 <= 0x7e) { - esc1 = String.fromCharCode(esc1); - resulting_string = - resulting_string.substr(0, resulting_string.length - esc2 - 2) + - (((esc1 === sep) || (esc1 === '\\')) ? '\\' : '') + esc1; - } - esc1 = 0; - } - if (esc1) { - esc1++; - } else if (!esc) { - esc = input.charAt(parser_pos) === '\\'; - } else { - esc = false; - if (opt_unescape_strings) { - if (input.charAt(parser_pos) === 'x') { - esc1++; - esc2 = 2; - } else if (input.charAt(parser_pos) === 'u') { - esc1++; - esc2 = 4; - } - } - } - parser_pos += 1; - if (parser_pos >= input_length) { - // incomplete string/rexp when end-of-file reached. - // bail out with what had been received so far. - return [resulting_string, 'TK_STRING']; - } - } - } - } - - parser_pos += 1; - - resulting_string += sep; - - if (sep === '/') { - // regexps may have modifiers /regexp/MOD , so fetch those, too - while (parser_pos < input_length && - in_array(input.charAt(parser_pos), wordchar)) { - resulting_string += input.charAt(parser_pos); - parser_pos += 1; - } - } - return [resulting_string, 'TK_STRING']; + function is_special_word(word) { + return in_array(word, ['case', 'return', 'do', 'if', 'throw', 'else']); } - if (c === '#') { - if (output.length === 0 && input.charAt(parser_pos) === '!') { - // shebang - resulting_string = c; - while (parser_pos < input_length && c != '\n') { - c = input.charAt(parser_pos); - resulting_string += c; - parser_pos += 1; + function in_array(what, arr) { + for (var i = 0; i < arr.length; i += 1) { + if (arr[i] === what) { + return true; + } } - output.push(trim(resulting_string) + '\n'); - print_newline(); - return get_next_token(); - } + return false; + } - // Spidermonkey-specific sharp variables for circular references - // https://developer.mozilla.org/En/Sharp_variables_in_JavaScript - // http://mxr.mozilla.org/mozilla-central/source/js/src/jsscan.cpp around line 1935 - let sharp = '#'; - if (parser_pos < input_length && in_array(input.charAt(parser_pos), digits)) { - do { - c = input.charAt(parser_pos); - sharp += c; - parser_pos += 1; - } while (parser_pos < input_length && c !== '#' && c !== '='); - if (c === '#') { - // - } else if (input.charAt(parser_pos) === '[' && - input.charAt(parser_pos + 1) === ']') { - sharp += '[]'; - parser_pos += 2; - } else if (input.charAt(parser_pos) === '{' && - input.charAt(parser_pos + 1) === '}') { - sharp += '{}'; - parser_pos += 2; + function look_up(exclude) { + var local_pos = parser_pos; + var c = input.charAt(local_pos); + while (in_array(c, whitespace) && c !== exclude) { + local_pos++; + if (local_pos >= input_length) { + return 0; + } + c = input.charAt(local_pos); } - return [sharp, 'TK_WORD']; - } + return c; } - if (c === '<' && input.substring(parser_pos - 1, parser_pos + 3) === '') { - flags.in_html_comment = false; - parser_pos += 2; - if (wanted_newline) { - print_newline(); - } - return ['-->', 'TK_COMMENT']; - } + n_newlines = 0; - if (in_array(c, punct)) { - while (parser_pos < input_length && - in_array(c + input.charAt(parser_pos), punct)) { - c += input.charAt(parser_pos); - parser_pos += 1; if (parser_pos >= input_length) { - break; + return ['', 'TK_EOF']; } - } - if (c === '=') { - return [c, 'TK_EQUALS']; - } else { - return [c, 'TK_OPERATOR']; - } + wanted_newline = false; + + var c = input.charAt(parser_pos); + parser_pos += 1; + + + var keep_whitespace = opt_keep_array_indentation && is_array(flags.mode); + + if (keep_whitespace) { + + // + // slight mess to allow nice preservation of array indentation and reindent that correctly + // first time when we get to the arrays: + // var a = [ + // ....'something' + // we make note of whitespace_count = 4 into flags.indentation_baseline + // so we know that 4 whitespaces in original source match indent_level of reindented source + // + // and afterwards, when we get to + // 'something, + // .......'something else' + // we know that this should be indented to indent_level + (7 - indentation_baseline) spaces + // + var whitespace_count = 0; + + while (in_array(c, whitespace)) { + + if (c === "\n") { + trim_output(); + output.push("\n"); + just_added_newline = true; + whitespace_count = 0; + } else { + if (c === '\t') { + whitespace_count += 4; + } else if (c === '\r') { + // nothing + } else { + whitespace_count += 1; + } + } + + if (parser_pos >= input_length) { + return ['', 'TK_EOF']; + } + + c = input.charAt(parser_pos); + parser_pos += 1; + + } + if (flags.indentation_baseline === -1) { + flags.indentation_baseline = whitespace_count; + } + + if (just_added_newline) { + for (i = 0; i < flags.indentation_level + 1; i += 1) { + output.push(indent_string); + } + if (flags.indentation_baseline !== -1) { + for (i = 0; i < whitespace_count - flags.indentation_baseline; i++) { + output.push(' '); + } + } + } + + } else { + while (in_array(c, whitespace)) { + + if (c === "\n") { + n_newlines += ((opt_max_preserve_newlines) ? (n_newlines <= opt_max_preserve_newlines) ? 1 : 0 : 1); + } + + + if (parser_pos >= input_length) { + return ['', 'TK_EOF']; + } + + c = input.charAt(parser_pos); + parser_pos += 1; + + } + + if (opt_preserve_newlines) { + if (n_newlines > 1) { + for (i = 0; i < n_newlines; i += 1) { + print_newline(i === 0); + just_added_newline = true; + } + } + } + wanted_newline = n_newlines > 0; + } + + + if (in_array(c, wordchar)) { + if (parser_pos < input_length) { + while (in_array(input.charAt(parser_pos), wordchar)) { + c += input.charAt(parser_pos); + parser_pos += 1; + if (parser_pos === input_length) { + break; + } + } + } + + // small and surprisingly unugly hack for 1E-10 representation + if (parser_pos !== input_length && c.match(/^[0-9]+[Ee]$/) && (input.charAt(parser_pos) === '-' || input.charAt(parser_pos) === '+')) { + + var sign = input.charAt(parser_pos); + parser_pos += 1; + + var t = get_next_token(); + c += sign + t[0]; + return [c, 'TK_WORD']; + } + + if (c === 'in') { // hack for 'in' operator + return [c, 'TK_OPERATOR']; + } + if (wanted_newline && last_type !== 'TK_OPERATOR' + && last_type !== 'TK_EQUALS' + && !flags.if_line && (opt_preserve_newlines || last_text !== 'var')) { + print_newline(); + } + return [c, 'TK_WORD']; + } + + if (c === '(' || c === '[') { + return [c, 'TK_START_EXPR']; + } + + if (c === ')' || c === ']') { + return [c, 'TK_END_EXPR']; + } + + if (c === '{') { + return [c, 'TK_START_BLOCK']; + } + + if (c === '}') { + return [c, 'TK_END_BLOCK']; + } + + if (c === ';') { + return [c, 'TK_SEMICOLON']; + } + + if (c === '/') { + var comment = ''; + // peek for comment /* ... */ + var inline_comment = true; + if (input.charAt(parser_pos) === '*') { + parser_pos += 1; + if (parser_pos < input_length) { + while (parser_pos < input_length && + ! (input.charAt(parser_pos) === '*' && input.charAt(parser_pos + 1) && input.charAt(parser_pos + 1) === '/')) { + c = input.charAt(parser_pos); + comment += c; + if (c === "\n" || c === "\r") { + inline_comment = false; + } + parser_pos += 1; + if (parser_pos >= input_length) { + break; + } + } + } + parser_pos += 2; + if (inline_comment && n_newlines === 0) { + return ['/*' + comment + '*/', 'TK_INLINE_COMMENT']; + } else { + return ['/*' + comment + '*/', 'TK_BLOCK_COMMENT']; + } + } + // peek for comment // ... + if (input.charAt(parser_pos) === '/') { + comment = c; + while (input.charAt(parser_pos) !== '\r' && input.charAt(parser_pos) !== '\n') { + comment += input.charAt(parser_pos); + parser_pos += 1; + if (parser_pos >= input_length) { + break; + } + } + if (wanted_newline) { + print_newline(); + } + return [comment, 'TK_COMMENT']; + } + + } + + if (c === "'" || // string + c === '"' || // string + (c === '/' && + ((last_type === 'TK_WORD' && is_special_word(last_text)) || + (last_text === ')' && in_array(flags.previous_mode, ['(COND-EXPRESSION)', '(FOR-EXPRESSION)'])) || + (last_type === 'TK_COMMA' || last_type === 'TK_COMMENT' || last_type === 'TK_START_EXPR' || last_type === 'TK_START_BLOCK' || last_type === 'TK_END_BLOCK' || last_type === 'TK_OPERATOR' || last_type === 'TK_EQUALS' || last_type === 'TK_EOF' || last_type === 'TK_SEMICOLON')))) { // regexp + var sep = c; + var esc = false; + var esc1 = 0; + var esc2 = 0; + resulting_string = c; + + if (parser_pos < input_length) { + if (sep === '/') { + // + // handle regexp separately... + // + var in_char_class = false; + while (esc || in_char_class || input.charAt(parser_pos) !== sep) { + resulting_string += input.charAt(parser_pos); + if (!esc) { + esc = input.charAt(parser_pos) === '\\'; + if (input.charAt(parser_pos) === '[') { + in_char_class = true; + } else if (input.charAt(parser_pos) === ']') { + in_char_class = false; + } + } else { + esc = false; + } + parser_pos += 1; + if (parser_pos >= input_length) { + // incomplete string/rexp when end-of-file reached. + // bail out with what had been received so far. + return [resulting_string, 'TK_STRING']; + } + } + + } else { + // + // and handle string also separately + // + while (esc || input.charAt(parser_pos) !== sep) { + resulting_string += input.charAt(parser_pos); + if (esc1 && esc1 >= esc2) { + esc1 = parseInt(resulting_string.substr(-esc2), 16); + if (esc1 && esc1 >= 0x20 && esc1 <= 0x7e) { + esc1 = String.fromCharCode(esc1); + resulting_string = resulting_string.substr(0, resulting_string.length - esc2 - 2) + (((esc1 === sep) || (esc1 === '\\')) ? '\\' : '') + esc1; + } + esc1 = 0; + } + if (esc1) { + esc1++; + } else if (!esc) { + esc = input.charAt(parser_pos) === '\\'; + } else { + esc = false; + if (opt_unescape_strings) { + if (input.charAt(parser_pos) === 'x') { + esc1++; + esc2 = 2; + } else if (input.charAt(parser_pos) === 'u') { + esc1++; + esc2 = 4; + } + } + } + parser_pos += 1; + if (parser_pos >= input_length) { + // incomplete string/rexp when end-of-file reached. + // bail out with what had been received so far. + return [resulting_string, 'TK_STRING']; + } + } + } + + + + } + + parser_pos += 1; + + resulting_string += sep; + + if (sep === '/') { + // regexps may have modifiers /regexp/MOD , so fetch those, too + while (parser_pos < input_length && in_array(input.charAt(parser_pos), wordchar)) { + resulting_string += input.charAt(parser_pos); + parser_pos += 1; + } + } + return [resulting_string, 'TK_STRING']; + } + + if (c === '#') { + + + if (output.length === 0 && input.charAt(parser_pos) === '!') { + // shebang + resulting_string = c; + while (parser_pos < input_length && c !== '\n') { + c = input.charAt(parser_pos); + resulting_string += c; + parser_pos += 1; + } + output.push(trim(resulting_string) + '\n'); + print_newline(); + return get_next_token(); + } + + + + // Spidermonkey-specific sharp variables for circular references + // https://developer.mozilla.org/En/Sharp_variables_in_JavaScript + // http://mxr.mozilla.org/mozilla-central/source/js/src/jsscan.cpp around line 1935 + var sharp = '#'; + if (parser_pos < input_length && in_array(input.charAt(parser_pos), digits)) { + do { + c = input.charAt(parser_pos); + sharp += c; + parser_pos += 1; + } while (parser_pos < input_length && c !== '#' && c !== '='); + if (c === '#') { + // + } else if (input.charAt(parser_pos) === '[' && input.charAt(parser_pos + 1) === ']') { + sharp += '[]'; + parser_pos += 2; + } else if (input.charAt(parser_pos) === '{' && input.charAt(parser_pos + 1) === '}') { + sharp += '{}'; + parser_pos += 2; + } + return [sharp, 'TK_WORD']; + } + } + + if (c === '<' && input.substring(parser_pos - 1, parser_pos + 3) === '') { + flags.in_html_comment = false; + parser_pos += 2; + if (wanted_newline) { + print_newline(); + } + return ['-->', 'TK_COMMENT']; + } + + if (in_array(c, punct)) { + while (parser_pos < input_length && in_array(c + input.charAt(parser_pos), punct)) { + c += input.charAt(parser_pos); + parser_pos += 1; + if (parser_pos >= input_length) { + break; + } + } + + if (c === ',') { + return [c, 'TK_COMMA']; + } else if (c === '=') { + return [c, 'TK_EQUALS']; + } else { + return [c, 'TK_OPERATOR']; + } + } + + return [c, 'TK_UNKNOWN']; } - return [c, 'TK_UNKNOWN']; - } - - //---------------------------------- - indent_string = ''; - while (opt_indent_size > 0) { - indent_string += opt_indent_char; - opt_indent_size -= 1; - } - - while (js_source_text && (js_source_text.charAt(0) === ' ' || - js_source_text.charAt(0) === '\t')) { - preindent_string += js_source_text.charAt(0); - js_source_text = js_source_text.substring(1); - } - input = js_source_text; - - last_word = ''; // last 'TK_WORD' passed - last_type = 'TK_START_EXPR'; // last token type - last_text = ''; // last token text - last_last_text = ''; // pre-last token text - output = []; - - do_block_just_closed = false; - - whitespace = "\n\r\t ".split(''); - wordchar = 'abcdefghijklmnopqrstuvwxyz' + - 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_$'.split(''); - digits = '0123456789'.split(''); - - punct = '+ - * / % & ++ -- = += -= *= /= %= == === != !== > < >= <= >> << ' + - '>>> >>>= >>= <<= && &= | || ! !! , : ? ^ ^= |= ::'; - punct += ' <%= <% %> '; // try not to break markup language identifiers - punct = punct.split(' '); - - // words which should always start on new line. - line_starters = ['continue', 'try', 'throw', 'return', 'var', 'if', 'switch', - 'case', 'default', 'for', 'while', 'break', 'function']; - - // states showing if we are currently in expression (i.e. "if" case) - - // 'EXPRESSION', or in usual block (like, procedure), 'BLOCK'. - // some formatting depends on that. - flag_store = []; - set_mode('BLOCK'); - - parser_pos = 0; - while (true) { - let t = get_next_token(parser_pos); - token_text = t[0]; - token_type = t[1]; - if (token_type === 'TK_EOF') { - break; + //---------------------------------- + indent_string = ''; + while (opt_indent_size > 0) { + indent_string += opt_indent_char; + opt_indent_size -= 1; } - switch (token_type) { + while (js_source_text && (js_source_text.charAt(0) === ' ' || js_source_text.charAt(0) === '\t')) { + preindent_string += js_source_text.charAt(0); + js_source_text = js_source_text.substring(1); + } + input = js_source_text; - case 'TK_START_EXPR': + last_word = ''; // last 'TK_WORD' passed + last_type = 'TK_START_EXPR'; // last token type + last_text = ''; // last token text + last_last_text = ''; // pre-last token text + output = []; - if (token_text === '[') { + do_block_just_closed = false; - if (last_type === 'TK_WORD' || last_text === ')') { - // this is array index specifier, break immediately - // a[x], fn()[x] - if (in_array(last_text, line_starters)) { - print_single_space(); - } - set_mode('(EXPRESSION)'); - print_token(); - break; + whitespace = "\n\r\t ".split(''); + wordchar = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_$'.split(''); + digits = '0123456789'.split(''); + + punct = '+ - * / % & ++ -- = += -= *= /= %= == === != !== > < >= <= >> << >>> >>>= >>= <<= && &= | || ! !! , : ? ^ ^= |= ::'; + punct += ' <%= <% %> '; // try to be a good boy and try not to break the markup language identifiers + punct = punct.split(' '); + + // words which should always start on new line. + line_starters = 'continue,try,throw,return,var,if,switch,case,default,for,while,break,function'.split(','); + + // states showing if we are currently in expression (i.e. "if" case) - 'EXPRESSION', or in usual block (like, procedure), 'BLOCK'. + // some formatting depends on that. + flag_store = []; + set_mode('BLOCK'); + + parser_pos = 0; + while (true) { + var t = get_next_token(); + token_text = t[0]; + token_type = t[1]; + if (token_type === 'TK_EOF') { + break; } - if (flags.mode === '[EXPRESSION]' || flags.mode === '[INDENTED-EXPRESSION]') { - if (last_last_text === ']' && last_text === ',') { - // ], [ goes to new line - if (flags.mode === '[EXPRESSION]') { - flags.mode = '[INDENTED-EXPRESSION]'; - if (!opt_keep_array_indentation) { - indent(); - } + switch (token_type) { + + case 'TK_START_EXPR': + + if (token_text === '[') { + + if (last_type === 'TK_WORD' || last_text === ')') { + // this is array index specifier, break immediately + // a[x], fn()[x] + if (in_array(last_text, line_starters)) { + print_single_space(); + } + set_mode('(EXPRESSION)'); + print_token(); + break; + } + + if (flags.mode === '[EXPRESSION]' || flags.mode === '[INDENTED-EXPRESSION]') { + if (last_last_text === ']' && last_text === ',') { + // ], [ goes to new line + if (flags.mode === '[EXPRESSION]') { + flags.mode = '[INDENTED-EXPRESSION]'; + if (!opt_keep_array_indentation) { + indent(); + } + } + set_mode('[EXPRESSION]'); + if (!opt_keep_array_indentation) { + print_newline(); + } + } else if (last_text === '[') { + if (flags.mode === '[EXPRESSION]') { + flags.mode = '[INDENTED-EXPRESSION]'; + if (!opt_keep_array_indentation) { + indent(); + } + } + set_mode('[EXPRESSION]'); + + if (!opt_keep_array_indentation) { + print_newline(); + } + } else { + set_mode('[EXPRESSION]'); + } + } else { + set_mode('[EXPRESSION]'); + } + + + + } else { + if (last_word === 'for') { + set_mode('(FOR-EXPRESSION)'); + } else if (in_array(last_word, ['if', 'while'])) { + set_mode('(COND-EXPRESSION)'); + } else { + set_mode('(EXPRESSION)'); + } } - set_mode('[EXPRESSION]'); - if (!opt_keep_array_indentation) { - print_newline(); + + if (last_text === ';' || last_type === 'TK_START_BLOCK') { + print_newline(); + } else if (last_type === 'TK_END_EXPR' || last_type === 'TK_START_EXPR' || last_type === 'TK_END_BLOCK' || last_text === '.') { + if (wanted_newline) { + print_newline(); + } + // do nothing on (( and )( and ][ and ]( and .( + } else if (last_type !== 'TK_WORD' && last_type !== 'TK_OPERATOR') { + print_single_space(); + } else if (last_word === 'function' || last_word === 'typeof') { + // function() vs function () + if (opt_jslint_happy) { + print_single_space(); + } + } else if (in_array(last_text, line_starters) || last_text === 'catch') { + if (opt_space_before_conditional) { + print_single_space(); + } } - } else if (last_text === '[') { - if (flags.mode === '[EXPRESSION]') { - flags.mode = '[INDENTED-EXPRESSION]'; - if (!opt_keep_array_indentation) { - indent(); - } - } - set_mode('[EXPRESSION]'); - - if (!opt_keep_array_indentation) { - print_newline(); - } - } else { - set_mode('[EXPRESSION]'); - } - } else { - set_mode('[EXPRESSION]'); - } - - - - } else { - if (last_word === 'for') { - set_mode('(FOR-EXPRESSION)'); - } else if (in_array(last_word, ['if', 'while'])) { - set_mode('(COND-EXPRESSION)'); - } else { - set_mode('(EXPRESSION)'); - } - } - - if (last_text === ';' || last_type === 'TK_START_BLOCK') { - print_newline(); - } else if (last_type === 'TK_END_EXPR' || last_type === 'TK_START_EXPR' || - last_type === 'TK_END_BLOCK' || last_text === '.') { - if (wanted_newline) { - print_newline(); - } - // do nothing on (( and )( and ][ and ]( and .( - } else if (last_type !== 'TK_WORD' && last_type !== 'TK_OPERATOR') { - print_single_space(); - } else if (last_word === 'function' || last_word === 'typeof') { - // function() vs function () - if (opt_jslint_happy) { - print_single_space(); - } - } else if (in_array(last_text, line_starters) || last_text === 'catch') { - if (opt_space_before_conditional) { - print_single_space(); - } - } - print_token(); - - break; - - case 'TK_END_EXPR': - if (token_text === ']') { - if (opt_keep_array_indentation) { - if (last_text === '}') { - // trim_output(); - // print_newline(true); - remove_indent(); print_token(); + + break; + + case 'TK_END_EXPR': + if (token_text === ']') { + if (opt_keep_array_indentation) { + if (last_text === '}') { + // trim_output(); + // print_newline(true); + remove_indent(); + print_token(); + restore_mode(); + break; + } + } else { + if (flags.mode === '[INDENTED-EXPRESSION]') { + if (last_text === ']') { + restore_mode(); + print_newline(); + print_token(); + break; + } + } + } + } restore_mode(); - break; - } - } else { - if (flags.mode === '[INDENTED-EXPRESSION]') { - if (last_text === ']') { - restore_mode(); - print_newline(); - print_token(); - break; - } - } - } - } - restore_mode(); - print_token(); - break; - - case 'TK_START_BLOCK': - - if (last_word === 'do') { - set_mode('DO_BLOCK'); - } else { - set_mode('BLOCK'); - } - if (opt_brace_style=="expand" || opt_brace_style=="expand-strict") { - let empty_braces = false; - if (opt_brace_style == "expand-strict") - { - empty_braces = (look_up() == '}'); - if (!empty_braces) { - print_newline(true); - } - } else { - if (last_type !== 'TK_OPERATOR') { - if (last_text === '=' || (is_special_word(last_text) && - last_text !== 'else')) { - print_single_space(); - } else { - print_newline(true); - } - } - } - print_token(); - if (!empty_braces) indent(); - } else { - if (last_type !== 'TK_OPERATOR' && last_type !== 'TK_START_EXPR') { - if (last_type === 'TK_START_BLOCK') { - print_newline(); - } else { - print_single_space(); - } - } else { - // if TK_OPERATOR or TK_START_EXPR - if (is_array(flags.previous_mode) && last_text === ',') { - if (last_last_text === '}') { - // }, { in array context - print_single_space(); - } else { - print_newline(); // [a, b, c, { - } - } - } - indent(); - print_token(); - } - - break; - - case 'TK_END_BLOCK': - restore_mode(); - if (opt_brace_style=="expand" || opt_brace_style == "expand-strict") { - if (last_text !== '{') { - print_newline(); - } - print_token(); - } else { - if (last_type === 'TK_START_BLOCK') { - // nothing - if (just_added_newline) { - remove_indent(); - } else { - // {} - trim_output(); - } - } else { - if (is_array(flags.mode) && opt_keep_array_indentation) { - // we REALLY need a newline here, but newliner would skip that - opt_keep_array_indentation = false; - print_newline(); - opt_keep_array_indentation = true; - - } else { - print_newline(); - } - } - print_token(); - } - break; - - case 'TK_WORD': - - // no, it's not you. even I have problems understanding how this works - // and what does what. - if (do_block_just_closed) { - // do {} ## while () - print_single_space(); - print_token(); - print_single_space(); - do_block_just_closed = false; - break; - } - - if (token_text === 'function') { - if (flags.var_line) { - flags.var_line_reindented = true; - } - if ((just_added_newline || last_text === ';') && last_text !== '{' - && last_type != 'TK_BLOCK_COMMENT' && last_type != 'TK_COMMENT') { - // make sure there is a nice clean space of at least one blank line - // before a new function definition - n_newlines = just_added_newline ? n_newlines : 0; - if ( ! opt_preserve_newlines) { - n_newlines = 1; - } - - for (let i = 0; i < 2 - n_newlines; i++) { - print_newline(false); - } - } - if (last_text === 'get' || last_text === 'set' || last_text === 'new') { - print_single_space(); - } - print_token(); - last_word = token_text; - break; - } - - if (token_text === 'case' || (token_text === 'default' && - flags.in_case_statement)) { - if (last_text === ':' || flags.case_body) { - // switch cases following one another - remove_indent(); - } else { - // case statement starts in the same line where switch - if (!opt_indent_case) - flags.indentation_level--; - print_newline(); - if (!opt_indent_case) - flags.indentation_level++; - } - print_token(); - flags.in_case = true; - flags.in_case_statement = true; - flags.case_body = false; - break; - } - - prefix = 'NONE'; - - if (last_type === 'TK_END_BLOCK') { - - if (!in_array(token_text.toLowerCase(), ['else', 'catch', 'finally'])) { - prefix = 'NEWLINE'; - } else { - if (opt_brace_style=="expand" || opt_brace_style=="end-expand" || - opt_brace_style == "expand-strict") { - prefix = 'NEWLINE'; - } else { - prefix = 'SPACE'; - print_single_space(); - } - } - } else if (last_type === 'TK_SEMICOLON' && (flags.mode === 'BLOCK' || - flags.mode === 'DO_BLOCK')) { - prefix = 'NEWLINE'; - } else if (last_type === 'TK_SEMICOLON' && is_expression(flags.mode)) { - prefix = 'SPACE'; - } else if (last_type === 'TK_STRING') { - prefix = 'NEWLINE'; - } else if (last_type === 'TK_WORD') { - if (last_text === 'else') { - // eat newlines between ...else *** some_op... - // won't preserve extra newlines in this place (if any), - // but don't care that much. - trim_output(true); - } - prefix = 'SPACE'; - } else if (last_type === 'TK_START_BLOCK') { - prefix = 'NEWLINE'; - } else if (last_type === 'TK_END_EXPR') { - print_single_space(); - prefix = 'NEWLINE'; - } - - if (in_array(token_text, line_starters) && last_text !== ')') { - if (last_text == 'else') { - prefix = 'SPACE'; - } else { - prefix = 'NEWLINE'; - } - - } - - if (flags.if_line && last_type === 'TK_END_EXPR') { - flags.if_line = false; - } - if (in_array(token_text.toLowerCase(), ['else', 'catch', 'finally'])) { - if (last_type !== 'TK_END_BLOCK' || opt_brace_style=="expand" || - opt_brace_style=="end-expand" || opt_brace_style == "expand-strict") { - print_newline(); - } else { - trim_output(true); - print_single_space(); - } - } else if (prefix === 'NEWLINE') { - if (is_special_word(last_text)) { - // no newline between 'return nnn' - print_single_space(); - } else if (last_type !== 'TK_END_EXPR') { - if ((last_type !== 'TK_START_EXPR' || token_text !== 'var') && - last_text !== ':') { - // no need to force newline on 'var': for (var x = 0...) - if (token_text === 'if' && last_word === 'else' && - last_text !== '{') { - // no newline for } else if { - print_single_space(); - } else { - flags.var_line = false; - flags.var_line_reindented = false; - print_newline(); - } - } - } else if (in_array(token_text, line_starters) && last_text != ')') { - flags.var_line = false; - flags.var_line_reindented = false; - print_newline(); - } - } else if (is_array(flags.mode) && last_text === ',' && - last_last_text === '}') { - print_newline(); // }, in lists get a newline treatment - } else if (prefix === 'SPACE') { - print_single_space(); - } - print_token(); - last_word = token_text; - - if (token_text === 'var') { - flags.var_line = true; - flags.var_line_reindented = false; - flags.var_line_tainted = false; - } - - if (token_text === 'if') { - flags.if_line = true; - } - if (token_text === 'else') { - flags.if_line = false; - } - - break; - - case 'TK_SEMICOLON': - - print_token(); - flags.var_line = false; - flags.var_line_reindented = false; - if (flags.mode == 'OBJECT') { - // OBJECT mode is weird and doesn't get reset too well. - flags.mode = 'BLOCK'; - } - break; - - case 'TK_STRING': - - if (last_type === 'TK_END_EXPR' && in_array(flags.previous_mode, - ['(COND-EXPRESSION)', '(FOR-EXPRESSION)'])) { - print_single_space(); - } else if (last_type === 'TK_COMMENT' || last_type == 'TK_STRING' || - last_type === 'TK_START_BLOCK' || - last_type === 'TK_END_BLOCK' || last_type === 'TK_SEMICOLON') { - print_newline(); - } else if (last_type === 'TK_WORD') { - print_single_space(); - } - print_token(); - break; - - case 'TK_EQUALS': - if (flags.var_line) { - // just got an '=' in a var-line, different formatting/line-breaking, - // etc will now be done. - flags.var_line_tainted = true; - } - print_single_space(); - print_token(); - print_single_space(); - break; - - case 'TK_OPERATOR': - let space_before = true; - let space_after = true; - - if (flags.var_line && token_text === ',' && (is_expression(flags.mode))) { - // do not break on comma, for(var a = 1, b = 2) - flags.var_line_tainted = false; - } - - if (flags.var_line) { - if (token_text === ',') { - if (flags.var_line_tainted) { print_token(); - flags.var_line_reindented = true; - flags.var_line_tainted = false; + break; + + case 'TK_START_BLOCK': + + if (last_word === 'do') { + set_mode('DO_BLOCK'); + } else { + set_mode('BLOCK'); + } + if (opt_brace_style === "expand" || opt_brace_style === "expand-strict") { + var empty_braces = false; + if (opt_brace_style === "expand-strict") { + empty_braces = (look_up() === '}'); + if (!empty_braces) { + print_newline(true); + } + } else { + if (last_type !== 'TK_OPERATOR') { + if (last_text === '=' || (is_special_word(last_text) && last_text !== 'else')) { + print_single_space(); + } else { + print_newline(true); + } + } + } + print_token(); + if (!empty_braces) { + indent(); + } + } else { + if (last_type !== 'TK_OPERATOR' && last_type !== 'TK_START_EXPR') { + if (last_type === 'TK_START_BLOCK') { + print_newline(); + } else { + print_single_space(); + } + } else { + // if TK_OPERATOR or TK_START_EXPR + if (is_array(flags.previous_mode) && last_text === ',') { + if (last_last_text === '}') { + // }, { in array context + print_single_space(); + } else { + print_newline(); // [a, b, c, { + } + } + } + indent(); + print_token(); + } + + break; + + case 'TK_END_BLOCK': + restore_mode(); + if (opt_brace_style === "expand" || opt_brace_style === "expand-strict") { + if (last_text !== '{') { + print_newline(); + } + print_token(); + } else { + if (last_type === 'TK_START_BLOCK') { + // nothing + if (just_added_newline) { + remove_indent(); + } else { + // {} + trim_output(); + } + } else { + if (is_array(flags.mode) && opt_keep_array_indentation) { + // we REALLY need a newline here, but newliner would skip that + opt_keep_array_indentation = false; + print_newline(); + opt_keep_array_indentation = true; + + } else { + print_newline(); + } + } + print_token(); + } + break; + + case 'TK_WORD': + + // no, it's not you. even I have problems understanding how this works + // and what does what. + if (do_block_just_closed) { + // do {} ## while () + print_single_space(); + print_token(); + print_single_space(); + do_block_just_closed = false; + break; + } + + prefix = 'NONE'; + + if (token_text === 'function') { + if (flags.var_line && last_type !== 'TK_EQUALS' ) { + flags.var_line_reindented = true; + } + if ((just_added_newline || last_text === ';') && last_text !== '{' + && last_type !== 'TK_BLOCK_COMMENT' && last_type !== 'TK_COMMENT') { + // make sure there is a nice clean space of at least one blank line + // before a new function definition + n_newlines = just_added_newline ? n_newlines : 0; + if (!opt_preserve_newlines) { + n_newlines = 1; + } + + for (var i = 0; i < 2 - n_newlines; i++) { + print_newline(false); + } + } + if (last_type === 'TK_WORD') { + if (last_text === 'get' || last_text === 'set' || last_text === 'new' || last_text === 'return') { + print_single_space(); + } else { + print_newline(); + } + } else if (last_type === 'TK_OPERATOR' || last_text === '=') { + // foo = function + print_single_space(); + } else if (is_expression(flags.mode)) { + //ää print nothing + } else { + print_newline(); + } + + print_token(); + last_word = token_text; + break; + } + + if (token_text === 'case' || (token_text === 'default' && flags.in_case_statement)) { + if (last_text === ':' || flags.case_body) { + // switch cases following one another + remove_indent(); + } else { + // case statement starts in the same line where switch + if (!opt_indent_case) { + flags.indentation_level--; + } + print_newline(); + if (!opt_indent_case) { + flags.indentation_level++; + } + } + print_token(); + flags.in_case = true; + flags.in_case_statement = true; + flags.case_body = false; + break; + } + + if (last_type === 'TK_END_BLOCK') { + + if (!in_array(token_text.toLowerCase(), ['else', 'catch', 'finally'])) { + prefix = 'NEWLINE'; + } else { + if (opt_brace_style === "expand" || opt_brace_style === "end-expand" || opt_brace_style === "expand-strict") { + prefix = 'NEWLINE'; + } else { + prefix = 'SPACE'; + print_single_space(); + } + } + } else if (last_type === 'TK_SEMICOLON' && (flags.mode === 'BLOCK' || flags.mode === 'DO_BLOCK')) { + prefix = 'NEWLINE'; + } else if (last_type === 'TK_SEMICOLON' && is_expression(flags.mode)) { + prefix = 'SPACE'; + } else if (last_type === 'TK_STRING') { + prefix = 'NEWLINE'; + } else if (last_type === 'TK_WORD') { + if (last_text === 'else') { + // eat newlines between ...else *** some_op... + // won't preserve extra newlines in this place (if any), but don't care that much + trim_output(true); + } + prefix = 'SPACE'; + } else if (last_type === 'TK_START_BLOCK') { + prefix = 'NEWLINE'; + } else if (last_type === 'TK_END_EXPR') { + print_single_space(); + prefix = 'NEWLINE'; + } + + if (in_array(token_text, line_starters) && last_text !== ')') { + if (last_text === 'else') { + prefix = 'SPACE'; + } else { + prefix = 'NEWLINE'; + } + + } + + if (flags.if_line && last_type === 'TK_END_EXPR') { + flags.if_line = false; + } + if (in_array(token_text.toLowerCase(), ['else', 'catch', 'finally'])) { + if (last_type !== 'TK_END_BLOCK' || opt_brace_style === "expand" || opt_brace_style === "end-expand" || opt_brace_style === "expand-strict") { + print_newline(); + } else { + trim_output(true); + print_single_space(); + } + } else if (prefix === 'NEWLINE') { + if (is_special_word(last_text)) { + // no newline between 'return nnn' + print_single_space(); + } else if (last_type !== 'TK_END_EXPR') { + if ((last_type !== 'TK_START_EXPR' || token_text !== 'var') && last_text !== ':') { + // no need to force newline on 'var': for (var x = 0...) + if (token_text === 'if' && last_word === 'else' && last_text !== '{') { + // no newline for } else if { + print_single_space(); + } else { + flags.var_line = false; + flags.var_line_reindented = false; + print_newline(); + } + } + } else if (in_array(token_text, line_starters) && last_text !== ')') { + flags.var_line = false; + flags.var_line_reindented = false; + print_newline(); + } + } else if (is_array(flags.mode) && last_text === ',' && last_last_text === '}') { + print_newline(); // }, in lists get a newline treatment + } else if (prefix === 'SPACE') { + print_single_space(); + } + print_token(); + last_word = token_text; + + if (token_text === 'var') { + flags.var_line = true; + flags.var_line_reindented = false; + flags.var_line_tainted = false; + } + + if (token_text === 'if') { + flags.if_line = true; + } + if (token_text === 'else') { + flags.if_line = false; + } + + break; + + case 'TK_SEMICOLON': + + print_token(); + flags.var_line = false; + flags.var_line_reindented = false; + if (flags.mode === 'OBJECT') { + // OBJECT mode is weird and doesn't get reset too well. + flags.mode = 'BLOCK'; + } + break; + + case 'TK_STRING': + + if (last_type === 'TK_END_EXPR' && in_array(flags.previous_mode, ['(COND-EXPRESSION)', '(FOR-EXPRESSION)'])) { + print_single_space(); + } else if (last_type === 'TK_COMMENT' || last_type === 'TK_STRING' || last_type === 'TK_START_BLOCK' || last_type === 'TK_END_BLOCK' || last_type === 'TK_SEMICOLON') { + print_newline(); + } else if (last_type === 'TK_WORD') { + print_single_space(); + } + print_token(); + break; + + case 'TK_EQUALS': + if (flags.var_line) { + // just got an '=' in a var-line, different formatting/line-breaking, etc will now be done + flags.var_line_tainted = true; + } + print_single_space(); + print_token(); + print_single_space(); + break; + + case 'TK_COMMA': + if (flags.var_line) { + if (is_expression(flags.mode) || last_type === 'TK_END_BLOCK' ) { + // do not break on comma, for(var a = 1, b = 2) + flags.var_line_tainted = false; + } + if (flags.var_line_tainted) { + print_token(); + flags.var_line_reindented = true; + flags.var_line_tainted = false; + print_newline(); + break; + } else { + flags.var_line_tainted = false; + } + + print_token(); + print_single_space(); + break; + } + + if (last_type === 'TK_COMMENT') { + print_newline(); + } + + if (last_type === 'TK_END_BLOCK' && flags.mode !== "(EXPRESSION)") { + print_token(); + if (flags.mode === 'OBJECT' && last_text === '}') { + print_newline(); + } else { + print_single_space(); + } + } else { + if (flags.mode === 'OBJECT') { + print_token(); + print_newline(); + } else { + // EXPR or DO_BLOCK + print_token(); + print_single_space(); + } + } + break; + + + case 'TK_OPERATOR': + + var space_before = true; + var space_after = true; + + if (is_special_word(last_text)) { + // "return" had a special handling in TK_WORD. Now we need to return the favor + print_single_space(); + print_token(); + break; + } + + // hack for actionscript's import .*; + if (token_text === '*' && last_type === 'TK_UNKNOWN' && !last_last_text.match(/^\d+$/)) { + print_token(); + break; + } + + if (token_text === ':' && flags.in_case) { + if (opt_indent_case) { + flags.case_body = true; + } + print_token(); // colon really asks for separate treatment + print_newline(); + flags.in_case = false; + break; + } + + if (token_text === '::') { + // no spaces around exotic namespacing syntax operator + print_token(); + break; + } + + if (in_array(token_text, ['--', '++', '!']) || (in_array(token_text, ['-', '+']) && (in_array(last_type, ['TK_START_BLOCK', 'TK_START_EXPR', 'TK_EQUALS', 'TK_OPERATOR']) || in_array(last_text, line_starters)))) { + // unary operators (and binary +/- pretending to be unary) special cases + + space_before = false; + space_after = false; + + if (last_text === ';' && is_expression(flags.mode)) { + // for (;; ++i) + // ^^^ + space_before = true; + } + if (last_type === 'TK_WORD' && in_array(last_text, line_starters)) { + space_before = true; + } + + if (flags.mode === 'BLOCK' && (last_text === '{' || last_text === ';')) { + // { foo; --i } + // foo(); --bar; + print_newline(); + } + } else if (token_text === '.') { + // decimal digits or object.property + space_before = false; + + } else if (token_text === ':') { + if (flags.ternary_depth === 0) { + if (flags.mode === 'BLOCK') { + flags.mode = 'OBJECT'; + } + space_before = false; + } else { + flags.ternary_depth -= 1; + } + } else if (token_text === '?') { + flags.ternary_depth += 1; + } + if (space_before) { + print_single_space(); + } + + print_token(); + + if (space_after) { + print_single_space(); + } + + break; + + case 'TK_BLOCK_COMMENT': + + var lines = split_newlines(token_text); + var j; // iterator for this case + + if (all_lines_start_with(lines.slice(1), '*')) { + // javadoc: reformat and reindent + print_newline(); + output.push(lines[0]); + for (j = 1; j < lines.length; j++) { + print_newline(); + output.push(' '); + output.push(trim(lines[j])); + } + + } else { + + // simple block comment: leave intact + if (lines.length > 1) { + // multiline comment block starts with a new line + print_newline(); + } else { + // single-line /* comment */ stays where it is + if (last_type === 'TK_END_BLOCK') { + print_newline(); + } else { + print_single_space(); + } + + } + + for (j = 0; j < lines.length; j++) { + output.push(lines[j]); + output.push("\n"); + } + + } + if (look_up('\n') !== '\n') { + print_newline(); + } + break; + + case 'TK_INLINE_COMMENT': + print_single_space(); + print_token(); + if (is_expression(flags.mode)) { + print_single_space(); + } else { + force_newline(); + } + break; + + case 'TK_COMMENT': + + if (last_text === ',' && !wanted_newline) { + trim_output(true); + } + if (last_type !== 'TK_COMMENT') { + if (wanted_newline) { + print_newline(); + } else { + print_single_space(); + } + } + print_token(); print_newline(); break; - } else { - flags.var_line_tainted = false; - } - // } else if (token_text === ':') { - // hmm, when does this happen? tests don't catch this - // flags.var_line = false; - } - } - if (is_special_word(last_text)) { - // "return" had a special handling in TK_WORD. Now we need to return the - // favor. - print_single_space(); - print_token(); - break; - } - - // hack for actionscript's import .*; - if (token_text == '*' && last_type == 'TK_UNKNOWN' && - !last_last_text.match(/^\d+$/)) { - print_token(); - break; - } - - if (token_text === ':' && flags.in_case) { - if (opt_indent_case) { - flags.case_body = true; - } - print_token(); // colon really asks for separate treatment - print_newline(); - flags.in_case = false; - break; - } - - if (token_text === '::') { - // no spaces around exotic namespacing syntax operator - print_token(); - break; - } - - if (token_text === ',') { - if (flags.var_line) { - if (flags.var_line_tainted) { + case 'TK_UNKNOWN': + if (is_special_word(last_text)) { + print_single_space(); + } print_token(); - print_newline(); - flags.var_line_tainted = false; - } else { - print_token(); - print_single_space(); - } - } else if (last_type === 'TK_END_BLOCK' && - flags.mode !== "(EXPRESSION)") { - print_token(); - if (flags.mode === 'OBJECT' && last_text === '}') { - print_newline(); - } else { - print_single_space(); - } - } else { - if (flags.mode === 'OBJECT') { - print_token(); - print_newline(); - } else { - // EXPR or DO_BLOCK - print_token(); - print_single_space(); - } - } - break; - } else if (in_array(token_text, ['--', '++', '!']) || - (in_array(token_text, ['-', '+']) && (in_array(last_type, - ['TK_START_BLOCK', 'TK_START_EXPR', 'TK_EQUALS', 'TK_OPERATOR']) || - in_array(last_text, line_starters)))) { - // unary operators (and binary +/- pretending to be unary) special cases - - space_before = false; - space_after = false; - - if (last_text === ';' && is_expression(flags.mode)) { - // for (;; ++i) - // ^^^ - space_before = true; - } - if (last_type === 'TK_WORD' && in_array(last_text, line_starters)) { - space_before = true; + break; } - if (flags.mode === 'BLOCK' && (last_text === '{' || last_text === ';')) { - // { foo; --i } - // foo(); --bar; - print_newline(); - } - } else if (token_text === '.') { - // decimal digits or object.property - space_before = false; - - } else if (token_text === ':') { - if (flags.ternary_depth == 0) { - if (flags.mode == 'BLOCK') { - flags.mode = 'OBJECT'; - } - space_before = false; - } else { - flags.ternary_depth -= 1; - } - } else if (token_text === '?') { - flags.ternary_depth += 1; - } - if (space_before) { - print_single_space(); - } - - print_token(); - - if (space_after) { - print_single_space(); - } - - if (token_text === '!') { - // flags.eat_next_space = true; - } - - break; - - case 'TK_BLOCK_COMMENT': - let lines = split_newlines(token_text); - - if (all_lines_start_with(lines.slice(1), '*')) { - // javadoc: reformat and reindent - print_newline(); - output.push(lines[0]); - for (let i = 1; i < lines.length; i++) { - print_newline(); - output.push(' '); - output.push(trim(lines[i])); - } - } else { - // simple block comment: leave intact - if (lines.length > 1) { - // multiline comment block starts with a new line - print_newline(); - } else { - // single-line /* comment */ stays where it is - if (last_type === 'TK_END_BLOCK') { - print_newline(); - } else { - print_single_space(); - } - - } - - for (let i = 0; i < lines.length; i++) { - output.push(lines[i]); - output.push("\n"); - } - } - if(look_up('\n') != '\n') { - print_newline(); - } - break; - - case 'TK_INLINE_COMMENT': - print_single_space(); - print_token(); - if (is_expression(flags.mode)) { - print_single_space(); - } else { - force_newline(); - } - break; - case 'TK_COMMENT': - - if (last_text == ',' && ! wanted_newline) { - trim_output(true); - } - if (last_type != 'TK_COMMENT') { - if (wanted_newline) { - print_newline(); - } else { - print_single_space(); - } - } - print_token(); - break; - case 'TK_UNKNOWN': - if (is_special_word(last_text)) { - print_single_space(); - } - print_token(); - break; + last_last_text = last_text; + last_type = token_type; + last_text = token_text; } - last_last_text = last_text; - last_type = token_type; - last_text = token_text; - } + var sweet_code = preindent_string + output.join('').replace(/[\r\n ]+$/, ''); + return sweet_code; - let sweet_code = preindent_string + output.join('').replace(/[\r\n ]+$/, ''); - return sweet_code; } diff --git a/browser/locales/en-US/chrome/browser/devtools/gclicommands.properties b/browser/locales/en-US/chrome/browser/devtools/gclicommands.properties index 18ec2afd9794..07a4b6b5d4d4 100644 --- a/browser/locales/en-US/chrome/browser/devtools/gclicommands.properties +++ b/browser/locales/en-US/chrome/browser/devtools/gclicommands.properties @@ -817,11 +817,10 @@ jsbIndentCharDesc=The chars used to indent each line # does. jsbIndentCharManual=The chars used to indent each line. The possible choices are space or tab. -# LOCALIZATION NOTE (jsbPreserveNewlinesDesc) A very short description of the -# 'jsb ' parameter. This string is designed to be shown -# in a menu alongside the command name, which is why it should be as short as -# possible. -jsbPreserveNewlinesDesc=Keep existing line breaks? +# the 'jsb ' parameter. This string is designed to be +# shown in a menu alongside the command name, which is why it should be as short +# as possible. +jsbDoNotPreserveNewlinesDesc=Do not preserve line breaks # LOCALIZATION NOTE (jsbPreserveNewlinesManual) A fuller description of the # 'jsb ' parameter, displayed when the user asks for help @@ -854,23 +853,18 @@ jsbJslintHappyManual=When set to true, jslint-stricter mode is enforced # 'jsb ' parameter. This string is designed to be shown # in a menu alongside the command name, which is why it should be as short as # possible. -jsbBraceStyleDesc=Collapse, expand, end-expand, expand-strict +jsbBraceStyleDesc=Select the coding style of braces # LOCALIZATION NOTE (jsbBraceStyleManual) A fuller description of the # 'jsb ' parameter, displayed when the user asks for help # on what it does. -jsbBraceStyleManual=The coding style of braces. Either collapse, expand, end-expand or expand-strict +jsbBraceStyleManual=

The coding style of braces. Select from one of the following:

  • collapse
    if (x == 1) {\n  ...\n} else {\n  ...\n}
  • expand
    if (x == 1)\n{\n  ...\n}\nelse\n{\n  ...\n}
  • end-expand
    if (x == 1) {\n  ...\n}\nelse {\n  ...\n}
  • expand-strict
    if (x == 1)\n{\n  return // This option can break scripts\n  {\n    a: 1\n  };\n} else {\n  ...\n}
-# LOCALIZATION NOTE (jsbSpaceBeforeConditionalDesc) A very short description of -# the 'jsb ' parameter. This string is designed to be -# shown in a menu alongside the command name, which is why it should be as short -# as possible. -jsbSpaceBeforeConditionalDesc=Space before if statements? - -# LOCALIZATION NOTE (jsbSpaceBeforeConditionalManual) A fuller description of -# the 'jsb ' parameter, displayed when the user asks for -# help on what it does. -jsbSpaceBeforeConditionalManual=Should a space be added before conditional statements? +# LOCALIZATION NOTE (jsbNoSpaceBeforeConditionalDesc) A very short description +# of the 'jsb ' parameter. This string is designed to +# be shown in a menu alongside the command name, which is why it should be as +# short as possible. +jsbNoSpaceBeforeConditionalDesc=No space before conditional statements # LOCALIZATION NOTE (jsbUnescapeStringsDesc) A very short description of the # 'jsb ' parameter. This string is designed to be shown @@ -887,6 +881,10 @@ jsbUnescapeStringsManual=Should printable characters in strings encoded in \\xNN # the jsb command. jsbInvalidURL=Please enter a valid URL +# LOCALIZATION NOTE (jsbOptionsDesc) The title of a set of options to +# the 'jsb' command, displayed as a heading to the list of options. +jsbOptionsDesc=Options + # LOCALIZATION NOTE (calllogDesc) A very short description of the # 'calllog' command. This string is designed to be shown in a menu # alongside the command name, which is why it should be as short as possible. diff --git a/browser/themes/gnomestripe/devtools/commandline.css b/browser/themes/gnomestripe/devtools/commandline.css index c0879482d0e8..cc7c99cc8b15 100644 --- a/browser/themes/gnomestripe/devtools/commandline.css +++ b/browser/themes/gnomestripe/devtools/commandline.css @@ -63,10 +63,15 @@ .gcli-row-out h4, .gcli-row-out h5, .gcli-row-out th, -.gcli-row-out strong { +.gcli-row-out strong, +.gcli-row-out pre { color: hsl(210,30%,95%); } +.gcli-row-out pre { + font-size: 80%; +} + .gcli-out-shortcut, .gcli-help-synopsis { padding: 0 3px; diff --git a/browser/themes/pinstripe/devtools/commandline.css b/browser/themes/pinstripe/devtools/commandline.css index 459639b38552..e248b2b439f5 100644 --- a/browser/themes/pinstripe/devtools/commandline.css +++ b/browser/themes/pinstripe/devtools/commandline.css @@ -65,10 +65,15 @@ .gcli-row-out h4, .gcli-row-out h5, .gcli-row-out th, -.gcli-row-out strong { +.gcli-row-out strong, +.gcli-row-out pre { color: hsl(210,30%,95%); } +.gcli-row-out pre { + font-size: 80%; +} + .gcli-out-shortcut, .gcli-help-synopsis { padding: 0 3px; diff --git a/browser/themes/winstripe/devtools/commandline.css b/browser/themes/winstripe/devtools/commandline.css index 0dfc8cb02c07..236af480db50 100644 --- a/browser/themes/winstripe/devtools/commandline.css +++ b/browser/themes/winstripe/devtools/commandline.css @@ -63,10 +63,15 @@ .gcli-row-out h4, .gcli-row-out h5, .gcli-row-out th, -.gcli-row-out strong { +.gcli-row-out strong, +.gcli-row-out pre { color: hsl(210,30%,95%); } +.gcli-row-out pre { + font-size: 80%; +} + .gcli-out-shortcut, .gcli-help-synopsis { padding: 0 3px; From e123498db297691a48c47da783996dec33f1a749 Mon Sep 17 00:00:00 2001 From: Michael Ratcliffe Date: Thu, 4 Oct 2012 18:05:38 +0100 Subject: [PATCH 04/18] Bug 797908 - [orange] SyntaxError: missing ) after argument list at resource:///modules/devtools/DeveloperToolbar.jsm:702 r=me --- browser/devtools/shared/DeveloperToolbar.jsm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/browser/devtools/shared/DeveloperToolbar.jsm b/browser/devtools/shared/DeveloperToolbar.jsm index 53d7756c5026..5007ca5e1be7 100644 --- a/browser/devtools/shared/DeveloperToolbar.jsm +++ b/browser/devtools/shared/DeveloperToolbar.jsm @@ -699,7 +699,7 @@ Object.defineProperty(OutputPanel.prototype, 'scrollbarWidth', { return this.__scrollbarWidth; }, enumerable: true -}; +}); /** * Prevent the popup from hiding if it is not permitted via this.canHide. From bfbaba58998b7dbf227a451ff637978007e23273 Mon Sep 17 00:00:00 2001 From: Joe Walker Date: Fri, 5 Oct 2012 00:46:38 +0100 Subject: [PATCH 05/18] =?UTF-8?q?Bug=20766133=20-=20[gcli]=20selected=20te?= =?UTF-8?q?xt=20looks=20bad;=20r=3Dd=C3=A3o?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- browser/themes/gnomestripe/browser.css | 6 ++++++ browser/themes/pinstripe/browser.css | 6 ++++++ browser/themes/winstripe/browser.css | 6 ++++++ 3 files changed, 18 insertions(+) diff --git a/browser/themes/gnomestripe/browser.css b/browser/themes/gnomestripe/browser.css index d1a917efad7a..51ad8e3bcb2b 100644 --- a/browser/themes/gnomestripe/browser.css +++ b/browser/themes/gnomestripe/browser.css @@ -2436,6 +2436,12 @@ html|*#gcli-output-frame { 0 1px 0 hsla(210,16%,76%,.15); } +.gclitoolbar-input-node > .textbox-input-box > html|*.textbox-input::-moz-selection { + background-color: hsl(210,30%,85%); + color: hsl(210,11%,16%); + text-shadow: none; +} + .gclitoolbar-complete-node { padding-left: 21px; background-color: transparent; diff --git a/browser/themes/pinstripe/browser.css b/browser/themes/pinstripe/browser.css index a896eec14b6d..507f60634ff7 100644 --- a/browser/themes/pinstripe/browser.css +++ b/browser/themes/pinstripe/browser.css @@ -3858,6 +3858,12 @@ html|*#gcli-output-frame { 0 1px 0 hsla(210,16%,76%,.15); } +.gclitoolbar-input-node > .textbox-input-box > html|*.textbox-input::-moz-selection { + background-color: hsl(210,30%,85%); + color: hsl(210,11%,16%); + text-shadow: none; +} + .gclitoolbar-complete-node { padding-left: 21px; background-color: transparent; diff --git a/browser/themes/winstripe/browser.css b/browser/themes/winstripe/browser.css index 918b7a52f3cc..d8b51dde9dcc 100644 --- a/browser/themes/winstripe/browser.css +++ b/browser/themes/winstripe/browser.css @@ -3117,6 +3117,12 @@ html|*#gcli-output-frame { 0 0 0 1px hsla(210,40%,83%,.1); } +.gclitoolbar-input-node > .textbox-input-box > html|*.textbox-input::-moz-selection { + background-color: hsl(210,30%,85%); + color: hsl(210,24%,16%); + text-shadow: none; +} + .gclitoolbar-complete-node { padding-left: 21px; background-color: transparent; From b21688b2f7a71b5c852f0ee47d31cdc575a3990e Mon Sep 17 00:00:00 2001 From: Joe Walker Date: Fri, 5 Oct 2012 00:46:38 +0100 Subject: [PATCH 06/18] Bug 788215 - GCLI problems when replacing valid arg value with a new value; r=dcamp --- browser/devtools/commandline/gcli.jsm | 98 +++++++++++++++------------ 1 file changed, 54 insertions(+), 44 deletions(-) diff --git a/browser/devtools/commandline/gcli.jsm b/browser/devtools/commandline/gcli.jsm index 5fa8c589a16a..60fa0ca32fe1 100644 --- a/browser/devtools/commandline/gcli.jsm +++ b/browser/devtools/commandline/gcli.jsm @@ -5469,7 +5469,7 @@ function Requisition(environment, doc) { // The command that we are about to execute. // @see setCommandConversion() this.commandAssignment = new CommandAssignment(); - this._setAssignment(this.commandAssignment, null, true); + this.setAssignment(this.commandAssignment, null); // The object that stores of Assignment objects that we are filling out. // The Assignment objects are stored under their param.name for named @@ -5556,7 +5556,7 @@ Requisition.prototype._commandAssignmentChanged = function(ev) { for (var i = 0; i < command.params.length; i++) { var param = command.params[i]; var assignment = new Assignment(param, i); - this._setAssignment(assignment, null, true); + this.setAssignment(assignment, null); assignment.onAssignmentChange.add(this._assignmentChanged, this); this._assignments[param.name] = assignment; } @@ -5678,27 +5678,23 @@ Requisition.prototype.getAssignments = function(includeCommand) { return assignments; }; -/** - * Alter the given assignment using the given arg. - * @param assignment The assignment to alter - * @param arg The new value for the assignment. An instance of Argument, or an - * instance of Conversion, or null to set the blank value. - */ -Requisition.prototype.setAssignment = function(assignment, arg) { - this._setAssignment(assignment, arg, false); -}; - /** * Internal function to alter the given assignment using the given arg. * @param assignment The assignment to alter * @param arg The new value for the assignment. An instance of Argument, or an * instance of Conversion, or null to set the blank value. - * @param skipArgUpdate (default=false) Adjusts the args in this requisition to - * keep things up to date. Args should only be skipped when setAssignment is - * being called as part of the update process. + * @param options There are a number of ways to customize how the assignment + * is made, including: + * - argUpdate: (default:false) Adjusts the args in this requisition to keep + * things up to date. Args should only be skipped when setAssignment is being + * called as part of the update process. + * - matchPadding: (default:false) If argUpdate=true, and matchPadding=true + * then further take the step of altering the whitespace on the prefix and + * suffix of the new argument to match that of the old argument. */ -Requisition.prototype._setAssignment = function(assignment, arg, skipArgUpdate) { - if (!skipArgUpdate) { +Requisition.prototype.setAssignment = function(assignment, arg, options) { + options = options || {}; + if (options.argUpdate) { var originalArgs = assignment.arg.getArgs(); // Update the args array @@ -5724,6 +5720,16 @@ Requisition.prototype._setAssignment = function(assignment, arg, skipArgUpdate) this._args.splice(index, 1); } else { + if (options.matchPadding) { + if (replacementArgs[i].prefix.length === 0 && + this._args[index].prefix.length !== 0) { + replacementArgs[i].prefix = this._args[index].prefix; + } + if (replacementArgs[i].suffix.length === 0 && + this._args[index].suffix.length !== 0) { + replacementArgs[i].suffix = this._args[index].suffix; + } + } this._args[index] = replacementArgs[i]; } } @@ -5761,7 +5767,7 @@ Requisition.prototype._setAssignment = function(assignment, arg, skipArgUpdate) */ Requisition.prototype.setBlankArguments = function() { this.getAssignments().forEach(function(assignment) { - this._setAssignment(assignment, null, true); + this.setAssignment(assignment, null); }, this); }; @@ -5801,13 +5807,13 @@ Requisition.prototype.complete = function(cursor, predictionChoice) { // logic, so we don't use addSpace if (assignment.isInName()) { var newArg = assignment.conversion.arg.beget({ prefixPostSpace: true }); - this.setAssignment(assignment, newArg); + this.setAssignment(assignment, newArg, { argUpdate: true }); } } else { // Mutate this argument to hold the completion var arg = assignment.arg.beget({ text: prediction.name }); - this.setAssignment(assignment, arg); + this.setAssignment(assignment, arg, { argUpdate: true }); if (!prediction.incomplete) { // The prediction is complete, add a space to let the user move-on @@ -5832,7 +5838,7 @@ Requisition.prototype.complete = function(cursor, predictionChoice) { Requisition.prototype._addSpace = function(assignment) { var arg = assignment.conversion.arg.beget({ suffixSpace: true }); if (arg !== assignment.conversion.arg) { - this.setAssignment(assignment, arg); + this.setAssignment(assignment, arg, { argUpdate: true }); } }; @@ -5844,7 +5850,7 @@ Requisition.prototype.decrement = function(assignment) { if (replacement != null) { var str = assignment.param.type.stringify(replacement); var arg = assignment.conversion.arg.beget({ text: str }); - this.setAssignment(assignment, arg); + this.setAssignment(assignment, arg, { argUpdate: true }); } }; @@ -5856,7 +5862,7 @@ Requisition.prototype.increment = function(assignment) { if (replacement != null) { var str = assignment.param.type.stringify(replacement); var arg = assignment.conversion.arg.beget({ text: str }); - this.setAssignment(assignment, arg); + this.setAssignment(assignment, arg, { argUpdate: true }); } }; @@ -6514,7 +6520,7 @@ Requisition.prototype._split = function(args) { // Special case: if the user enters { console.log('foo'); } then we need to // use the hidden 'eval' command conversion = new Conversion(evalCommand, new ScriptArgument()); - this._setAssignment(this.commandAssignment, conversion, true); + this.setAssignment(this.commandAssignment, conversion); return; } @@ -6541,7 +6547,7 @@ Requisition.prototype._split = function(args) { argsUsed++; } - this._setAssignment(this.commandAssignment, conversion, true); + this.setAssignment(this.commandAssignment, conversion); for (var i = 0; i < argsUsed; i++) { args.shift(); @@ -6588,7 +6594,7 @@ Requisition.prototype._assign = function(args) { var assignment = this.getAssignment(0); if (assignment.param.type instanceof StringType) { var arg = (args.length === 1) ? args[0] : new MergedArgument(args); - this._setAssignment(assignment, arg, true); + this.setAssignment(assignment, arg); return; } } @@ -6633,7 +6639,7 @@ Requisition.prototype._assign = function(args) { arrayArg.addArgument(arg); } else { - this._setAssignment(assignment, arg, true); + this.setAssignment(assignment, arg); } } else { @@ -6650,7 +6656,7 @@ Requisition.prototype._assign = function(args) { // If not set positionally, and we can't set it non-positionally, // we have to default it to prevent previous values surviving if (!assignment.param.isPositionalAllowed) { - this._setAssignment(assignment, null, true); + this.setAssignment(assignment, null); return; } @@ -6667,7 +6673,7 @@ Requisition.prototype._assign = function(args) { } else { if (args.length === 0) { - this._setAssignment(assignment, null, true); + this.setAssignment(assignment, null); } else { var arg = args.splice(0, 1)[0]; @@ -6681,7 +6687,7 @@ Requisition.prototype._assign = function(args) { this._unassigned.push(new UnassignedAssignment(this, arg)); } else { - this._setAssignment(assignment, arg, true); + this.setAssignment(assignment, arg); } } } @@ -6690,7 +6696,7 @@ Requisition.prototype._assign = function(args) { // Now we need to assign the array argument (if any) Object.keys(arrayArgs).forEach(function(name) { var assignment = this.getAssignment(name); - this._setAssignment(assignment, arrayArgs[name], true); + this.setAssignment(assignment, arrayArgs[name]); }, this); // What's left is can't be assigned, but we need to extract @@ -8066,8 +8072,10 @@ JavascriptField.prototype.setConversion = function(conversion) { }; JavascriptField.prototype.itemClicked = function(ev) { - this.onFieldChange(ev); - this.setMessage(ev.conversion.message); + var conversion = this.type.parse(ev.arg); + + this.onFieldChange({ conversion: conversion }); + this.setMessage(conversion.message); }; JavascriptField.prototype.onInputChange = function(ev) { @@ -8180,12 +8188,11 @@ Menu.prototype.destroy = function() { * @param ev The click event from the browser */ Menu.prototype.onItemClickInternal = function(ev) { - var name = ev.currentTarget.querySelector('.gcli-menu-name').innerHTML; + var name = ev.currentTarget.querySelector('.gcli-menu-name').textContent; var arg = new Argument(name); arg.suffix = ' '; - var conversion = this.type.parse(arg); - this.onItemClick({ conversion: conversion }); + this.onItemClick({ arg: arg }); }; /** @@ -8201,7 +8208,7 @@ Menu.prototype.show = function(items, match) { if (match) { this.items = this.items.map(function(item) { - return gethighlightingProxy(item, match, this.template.ownerDocument); + return getHighlightingProxy(item, match, this.template.ownerDocument); }.bind(this)); } @@ -8227,7 +8234,7 @@ Menu.prototype.show = function(items, match) { /** * Create a proxy around an item that highlights matching text */ -function gethighlightingProxy(item, match, document) { +function getHighlightingProxy(item, match, document) { if (typeof Proxy === 'undefined') { return item; } @@ -8289,12 +8296,12 @@ Menu.prototype.selectChoice = function() { return false; } - var name = selected.innerHTML; + var name = selected.textContent; var arg = new Argument(name); arg.suffix = ' '; + arg.prefix = ' '; - var conversion = this.type.parse(arg); - this.onItemClick({ conversion: conversion }); + this.onItemClick({ arg: arg }); return true; }; @@ -8498,8 +8505,10 @@ SelectionTooltipField.prototype.setConversion = function(conversion) { }; SelectionTooltipField.prototype.itemClicked = function(ev) { - this.onFieldChange(ev); - this.setMessage(ev.conversion.message); + var conversion = this.type.parse(ev.arg); + + this.onFieldChange({ conversion: conversion }); + this.setMessage(conversion.message); }; SelectionTooltipField.prototype.onInputChange = function(ev) { @@ -10340,7 +10349,8 @@ Tooltip.prototype.selectChoice = function(ev) { * Called by the onFieldChange event on the current Field */ Tooltip.prototype.fieldChanged = function(ev) { - this.requisition.setAssignment(this.assignment, ev.conversion.arg); + var options = { argUpdate: true, matchPadding: true }; + this.requisition.setAssignment(this.assignment, ev.conversion.arg, options); var isError = ev.conversion.message != null && ev.conversion.message !== ''; this.focusManager.setError(isError); From 3145b5fb4f61c889c102ec4d476bdecc3baf199a Mon Sep 17 00:00:00 2001 From: saran Date: Fri, 5 Oct 2012 00:46:38 +0100 Subject: [PATCH 07/18] Bug 768399 - GCLI output should not be forced to be xhtml; r=jwalker --- browser/devtools/commandline/gcli.jsm | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/browser/devtools/commandline/gcli.jsm b/browser/devtools/commandline/gcli.jsm index 60fa0ca32fe1..b222e7ba9227 100644 --- a/browser/devtools/commandline/gcli.jsm +++ b/browser/devtools/commandline/gcli.jsm @@ -3056,7 +3056,10 @@ exports.setContents = function(elem, contents) { return; } - if (exports.isXmlDocument(elem.ownerDocument)) { + if ('innerHTML' in elem) { + elem.innerHTML = contents; + } + else { try { var ns = elem.ownerDocument.documentElement.namespaceURI; if (!ns) { @@ -3076,9 +3079,6 @@ exports.setContents = function(elem, contents) { throw ex; } } - else { - elem.innerHTML = contents; - } }; /** From 9a493174391e245fb942ed5b4b3ee109e8a4b189 Mon Sep 17 00:00:00 2001 From: Paul Rouget Date: Thu, 4 Oct 2012 19:37:36 +0200 Subject: [PATCH 08/18] Bug 796029 - [markup panel] Consider disabling the preview if we can't solve bug 795956; r=dcamp --- browser/app/profile/firefox.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/browser/app/profile/firefox.js b/browser/app/profile/firefox.js index 45dff47da615..3b4984b69bce 100644 --- a/browser/app/profile/firefox.js +++ b/browser/app/profile/firefox.js @@ -1014,7 +1014,7 @@ pref("devtools.inspector.htmlHeight", 112); pref("devtools.inspector.htmlPanelOpen", false); pref("devtools.inspector.sidebarOpen", false); pref("devtools.inspector.activeSidebar", "ruleview"); -pref("devtools.inspector.markupPreview", true); +pref("devtools.inspector.markupPreview", false); // Enable the Layout View pref("devtools.layoutview.enabled", true); From 7df2b1ed8f67f8465fd72b7e68bf372aa14bc009 Mon Sep 17 00:00:00 2001 From: Paul Rouget Date: Fri, 5 Oct 2012 13:23:12 +0200 Subject: [PATCH 09/18] Bug 798284 - [responsive mode] dispatch some events; r=past --- browser/devtools/responsivedesign/responsivedesign.jsm | 10 ++++++++++ .../responsivedesign/test/browser_responsiveui.js | 10 +++++----- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/browser/devtools/responsivedesign/responsivedesign.jsm b/browser/devtools/responsivedesign/responsivedesign.jsm index c43a071594e4..d1bb7aec3262 100644 --- a/browser/devtools/responsivedesign/responsivedesign.jsm +++ b/browser/devtools/responsivedesign/responsivedesign.jsm @@ -10,6 +10,7 @@ const Cu = Components.utils; Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource:///modules/devtools/FloatingScrollbars.jsm"); +Cu.import("resource:///modules/devtools/EventEmitter.jsm"); var EXPORTED_SYMBOLS = ["ResponsiveUIManager"]; @@ -67,6 +68,13 @@ let ResponsiveUIManager = { default: } }, + + get events() { + if (!this._eventEmitter) { + this._eventEmitter = new EventEmitter(); + } + return this._eventEmitter; + }, } let presets = [ @@ -162,6 +170,7 @@ function ResponsiveUI(aWindow, aTab) } catch(e) {} switchToFloatingScrollbars(this.tab); + ResponsiveUIManager.events.emit("on", this.tab, this); } ResponsiveUI.prototype = { @@ -215,6 +224,7 @@ ResponsiveUI.prototype = { this.stack.removeAttribute("responsivemode"); delete this.tab.__responsiveUI; + ResponsiveUIManager.events.emit("off", this.tab, this); }, /** diff --git a/browser/devtools/responsivedesign/test/browser_responsiveui.js b/browser/devtools/responsivedesign/test/browser_responsiveui.js index 071f32d34bb8..bae3732f2a75 100644 --- a/browser/devtools/responsivedesign/test/browser_responsiveui.js +++ b/browser/devtools/responsivedesign/test/browser_responsiveui.js @@ -3,6 +3,7 @@ function test() { let instance, widthBeforeClose, heightBeforeClose; + let events = ResponsiveUI.ResponsiveUIManager.events; waitForExplicitFinish(); @@ -16,8 +17,8 @@ function test() { function startTest() { document.getElementById("Tools:ResponsiveUI").removeAttribute("disabled"); + events.once("on", function() {executeSoon(onUIOpen)}); synthesizeKeyFromKeyTag("key_responsiveUI"); - executeSoon(onUIOpen); } function onUIOpen() { @@ -118,14 +119,13 @@ function test() { widthBeforeClose = content.innerWidth; heightBeforeClose = content.innerHeight; + events.once("off", function() {executeSoon(restart)}); EventUtils.synthesizeKey("VK_ESCAPE", {}); - - executeSoon(restart); } function restart() { + events.once("on", function() {executeSoon(onUIOpen2)}); synthesizeKeyFromKeyTag("key_responsiveUI"); - executeSoon(onUIOpen2); } function onUIOpen2() { @@ -138,8 +138,8 @@ function test() { is(content.innerWidth, widthBeforeClose, "width restored."); is(content.innerHeight, heightBeforeClose, "height restored."); + events.once("off", function() {executeSoon(finishUp)}); EventUtils.synthesizeKey("VK_ESCAPE", {}); - executeSoon(finishUp); } function finishUp() { From 429c694e6d6bf82987b7d2fdb1c1988f7db5330b Mon Sep 17 00:00:00 2001 From: Girish Sharma Date: Thu, 4 Oct 2012 10:24:46 +0530 Subject: [PATCH 10/18] Bug 790272 - Unnecessary repaints of the Layout View, r=paul, r=jaws --- browser/devtools/layoutview/LayoutView.jsm | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/browser/devtools/layoutview/LayoutView.jsm b/browser/devtools/layoutview/LayoutView.jsm index 189b74c170b8..600cff785496 100644 --- a/browser/devtools/layoutview/LayoutView.jsm +++ b/browser/devtools/layoutview/LayoutView.jsm @@ -129,6 +129,8 @@ LayoutView.prototype = { this.iframe.removeEventListener("keypress", this.bound_handleKeypress, true); this.inspector.chromeWindow.removeEventListener("message", this.onMessage, true); this.close(); + this.sizeHeadingLabel = null; + this.sizeLabel = null; this.iframe = null; this.view.parentNode.removeChild(this.view); }, @@ -159,6 +161,10 @@ LayoutView.prototype = { this.documentReady = true; this.doc = this.iframe.contentDocument; + // Save reference to the labels displaying size of the node. + this.sizeLabel = this.doc.querySelector(".size > span"); + this.sizeHeadingLabel = this.doc.getElementById("element-size"); + // We can't do that earlier because open() and close() need to do stuff // inside the iframe. @@ -299,10 +305,9 @@ LayoutView.prototype = { let width = Math.round(clientRect.width); let height = Math.round(clientRect.height); - let elt = this.doc.querySelector("#element-size"); let newLabel = width + "x" + height; - if (elt.textContent != newLabel) { - elt.textContent = newLabel; + if (this.sizeHeadingLabel.textContent != newLabel) { + this.sizeHeadingLabel.textContent = newLabel; } // If the view is closed, no need to do anything more. @@ -312,7 +317,6 @@ LayoutView.prototype = { let style = this.browser.contentWindow.getComputedStyle(node);; for (let i in this.map) { - let selector = this.map[i].selector; let property = this.map[i].property; this.map[i].value = parseInt(style.getPropertyValue(property)); } @@ -326,6 +330,10 @@ LayoutView.prototype = { for (let i in this.map) { let selector = this.map[i].selector; let span = this.doc.querySelector(selector); + if (span.textContent.length > 0 && + span.textContent == this.map[i].value) { + continue; + } span.textContent = this.map[i].value; } @@ -335,7 +343,10 @@ LayoutView.prototype = { height -= this.map.borderTop.value + this.map.borderBottom.value + this.map.paddingTop.value + this.map.paddingBottom.value; - this.doc.querySelector(".size > span").textContent = width + "x" + height; + let newValue = width + "x" + height; + if (this.sizeLabel.textContent != newValue) { + this.sizeLabel.textContent = newValue; + } }, /** From 8d100f55769f22c9d65d4b30457f0162e1ee4257 Mon Sep 17 00:00:00 2001 From: Panos Astithas Date: Mon, 24 Sep 2012 12:43:26 +0100 Subject: [PATCH 11/18] Bug 792925 - Dynamically-added tab-scoped actors should get a reference to their parent; r=msucan --- toolkit/devtools/debugger/server/dbg-browser-actors.js | 2 +- toolkit/devtools/debugger/server/dbg-server.js | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/toolkit/devtools/debugger/server/dbg-browser-actors.js b/toolkit/devtools/debugger/server/dbg-browser-actors.js index 3d2059df3d32..21ab7b1bceee 100644 --- a/toolkit/devtools/debugger/server/dbg-browser-actors.js +++ b/toolkit/devtools/debugger/server/dbg-browser-actors.js @@ -141,7 +141,7 @@ BrowserRootActor.prototype = { for (let name in aFactories) { let actor = this._extraActors[name]; if (!actor) { - actor = aFactories[name].bind(null, this.conn); + actor = aFactories[name].bind(null, this.conn, this); actor.prototype = aFactories[name].prototype; actor.parentID = this.actorID; this._extraActors[name] = actor; diff --git a/toolkit/devtools/debugger/server/dbg-server.js b/toolkit/devtools/debugger/server/dbg-server.js index 4ff6acda6cf0..70a6ec37a3b7 100644 --- a/toolkit/devtools/debugger/server/dbg-server.js +++ b/toolkit/devtools/debugger/server/dbg-server.js @@ -535,6 +535,7 @@ DebuggerServerConnection.prototype = { "': " + safeErrorString(e)) }); } + instance.parentID = actor.parentID; // We want the newly-constructed actor to completely replace the factory // actor. Reusing the existing actor ID will make sure ActorPool.addActor // does the right thing. From e37bda789babadb6d1a10f9cbedf933eff9891c0 Mon Sep 17 00:00:00 2001 From: Mihai Sucan Date: Wed, 26 Sep 2012 18:07:57 +0100 Subject: [PATCH 12/18] Bug 768096 - Web Console remote debugging protocol support - Part 1: page errors; r=past,robcee --HG-- rename : browser/devtools/webconsole/WebConsoleUtils.jsm => toolkit/devtools/webconsole/WebConsoleUtils.jsm --- .../devtools/webconsole/HUDService-content.js | 7 +- browser/devtools/webconsole/HUDService.jsm | 79 +++--- browser/devtools/webconsole/Makefile.in | 1 - browser/devtools/webconsole/NetworkPanel.jsm | 7 +- browser/devtools/webconsole/PropertyPanel.jsm | 2 +- ...er_webconsole_bug_580454_timestamp_l10n.js | 2 +- ...owser_webconsole_bug_587617_output_copy.js | 2 +- ...eactivateHUDForContext_unfocused_window.js | 52 +++- ...r_webconsole_bug_601909_remember_height.js | 32 ++- ...owser_webconsole_bug_613280_jsterm_copy.js | 2 +- ...webconsole_bug_626484_output_copy_order.js | 2 +- ...console_bug_632347_iterators_generators.js | 2 +- ...e_bug_651501_document_body_autocomplete.js | 2 +- .../test/browser_webconsole_chrome.js | 4 +- ...le_messages_inserts_newlines_in_between.js | 2 +- .../test/browser_webconsole_network_panel.js | 7 +- .../browser_webconsole_property_provider.js | 2 +- browser/devtools/webconsole/test/head.js | 6 +- browser/devtools/webconsole/webconsole.js | 261 +++++++++++++++++- toolkit/devtools/Makefile.in | 1 + toolkit/devtools/debugger/dbg-client.jsm | 68 ++++- .../devtools/debugger/server/dbg-server.js | 2 + toolkit/devtools/jar.mn | 1 + toolkit/devtools/webconsole/Makefile.in | 17 ++ .../devtools/webconsole/WebConsoleClient.jsm | 103 +++++++ .../devtools/webconsole/WebConsoleUtils.jsm | 167 ++++++++++- .../webconsole/dbg-webconsole-actors.js | 193 +++++++++++++ 27 files changed, 911 insertions(+), 115 deletions(-) create mode 100644 toolkit/devtools/webconsole/Makefile.in create mode 100644 toolkit/devtools/webconsole/WebConsoleClient.jsm rename {browser => toolkit}/devtools/webconsole/WebConsoleUtils.jsm (85%) create mode 100644 toolkit/devtools/webconsole/dbg-webconsole-actors.js diff --git a/browser/devtools/webconsole/HUDService-content.js b/browser/devtools/webconsole/HUDService-content.js index 72c377c976df..05bb00241cd3 100644 --- a/browser/devtools/webconsole/HUDService-content.js +++ b/browser/devtools/webconsole/HUDService-content.js @@ -11,12 +11,13 @@ let Cc = Components.classes; let Ci = Components.interfaces; let Cu = Components.utils; +const STRINGS_URI = "chrome://browser/locale/devtools/webconsole.properties"; let tempScope = {}; Cu.import("resource://gre/modules/XPCOMUtils.jsm", tempScope); Cu.import("resource://gre/modules/Services.jsm", tempScope); Cu.import("resource://gre/modules/ConsoleAPIStorage.jsm", tempScope); -Cu.import("resource:///modules/WebConsoleUtils.jsm", tempScope); +Cu.import("resource://gre/modules/devtools/WebConsoleUtils.jsm", tempScope); Cu.import("resource:///modules/NetworkHelper.jsm", tempScope); Cu.import("resource://gre/modules/NetUtil.jsm", tempScope); @@ -24,7 +25,7 @@ let XPCOMUtils = tempScope.XPCOMUtils; let Services = tempScope.Services; let gConsoleStorage = tempScope.ConsoleAPIStorage; let WebConsoleUtils = tempScope.WebConsoleUtils; -let l10n = WebConsoleUtils.l10n; +let l10n = new WebConsoleUtils.l10n(STRINGS_URI); let JSPropertyProvider = tempScope.JSPropertyProvider; let NetworkHelper = tempScope.NetworkHelper; let NetUtil = tempScope.NetUtil; @@ -185,6 +186,8 @@ let Manager = { if (aMessage.cachedMessages) { this._sendCachedMessages(aMessage.cachedMessages); } + + this.sendMessage("WebConsole:Initialized", {}); }, /** diff --git a/browser/devtools/webconsole/HUDService.jsm b/browser/devtools/webconsole/HUDService.jsm index fa15e1a9fe97..0762942d6a9e 100644 --- a/browser/devtools/webconsole/HUDService.jsm +++ b/browser/devtools/webconsole/HUDService.jsm @@ -18,12 +18,10 @@ XPCOMUtils.defineLazyModuleGetter(this, "Services", "resource://gre/modules/Services.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "WebConsoleUtils", - "resource:///modules/WebConsoleUtils.jsm"); - -XPCOMUtils.defineLazyGetter(this, "l10n", function() { - return WebConsoleUtils.l10n; -}); + "resource://gre/modules/devtools/WebConsoleUtils.jsm"); +const STRINGS_URI = "chrome://browser/locale/devtools/webconsole.properties"; +let l10n = new WebConsoleUtils.l10n(STRINGS_URI); var EXPORTED_SYMBOLS = ["HUDService"]; @@ -172,7 +170,10 @@ HUD_SERVICE.prototype = let hud = this.getHudReferenceById(hudId); let document = hud.chromeDocument; - hud.destroy(); + hud.destroy(function() { + let id = WebConsoleUtils.supportsString(hudId); + Services.obs.notifyObservers(id, "web-console-destroyed", null); + }); delete this.hudReferences[hudId]; @@ -199,9 +200,6 @@ HUD_SERVICE.prototype = contentWindow.focus(); HeadsUpDisplayUICommands.refreshCommand(); - - let id = WebConsoleUtils.supportsString(hudId); - Services.obs.notifyObservers(id, "web-console-destroyed", null); }, /** @@ -539,7 +537,7 @@ WebConsole.prototype = { * @type array */ _messageListeners: ["JSTerm:EvalObject", "WebConsole:ConsoleAPI", - "WebConsole:CachedMessages", "WebConsole:PageError", "JSTerm:EvalResult", + "WebConsole:CachedMessages", "WebConsole:Initialized", "JSTerm:EvalResult", "JSTerm:AutocompleteProperties", "JSTerm:ClearOutput", "JSTerm:InspectObject", "WebConsole:NetworkActivity", "WebConsole:FileActivity", "WebConsole:LocationChange", @@ -926,8 +924,7 @@ WebConsole.prototype = { }, this); let message = { - features: ["ConsoleAPI", "JSTerm", "PageError", "NetworkMonitor", - "LocationChange"], + features: ["ConsoleAPI", "JSTerm", "NetworkMonitor", "LocationChange"], cachedMessages: ["ConsoleAPI", "PageError"], NetworkMonitor: { monitorFileActivity: true }, JSTerm: { notifyNonNativeConsoleAPI: true }, @@ -940,19 +937,6 @@ WebConsole.prototype = { this.sendMessageToContent("WebConsole:Init", message); }, - /** - * Callback method for when the Web Console initialization is complete. For - * now this method sends the web-console-created notification using the - * nsIObserverService. - * - * @private - */ - _onInitComplete: function WC__onInitComplete() - { - let id = WebConsoleUtils.supportsString(this.hudId); - Services.obs.notifyObservers(id, "web-console-created", null); - }, - /** * Handler for messages that have an associated callback function. The * this.sendMessageToContent() allows one to provide a function to be invoked @@ -1051,8 +1035,12 @@ WebConsole.prototype = { /** * Destroy the object. Call this method to avoid memory leaks when the Web * Console is closed. + * + * @param function [aOnDestroy] + * Optional function to invoke when the Web Console instance is + * destroyed. */ - destroy: function WC_destroy() + destroy: function WC_destroy(aOnDestroy) { this.sendMessageToContent("WebConsole:Destroy", {}); @@ -1072,24 +1060,31 @@ WebConsole.prototype = { } } + let onDestroy = function WC_onDestroyUI() { + // Remove the iframe and the consolePanel if the Web Console is inside a + // floating panel. + if (this.consolePanel && this.consolePanel.parentNode) { + this.consolePanel.hidePopup(); + this.consolePanel.parentNode.removeChild(this.consolePanel); + this.consolePanel = null; + } + + if (this.iframe.parentNode) { + this.iframe.parentNode.removeChild(this.iframe); + } + + if (this.splitter.parentNode) { + this.splitter.parentNode.removeChild(this.splitter); + } + + aOnDestroy && aOnDestroy(); + }.bind(this); + if (this.ui) { - this.ui.destroy(); + this.ui.destroy(onDestroy); } - - // Remove the iframe and the consolePanel if the Web Console is inside a - // floating panel. - if (this.consolePanel && this.consolePanel.parentNode) { - this.consolePanel.hidePopup(); - this.consolePanel.parentNode.removeChild(this.consolePanel); - this.consolePanel = null; - } - - if (this.iframe.parentNode) { - this.iframe.parentNode.removeChild(this.iframe); - } - - if (this.splitter.parentNode) { - this.splitter.parentNode.removeChild(this.splitter); + else { + onDestroy(); } }, }; diff --git a/browser/devtools/webconsole/Makefile.in b/browser/devtools/webconsole/Makefile.in index d8ae9a8729da..2fa5c1f0962c 100644 --- a/browser/devtools/webconsole/Makefile.in +++ b/browser/devtools/webconsole/Makefile.in @@ -16,7 +16,6 @@ EXTRA_JS_MODULES = \ NetworkHelper.jsm \ NetworkPanel.jsm \ AutocompletePopup.jsm \ - WebConsoleUtils.jsm \ $(NULL) TEST_DIRS = test diff --git a/browser/devtools/webconsole/NetworkPanel.jsm b/browser/devtools/webconsole/NetworkPanel.jsm index b3a6bcd893b9..91ce3fff4fe8 100644 --- a/browser/devtools/webconsole/NetworkPanel.jsm +++ b/browser/devtools/webconsole/NetworkPanel.jsm @@ -22,11 +22,10 @@ XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", "resource://gre/modules/NetUtil.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "WebConsoleUtils", - "resource:///modules/WebConsoleUtils.jsm"); + "resource://gre/modules/devtools/WebConsoleUtils.jsm"); -XPCOMUtils.defineLazyGetter(this, "l10n", function() { - return WebConsoleUtils.l10n; -}); +const STRINGS_URI = "chrome://browser/locale/devtools/webconsole.properties"; +let l10n = new WebConsoleUtils.l10n(STRINGS_URI); var EXPORTED_SYMBOLS = ["NetworkPanel"]; diff --git a/browser/devtools/webconsole/PropertyPanel.jsm b/browser/devtools/webconsole/PropertyPanel.jsm index 2403ef8de5a3..6fcbdd124d39 100644 --- a/browser/devtools/webconsole/PropertyPanel.jsm +++ b/browser/devtools/webconsole/PropertyPanel.jsm @@ -13,7 +13,7 @@ const Cu = Components.utils; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "WebConsoleUtils", - "resource:///modules/WebConsoleUtils.jsm"); + "resource://gre/modules/devtools/WebConsoleUtils.jsm"); var EXPORTED_SYMBOLS = ["PropertyPanel", "PropertyTreeView"]; diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_580454_timestamp_l10n.js b/browser/devtools/webconsole/test/browser_webconsole_bug_580454_timestamp_l10n.js index 1b5faa3e055d..d1c9487c31b1 100644 --- a/browser/devtools/webconsole/test/browser_webconsole_bug_580454_timestamp_l10n.js +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_580454_timestamp_l10n.js @@ -21,7 +21,7 @@ function test() { browser.removeEventListener("DOMContentLoaded", testTimestamp, false); const TEST_TIMESTAMP = 12345678; let date = new Date(TEST_TIMESTAMP); - let localizedString = WebConsoleUtils.l10n.timestampString(TEST_TIMESTAMP); + let localizedString = WCU_l10n.timestampString(TEST_TIMESTAMP); isnot(localizedString.indexOf(date.getHours()), -1, "the localized " + "timestamp contains the hours"); isnot(localizedString.indexOf(date.getMinutes()), -1, "the localized " + diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_587617_output_copy.js b/browser/devtools/webconsole/test/browser_webconsole_bug_587617_output_copy.js index 03d8073d6258..8ad4a4b70c47 100644 --- a/browser/devtools/webconsole/test/browser_webconsole_bug_587617_output_copy.js +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_587617_output_copy.js @@ -81,7 +81,7 @@ function testContextMenuCopy() { } function getExpectedClipboardText(aItem) { - return "[" + WebConsoleUtils.l10n.timestampString(aItem.timestamp) + "] " + + return "[" + WCU_l10n.timestampString(aItem.timestamp) + "] " + aItem.clipboardText; } diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_597103_deactivateHUDForContext_unfocused_window.js b/browser/devtools/webconsole/test/browser_webconsole_bug_597103_deactivateHUDForContext_unfocused_window.js index 4fc5d27a6852..f1e40dfaa3cc 100644 --- a/browser/devtools/webconsole/test/browser_webconsole_bug_597103_deactivateHUDForContext_unfocused_window.js +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_597103_deactivateHUDForContext_unfocused_window.js @@ -14,25 +14,35 @@ let tab1, tab2, win1, win2; let noErrors = true; function tab1Loaded(aEvent) { - browser.removeEventListener(aEvent.type, arguments.callee, true); + browser.removeEventListener(aEvent.type, tab1Loaded, true); win2 = OpenBrowserWindow(); win2.addEventListener("load", win2Loaded, true); } function win2Loaded(aEvent) { - win2.removeEventListener(aEvent.type, arguments.callee, true); + win2.removeEventListener(aEvent.type, win2Loaded, true); - tab2 = win2.gBrowser.addTab(); + tab2 = win2.gBrowser.addTab(TEST_URI); win2.gBrowser.selectedTab = tab2; tab2.linkedBrowser.addEventListener("load", tab2Loaded, true); - tab2.linkedBrowser.contentWindow.location = TEST_URI; } function tab2Loaded(aEvent) { - tab2.linkedBrowser.removeEventListener(aEvent.type, arguments.callee, true); + tab2.linkedBrowser.removeEventListener(aEvent.type, tab2Loaded, true); - waitForFocus(function() { + let consolesOpened = 0; + function onWebConsoleOpen() { + consolesOpened++; + if (consolesOpened == 2) { + Services.obs.removeObserver(onWebConsoleOpen, "web-console-created"); + executeSoon(closeConsoles); + } + } + + Services.obs.addObserver(onWebConsoleOpen, "web-console-created", false); + + function openConsoles() { try { HUDService.activateHUDForContext(tab1); } @@ -48,6 +58,20 @@ function tab2Loaded(aEvent) { ok(false, "HUDService.activateHUDForContext(tab2) exception: " + ex); noErrors = false; } + } + + let consolesClosed = 0; + function onWebConsoleClose() + { + consolesClosed++; + if (consolesClosed == 2) { + Services.obs.removeObserver(onWebConsoleClose, "web-console-destroyed"); + executeSoon(testEnd); + } + } + + function closeConsoles() { + Services.obs.addObserver(onWebConsoleClose, "web-console-destroyed", false); try { HUDService.deactivateHUDForContext(tab1); @@ -64,20 +88,26 @@ function tab2Loaded(aEvent) { ok(false, "HUDService.deactivateHUDForContext(tab2) exception: " + ex); noErrors = false; } + } - if (noErrors) { - ok(true, "there were no errors"); - } + function testEnd() { + ok(noErrors, "there were no errors"); - win2.gBrowser.removeTab(tab2); + Array.forEach(win1.gBrowser.tabs, function(aTab) { + win1.gBrowser.removeTab(aTab); + }); + Array.forEach(win2.gBrowser.tabs, function(aTab) { + win2.gBrowser.removeTab(aTab); + }); executeSoon(function() { win2.close(); tab1 = tab2 = win1 = win2 = null; finishTest(); }); + } - }, tab2.linkedBrowser.contentWindow); + waitForFocus(openConsoles, tab2.linkedBrowser.contentWindow); } function test() { diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_601909_remember_height.js b/browser/devtools/webconsole/test/browser_webconsole_bug_601909_remember_height.js index e2e028f9e52c..de8649179159 100644 --- a/browser/devtools/webconsole/test/browser_webconsole_bug_601909_remember_height.js +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_601909_remember_height.js @@ -16,14 +16,14 @@ const MINIMUM_CONSOLE_HEIGHT = 150; const MINIMUM_PAGE_HEIGHT = 50; const HEIGHT_PREF = "devtools.hud.height"; -let hud, newHeight, height, innerHeight; +let hud, newHeight, height, innerHeight, testDriver; -function performTests(aWebConsole) +function testGen() { - hud = aWebConsole.iframe; height = parseInt(hud.style.height); toggleConsole(); + yield; is(newHeight, height, "same height after reopening the console"); is(Services.prefs.getIntPref(HEIGHT_PREF), HUDService.lastConsoleHeight, @@ -31,6 +31,7 @@ function performTests(aWebConsole) setHeight(Math.ceil(innerHeight * 0.5)); toggleConsole(); + yield; is(newHeight, height, "same height after reopening the console"); is(Services.prefs.getIntPref(HEIGHT_PREF), HUDService.lastConsoleHeight, @@ -38,6 +39,7 @@ function performTests(aWebConsole) setHeight(MINIMUM_CONSOLE_HEIGHT - 1); toggleConsole(); + yield; is(newHeight, MINIMUM_CONSOLE_HEIGHT, "minimum console height is respected"); is(Services.prefs.getIntPref(HEIGHT_PREF), HUDService.lastConsoleHeight, @@ -45,6 +47,7 @@ function performTests(aWebConsole) setHeight(innerHeight - MINIMUM_PAGE_HEIGHT + 1); toggleConsole(); + yield; is(newHeight, innerHeight - MINIMUM_PAGE_HEIGHT, "minimum page height is respected"); @@ -54,6 +57,7 @@ function performTests(aWebConsole) setHeight(Math.ceil(innerHeight * 0.6)); Services.prefs.setIntPref(HEIGHT_PREF, -1); toggleConsole(); + yield; is(newHeight, height, "same height after reopening the console"); is(Services.prefs.getIntPref(HEIGHT_PREF), -1, "pref is not updated"); @@ -62,17 +66,23 @@ function performTests(aWebConsole) HUDService.lastConsoleHeight = 0; Services.prefs.setIntPref(HEIGHT_PREF, 0); + hud = testDriver = null; executeSoon(finishTest); + + yield; } function toggleConsole() { - closeConsole(); - openConsole(); + closeConsole(null, function() { + openConsole(null, function() { + let hudId = HUDService.getHudIdByWindow(content); + hud = HUDService.hudReferences[hudId].iframe; + newHeight = parseInt(hud.style.height); - let hudId = HUDService.getHudIdByWindow(content); - hud = HUDService.hudReferences[hudId].iframe; - newHeight = parseInt(hud.style.height); + testDriver.next(); + }); + }); } function setHeight(aHeight) @@ -87,7 +97,11 @@ function test() browser.addEventListener("load", function onLoad() { browser.removeEventListener("load", onLoad, true); innerHeight = content.innerHeight; - openConsole(null, performTests); + openConsole(null, function(aHud) { + hud = aHud.iframe; + testDriver = testGen(); + testDriver.next(); + }); }, true); } diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_613280_jsterm_copy.js b/browser/devtools/webconsole/test/browser_webconsole_bug_613280_jsterm_copy.js index 98bdd0daa781..8748b0aa7427 100644 --- a/browser/devtools/webconsole/test/browser_webconsole_bug_613280_jsterm_copy.js +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_613280_jsterm_copy.js @@ -80,6 +80,6 @@ function performTest(HUD) { } function getExpectedClipboardText(aItem) { - return "[" + WebConsoleUtils.l10n.timestampString(aItem.timestamp) + "] " + + return "[" + WCU_l10n.timestampString(aItem.timestamp) + "] " + aItem.clipboardText; } diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_626484_output_copy_order.js b/browser/devtools/webconsole/test/browser_webconsole_bug_626484_output_copy_order.js index 52badb2e3189..5b519392253d 100644 --- a/browser/devtools/webconsole/test/browser_webconsole_bug_626484_output_copy_order.js +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_626484_output_copy_order.js @@ -58,7 +58,7 @@ function getExpectedClipboardText(aItemCount) { for (let i = 0; i < aItemCount; i++) { let item = outputNode.getItemAtIndex(i); expectedClipboardText.push("[" + - WebConsoleUtils.l10n.timestampString(item.timestamp) + "] " + + WCU_l10n.timestampString(item.timestamp) + "] " + item.clipboardText); } return expectedClipboardText.join("\n"); diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_632347_iterators_generators.js b/browser/devtools/webconsole/test/browser_webconsole_bug_632347_iterators_generators.js index c92d9b64542e..589f7aff57e3 100644 --- a/browser/devtools/webconsole/test/browser_webconsole_bug_632347_iterators_generators.js +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_632347_iterators_generators.js @@ -15,7 +15,7 @@ function test() { function consoleOpened(HUD) { let tmp = {}; - Cu.import("resource:///modules/WebConsoleUtils.jsm", tmp); + Cu.import("resource://gre/modules/devtools/WebConsoleUtils.jsm", tmp); let WCU = tmp.WebConsoleUtils; let JSPropertyProvider = tmp.JSPropertyProvider; tmp = null; diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_651501_document_body_autocomplete.js b/browser/devtools/webconsole/test/browser_webconsole_bug_651501_document_body_autocomplete.js index 0e648d0dfd53..76a99fa9325f 100644 --- a/browser/devtools/webconsole/test/browser_webconsole_bug_651501_document_body_autocomplete.js +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_651501_document_body_autocomplete.js @@ -23,7 +23,7 @@ function consoleOpened(aHud) { let completeNode = jsterm.completeNode; let tmp = {}; - Cu.import("resource:///modules/WebConsoleUtils.jsm", tmp); + Cu.import("resource://gre/modules/devtools/WebConsoleUtils.jsm", tmp); let WCU = tmp.WebConsoleUtils; tmp = null; diff --git a/browser/devtools/webconsole/test/browser_webconsole_chrome.js b/browser/devtools/webconsole/test/browser_webconsole_chrome.js index 0504ecfd2ddc..8e3bf03056aa 100644 --- a/browser/devtools/webconsole/test/browser_webconsole_chrome.js +++ b/browser/devtools/webconsole/test/browser_webconsole_chrome.js @@ -6,7 +6,7 @@ // Tests that code completion works properly. function test() { - addTab("about:addons"); + addTab("about:credits"); browser.addEventListener("load", function onLoad() { browser.removeEventListener("load", onLoad, true); openConsole(null, testChrome); @@ -15,7 +15,7 @@ function test() { function testChrome(hud) { ok(hud, "we have a console"); - + ok(hud.iframe, "we have the console iframe"); let jsterm = hud.jsterm; diff --git a/browser/devtools/webconsole/test/browser_webconsole_copying_multiple_messages_inserts_newlines_in_between.js b/browser/devtools/webconsole/test/browser_webconsole_copying_multiple_messages_inserts_newlines_in_between.js index ed1e4bdcbdf4..33ad67819a7a 100644 --- a/browser/devtools/webconsole/test/browser_webconsole_copying_multiple_messages_inserts_newlines_in_between.js +++ b/browser/devtools/webconsole/test/browser_webconsole_copying_multiple_messages_inserts_newlines_in_between.js @@ -52,7 +52,7 @@ function testClipboard() { for (let i = 0; i < outputNode.itemCount; i++) { let item = outputNode.getItemAtIndex(i); clipboardTexts.push("[" + - WebConsoleUtils.l10n.timestampString(item.timestamp) + + WCU_l10n.timestampString(item.timestamp) + "] " + item.clipboardText); } diff --git a/browser/devtools/webconsole/test/browser_webconsole_network_panel.js b/browser/devtools/webconsole/test/browser_webconsole_network_panel.js index 0a6f3b5726b6..d0c1a5b37f87 100644 --- a/browser/devtools/webconsole/test/browser_webconsole_network_panel.js +++ b/browser/devtools/webconsole/test/browser_webconsole_network_panel.js @@ -64,11 +64,6 @@ function testGen() { let hud = HUDService.getHudByWindow(content); let filterBox = hud.ui.filterBox; - let tempScope = {}; - Cu.import("resource:///modules/WebConsoleUtils.jsm", tempScope); - let l10n = tempScope.WebConsoleUtils.l10n; - tempScope = null; - let httpActivity = { meta: { stages: [], @@ -442,7 +437,7 @@ function testGen() { }); let responseString = - l10n.getFormatStr("NetworkPanel.responseBodyUnableToDisplay.content", + WCU_l10n.getFormatStr("NetworkPanel.responseBodyUnableToDisplay.content", ["application/x-shockwave-flash"]); checkNodeContent(networkPanel, "responseBodyUnknownTypeContent", responseString); networkPanel.panel.hidePopup(); diff --git a/browser/devtools/webconsole/test/browser_webconsole_property_provider.js b/browser/devtools/webconsole/test/browser_webconsole_property_provider.js index 7a98f0dcd119..9d36a98ba9ae 100644 --- a/browser/devtools/webconsole/test/browser_webconsole_property_provider.js +++ b/browser/devtools/webconsole/test/browser_webconsole_property_provider.js @@ -17,7 +17,7 @@ function testPropertyProvider() { browser.removeEventListener("load", testPropertyProvider, true); let tmp = {}; - Cu.import("resource:///modules/WebConsoleUtils.jsm", tmp); + Cu.import("resource://gre/modules/devtools/WebConsoleUtils.jsm", tmp); let JSPropertyProvider = tmp.JSPropertyProvider; tmp = null; diff --git a/browser/devtools/webconsole/test/head.js b/browser/devtools/webconsole/test/head.js index 8184a1798d7d..de8ed98423f3 100644 --- a/browser/devtools/webconsole/test/head.js +++ b/browser/devtools/webconsole/test/head.js @@ -6,8 +6,10 @@ let tempScope = {}; Cu.import("resource:///modules/HUDService.jsm", tempScope); let HUDService = tempScope.HUDService; -Cu.import("resource:///modules/WebConsoleUtils.jsm", tempScope); +Cu.import("resource://gre/modules/devtools/WebConsoleUtils.jsm", tempScope); let WebConsoleUtils = tempScope.WebConsoleUtils; +const WEBCONSOLE_STRINGS_URI = "chrome://browser/locale/devtools/webconsole.properties"; +let WCU_l10n = new WebConsoleUtils.l10n(WEBCONSOLE_STRINGS_URI); function log(aMsg) { @@ -252,7 +254,7 @@ function tearDown() while (gBrowser.tabs.length > 1) { gBrowser.removeCurrentTab(); } - tab = browser = hudId = hud = filterBox = outputNode = cs = null; + WCU_l10n = tab = browser = hudId = hud = filterBox = outputNode = cs = null; } registerCleanupFunction(tearDown); diff --git a/browser/devtools/webconsole/webconsole.js b/browser/devtools/webconsole/webconsole.js index 2b26fb2036c5..b034661fb824 100644 --- a/browser/devtools/webconsole/webconsole.js +++ b/browser/devtools/webconsole/webconsole.js @@ -15,6 +15,12 @@ Cu.import("resource://gre/modules/XPCOMUtils.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "Services", "resource://gre/modules/Services.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "DebuggerServer", + "resource://gre/modules/devtools/dbg-server.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "DebuggerClient", + "resource://gre/modules/devtools/dbg-client.jsm"); + XPCOMUtils.defineLazyServiceGetter(this, "clipboardHelper", "@mozilla.org/widget/clipboardhelper;1", "nsIClipboardHelper"); @@ -32,11 +38,10 @@ XPCOMUtils.defineLazyModuleGetter(this, "AutocompletePopup", "resource:///modules/AutocompletePopup.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "WebConsoleUtils", - "resource:///modules/WebConsoleUtils.jsm"); + "resource://gre/modules/devtools/WebConsoleUtils.jsm"); -XPCOMUtils.defineLazyGetter(this, "l10n", function() { - return WebConsoleUtils.l10n; -}); +const STRINGS_URI = "chrome://browser/locale/devtools/webconsole.properties"; +let l10n = new WebConsoleUtils.l10n(STRINGS_URI); // The XUL namespace. @@ -187,6 +192,8 @@ function WebConsoleFrame(aWebConsoleOwner, aPosition) this.jsterm = new JSTerm(this); this.jsterm.inputNode.focus(); + + this._initConnection(); } WebConsoleFrame.prototype = { @@ -197,6 +204,23 @@ WebConsoleFrame.prototype = { */ owner: null, + /** + * Proxy between the Web Console and the remote Web Console instance. This + * object holds methods used for connecting, listening and disconnecting from + * the remote server, using the remote debugging protocol. + * + * @see WebConsoleConnectionProxy + * @type object + */ + proxy: null, + + /** + * Tells if the Web Console initialization via message manager completed. + * @private + * @type boolean + */ + _messageManagerInitComplete: false, + /** * Getter for the xul:popupset that holds any popups we open. * @type nsIDOMElement @@ -311,6 +335,21 @@ WebConsoleFrame.prototype = { this.owner.sendMessageToContent("WebConsole:SetPreferences", message); }, + /** + * Connect to the server using the remote debugging protocol. + * @private + */ + _initConnection: function WCF__initConnection() + { + this.proxy = new WebConsoleConnectionProxy(this); + this.proxy.initServer(); + this.proxy.connect(function() { + if (this._messageManagerInitComplete) { + this._onInitComplete(); + } + }.bind(this)); + }, + /** * Find the Web Console UI elements and setup event listeners as needed. * @private @@ -486,6 +525,19 @@ WebConsoleFrame.prototype = { }, this); }, + /** + * Callback method for when the Web Console initialization is complete. For + * now this method sends the web-console-created notification using the + * nsIObserverService. + * + * @private + */ + _onInitComplete: function WC__onInitComplete() + { + let id = WebConsoleUtils.supportsString(this.hudId); + Services.obs.notifyObservers(id, "web-console-created", null); + }, + /** * Handle the "command" event for the buttons that allow the user to * reposition the Web Console UI. @@ -620,16 +672,11 @@ WebConsoleFrame.prototype = { this.outputMessage(CATEGORY_WEBDEV, this.logConsoleAPIMessage, [aMessage.json]); break; - case "WebConsole:PageError": { - let pageError = aMessage.json.pageError; - let category = Utils.categoryForScriptError(pageError); - this.outputMessage(category, this.reportPageError, - [category, pageError]); + case "WebConsole:Initialized": + this._onMessageManagerInitComplete(); break; - } case "WebConsole:CachedMessages": this._displayCachedConsoleMessages(aMessage.json.messages); - this.owner._onInitComplete(); break; case "WebConsole:NetworkActivity": this.handleNetworkActivity(aMessage.json); @@ -647,6 +694,20 @@ WebConsoleFrame.prototype = { } }, + /** + * Callback method used to track the Web Console initialization via message + * manager. + * + * @private + */ + _onMessageManagerInitComplete: function WCF__onMessageManagerInitComplete() + { + this._messageManagerInitComplete = true; + if (this.proxy.connected) { + this._onInitComplete(); + } + }, + /** * The event handler that is called whenever a user switches a filter on or * off. @@ -1226,8 +1287,7 @@ WebConsoleFrame.prototype = { // Warnings and legacy strict errors become warnings; other types become // errors. let severity = SEVERITY_ERROR; - if ((aScriptError.flags & aScriptError.warningFlag) || - (aScriptError.flags & aScriptError.strictFlag)) { + if (aScriptError.warning || aScriptError.strict) { severity = SEVERITY_WARNING; } @@ -1239,6 +1299,19 @@ WebConsoleFrame.prototype = { return node; }, + /** + * Handle PageError objects received from the server. This method outputs the + * given error. + * + * @param nsIScriptError aPageError + * The error received from the server. + */ + handlePageError: function WCF_handlePageError(aPageError) + { + let category = Utils.categoryForScriptError(aPageError); + this.outputMessage(category, this.reportPageError, [category, aPageError]); + }, + /** * Log network activity. * @@ -2337,9 +2410,17 @@ WebConsoleFrame.prototype = { /** * Destroy the HUD object. Call this method to avoid memory leaks when the Web * Console is closed. + * + * @param function [aOnDestroy] + * Optional function to invoke when the Web Console instance is + * destroyed. */ - destroy: function WCF_destroy() + destroy: function WCF_destroy(aOnDestroy) { + if (this.proxy) { + this.proxy.disconnect(aOnDestroy); + } + if (this.jsterm) { this.jsterm.destroy(); } @@ -3535,6 +3616,158 @@ CommandController.prototype = { } }; +/////////////////////////////////////////////////////////////////////////////// +// Web Console connection proxy +/////////////////////////////////////////////////////////////////////////////// + +/** + * The WebConsoleConnectionProxy handles the connection between the Web Console + * and the application we connect to through the remote debug protocol. + * + * @constructor + * @param object aWebConsole + * The Web Console instance that owns this connection proxy. + */ +function WebConsoleConnectionProxy(aWebConsole) +{ + this.owner = aWebConsole; + + this._onPageError = this._onPageError.bind(this); +} + +WebConsoleConnectionProxy.prototype = { + /** + * The owning Web Console instance. + * + * @see WebConsoleFrame + * @type object + */ + owner: null, + + /** + * The DebuggerClient object. + * + * @see DebuggerClient + * @type object + */ + client: null, + + /** + * Tells if the connection is established. + * @type boolean + */ + connected: false, + + /** + * The WebConsoleActor ID. + * + * @private + * @type string + */ + _consoleActor: null, + + /** + * Initialize the debugger server. + */ + initServer: function WCCP_initServer() + { + if (!DebuggerServer.initialized) { + DebuggerServer.init(); + DebuggerServer.addBrowserActors(); + } + }, + + /** + * Initialize a debugger client and connect it to the debugger server. + * + * @param function [aCallback] + * Optional function to invoke when connection is established. + */ + connect: function WCCP_connect(aCallback) + { + let transport = DebuggerServer.connectPipe(); + let client = this.client = new DebuggerClient(transport); + + client.addListener("pageError", this._onPageError); + + let listeners = ["PageError"]; + + client.connect(function(aType, aTraits) { + client.listTabs(function(aResponse) { + let tab = aResponse.tabs[aResponse.selected]; + this._consoleActor = tab.consoleActor; + client.attachConsole(tab.consoleActor, listeners, + this._onAttachConsole.bind(this, aCallback)); + }.bind(this)); + }.bind(this)); + }, + + /** + * The "attachConsole" response handler. + * + * @private + * @param function [aCallback] + * Optional function to invoke once the connection is established. + * @param object aResponse + * The JSON response object received from the server. + * @param object aWebConsoleClient + * The WebConsoleClient instance for the attached console, for the + * specific tab we work with. + */ + _onAttachConsole: + function WCCP__onAttachConsole(aCallback, aResponse, aWebConsoleClient) + { + if (aResponse.error) { + Cu.reportError("attachConsole failed: " + aResponse.error + " " + + aResponse.message); + return; + } + + this.webConsoleClient = aWebConsoleClient; + + this.connected = true; + aCallback && aCallback(); + }, + + /** + * The "pageError" message type handler. We redirect any page errors to the UI + * for displaying. + * + * @private + * @param string aType + * Message type. + * @param object aPacket + * The message received from the server. + */ + _onPageError: function WCCP__onPageError(aType, aPacket) + { + if (this.owner && aPacket.from == this._consoleActor) { + this.owner.handlePageError(aPacket.pageError); + } + }, + + /** + * Disconnect the Web Console from the remote server. + * + * @param function [aOnDisconnect] + * Optional function to invoke when the connection is dropped. + */ + disconnect: function WCCP_disconnect(aOnDisconnect) + { + if (!this.client) { + aOnDisconnect && aOnDisconnect(); + return; + } + + this.client.removeListener("pageError", this._onPageError); + this.client.close(aOnDisconnect); + + this.client = null; + this.webConsoleClient = null; + this.connected = false; + }, +}; + function gSequenceId() { return gSequenceId.n++; diff --git a/toolkit/devtools/Makefile.in b/toolkit/devtools/Makefile.in index e9de48419bcf..a546b455666b 100644 --- a/toolkit/devtools/Makefile.in +++ b/toolkit/devtools/Makefile.in @@ -12,6 +12,7 @@ include $(topsrcdir)/config/config.mk PARALLEL_DIRS += \ debugger \ sourcemap \ + webconsole \ $(NULL) include $(topsrcdir)/config/rules.mk diff --git a/toolkit/devtools/debugger/dbg-client.jsm b/toolkit/devtools/debugger/dbg-client.jsm index 86af464ae732..adf95367c3c8 100644 --- a/toolkit/devtools/debugger/dbg-client.jsm +++ b/toolkit/devtools/debugger/dbg-client.jsm @@ -22,6 +22,9 @@ XPCOMUtils.defineLazyServiceGetter(this, "socketTransportService", "@mozilla.org/network/socket-transport-service;1", "nsISocketTransportService"); +XPCOMUtils.defineLazyModuleGetter(this, "WebConsoleClient", + "resource://gre/modules/devtools/WebConsoleClient.jsm"); + let wantLogging = Services.prefs.getBoolPref("devtools.debugger.log"); function dumpn(str) @@ -167,6 +170,7 @@ const UnsolicitedNotifications = { "newScript": "newScript", "tabDetached": "tabDetached", "tabNavigated": "tabNavigated", + "pageError": "pageError", "profilerStateChanged": "profilerStateChanged" }; @@ -194,6 +198,7 @@ function DebuggerClient(aTransport) this._transport.hooks = this; this._threadClients = {}; this._tabClients = {}; + this._consoleClients = {}; this._pendingRequests = []; this._activeRequests = {}; @@ -249,10 +254,32 @@ DebuggerClient.prototype = { } }.bind(this); - if (this.activeThread) { - this.activeThread.detach(detachTab); - } else { - detachTab(); + let detachThread = function _detachThread() { + if (this.activeThread) { + this.activeThread.detach(detachTab); + } else { + detachTab(); + } + }.bind(this); + + let consolesClosed = 0; + let consolesToClose = 0; + + let onConsoleClose = function _onConsoleClose() { + consolesClosed++; + if (consolesClosed >= consolesToClose) { + this._consoleClients = {}; + detachThread(); + } + }.bind(this); + + for each (let client in this._consoleClients) { + consolesToClose++; + client.close(onConsoleClose); + } + + if (!consolesToClose) { + detachThread(); } }, @@ -282,8 +309,9 @@ DebuggerClient.prototype = { let self = this; let packet = { to: aTabActor, type: "attach" }; this.request(packet, function(aResponse) { + let tabClient; if (!aResponse.error) { - var tabClient = new TabClient(self, aTabActor); + tabClient = new TabClient(self, aTabActor); self._tabClients[aTabActor] = tabClient; self.activeTab = tabClient; } @@ -291,6 +319,36 @@ DebuggerClient.prototype = { }); }, + /** + * Attach to a Web Console actor. + * + * @param string aConsoleActor + * The ID for the console actor to attach to. + * @param array aListeners + * The console listeners you want to start. + * @param function aOnResponse + * Called with the response packet and a WebConsoleClient + * instance (which will be undefined on error). + */ + attachConsole: + function DC_attachConsole(aConsoleActor, aListeners, aOnResponse) { + let self = this; + let packet = { + to: aConsoleActor, + type: "startListeners", + listeners: aListeners, + }; + + this.request(packet, function(aResponse) { + let consoleClient; + if (!aResponse.error) { + consoleClient = new WebConsoleClient(self, aConsoleActor); + self._consoleClients[aConsoleActor] = consoleClient; + } + aOnResponse(aResponse, consoleClient); + }); + }, + /** * Attach to a thread actor. * diff --git a/toolkit/devtools/debugger/server/dbg-server.js b/toolkit/devtools/debugger/server/dbg-server.js index 70a6ec37a3b7..78f989f693c1 100644 --- a/toolkit/devtools/debugger/server/dbg-server.js +++ b/toolkit/devtools/debugger/server/dbg-server.js @@ -185,6 +185,8 @@ var DebuggerServer = { */ addBrowserActors: function DH_addBrowserActors() { this.addActors("chrome://global/content/devtools/dbg-browser-actors.js"); + this.addActors("chrome://global/content/devtools/dbg-webconsole-actors.js"); + this.addTabActor(this.WebConsoleActor, "consoleActor"); if ("nsIProfiler" in Ci) this.addActors("chrome://global/content/devtools/dbg-profiler-actors.js"); }, diff --git a/toolkit/devtools/jar.mn b/toolkit/devtools/jar.mn index 7a1a7491a8a3..f7eba614fa8e 100644 --- a/toolkit/devtools/jar.mn +++ b/toolkit/devtools/jar.mn @@ -7,4 +7,5 @@ toolkit.jar: content/global/devtools/dbg-server.js (debugger/server/dbg-server.js) content/global/devtools/dbg-script-actors.js (debugger/server/dbg-script-actors.js) content/global/devtools/dbg-browser-actors.js (debugger/server/dbg-browser-actors.js) + content/global/devtools/dbg-webconsole-actors.js (webconsole/dbg-webconsole-actors.js) content/global/devtools/dbg-profiler-actors.js (debugger/server/dbg-profiler-actors.js) diff --git a/toolkit/devtools/webconsole/Makefile.in b/toolkit/devtools/webconsole/Makefile.in new file mode 100644 index 000000000000..e2f3ae0ee300 --- /dev/null +++ b/toolkit/devtools/webconsole/Makefile.in @@ -0,0 +1,17 @@ +# 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/. + +DEPTH = ../../.. +topsrcdir = @top_srcdir@ +srcdir = @srcdir@ +VPATH = @srcdir@ + +include $(DEPTH)/config/autoconf.mk + +#TEST_DIRS += tests + +include $(topsrcdir)/config/rules.mk + +libs:: + $(INSTALL) $(IFLAGS1) $(srcdir)/*.jsm $(FINAL_TARGET)/modules/devtools diff --git a/toolkit/devtools/webconsole/WebConsoleClient.jsm b/toolkit/devtools/webconsole/WebConsoleClient.jsm new file mode 100644 index 000000000000..cf4fd743411b --- /dev/null +++ b/toolkit/devtools/webconsole/WebConsoleClient.jsm @@ -0,0 +1,103 @@ +/* -*- Mode: js2; js2-basic-offset: 2; indent-tabs-mode: nil; -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* 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/. */ + +"use strict"; + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cu = Components.utils; + +var EXPORTED_SYMBOLS = ["WebConsoleClient"]; + +/** + * A WebConsoleClient is used as a front end for the WebConsoleActor that is + * created on the server, hiding implementation details. + * + * @param object aDebuggerClient + * The DebuggerClient instance we live for. + * @param string aActor + * The WebConsoleActor ID. + */ +function WebConsoleClient(aDebuggerClient, aActor) +{ + this._actor = aActor; + this._client = aDebuggerClient; +} + +WebConsoleClient.prototype = { + /** + * Retrieve the cached messages from the server. + * + * @see this.CACHED_MESSAGES + * @param array aTypes + * The array of message types you want from the server. See + * this.CACHED_MESSAGES for known types. + * @param function aOnResponse + * The function invoked when the response is received. + */ + getCachedMessages: function WCC_getCachedMessages(aTypes, aOnResponse) + { + let packet = { + to: this._actor, + type: "getCachedMessages", + messageTypes: aTypes, + }; + this._client.request(packet, aOnResponse); + }, + + /** + * Start the given Web Console listeners. + * + * @see this.LISTENERS + * @param array aListeners + * Array of listeners you want to start. See this.LISTENERS for + * known listeners. + * @param function aOnResponse + * Function to invoke when the server response is received. + */ + startListeners: function WCC_startListeners(aListeners, aOnResponse) + { + let packet = { + to: this._actor, + type: "startListeners", + listeners: aListeners, + }; + this._client.request(packet, aOnResponse); + }, + + /** + * Stop the given Web Console listeners. + * + * @see this.LISTENERS + * @param array aListeners + * Array of listeners you want to stop. See this.LISTENERS for + * known listeners. + * @param function aOnResponse + * Function to invoke when the server response is received. + */ + stopListeners: function WCC_stopListeners(aListeners, aOnResponse) + { + let packet = { + to: this._actor, + type: "stopListeners", + listeners: aListeners, + }; + this._client.request(packet, aOnResponse); + }, + + /** + * Close the WebConsoleClient. This stops all the listeners on the server and + * detaches from the console actor. + * + * @param function aOnResponse + * Function to invoke when the server response is received. + */ + close: function WCC_close(aOnResponse) + { + this.stopListeners(null, aOnResponse); + this._client = null; + }, +}; diff --git a/browser/devtools/webconsole/WebConsoleUtils.jsm b/toolkit/devtools/webconsole/WebConsoleUtils.jsm similarity index 85% rename from browser/devtools/webconsole/WebConsoleUtils.jsm rename to toolkit/devtools/webconsole/WebConsoleUtils.jsm index fc1293631f19..c600eabf18ee 100644 --- a/browser/devtools/webconsole/WebConsoleUtils.jsm +++ b/toolkit/devtools/webconsole/WebConsoleUtils.jsm @@ -15,9 +15,8 @@ Cu.import("resource://gre/modules/XPCOMUtils.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "Services", "resource://gre/modules/Services.jsm"); -var EXPORTED_SYMBOLS = ["WebConsoleUtils", "JSPropertyProvider"]; - -const STRINGS_URI = "chrome://browser/locale/devtools/webconsole.properties"; +var EXPORTED_SYMBOLS = ["WebConsoleUtils", "JSPropertyProvider", + "PageErrorListener"]; const TYPES = { OBJECT: 0, FUNCTION: 1, @@ -645,7 +644,22 @@ var WebConsoleUtils = { // Localization ////////////////////////////////////////////////////////////////////////// -WebConsoleUtils.l10n = { +WebConsoleUtils.l10n = function WCU_l10n(aBundleURI) +{ + this._bundleUri = aBundleURI; +}; + +WebConsoleUtils.l10n.prototype = { + _stringBundle: null, + + get stringBundle() + { + if (!this._stringBundle) { + this._stringBundle = Services.strings.createBundle(this._bundleUri); + } + return this._stringBundle; + }, + /** * Generates a formatted timestamp string for displaying in console messages. * @@ -710,10 +724,6 @@ WebConsoleUtils.l10n = { }, }; -XPCOMUtils.defineLazyGetter(WebConsoleUtils.l10n, "stringBundle", function() { - return Services.strings.createBundle(STRINGS_URI); -}); - ////////////////////////////////////////////////////////////////////////// // JS Completer @@ -1012,3 +1022,144 @@ function getMatchedProps(aObj, aOptions = {matchProp: ""}) return JSPropertyProvider; })(WebConsoleUtils); + +/////////////////////////////////////////////////////////////////////////////// +// The page errors listener +/////////////////////////////////////////////////////////////////////////////// + +/** + * The nsIConsoleService listener. This is used to send all the page errors + * (JavaScript, CSS and more) to the remote Web Console instance. + * + * @constructor + * @param nsIDOMWindow aWindow + * The window object for which we are created. + * @param object aListener + * The listener object must have a method: onPageError. This method is + * invoked with one argument, the nsIScriptError, whenever a relevant + * page error is received. + */ +function PageErrorListener(aWindow, aListener) +{ + this.window = aWindow; + this.listener = aListener; +} + +PageErrorListener.prototype = +{ + QueryInterface: XPCOMUtils.generateQI([Ci.nsIConsoleListener]), + + /** + * The content window for which we listen to page errors. + * @type nsIDOMWindow + */ + window: null, + + /** + * The listener object which is notified of page errors. It must have + * a onPageError method which is invoked with one argument: the nsIScriptError. + * @type object + */ + listener: null, + + /** + * Initialize the nsIConsoleService listener. + */ + init: function PEL_init() + { + Services.console.registerListener(this); + }, + + /** + * The nsIConsoleService observer. This method takes all the script error + * messages belonging to the current window and sends them to the remote Web + * Console instance. + * + * @param nsIScriptError aScriptError + * The script error object coming from the nsIConsoleService. + */ + observe: function PEL_observe(aScriptError) + { + if (!this.window || !this.listener || + !(aScriptError instanceof Ci.nsIScriptError) || + !aScriptError.outerWindowID) { + return; + } + + if (!this.isCategoryAllowed(aScriptError.category)) { + return; + } + + let errorWindow = + WebConsoleUtils.getWindowByOuterId(aScriptError.outerWindowID, this.window); + if (!errorWindow || errorWindow.top != this.window) { + return; + } + + this.listener.onPageError(aScriptError); + }, + + /** + * Check if the given script error category is allowed to be tracked or not. + * We ignore chrome-originating errors as we only care about content. + * + * @param string aCategory + * The nsIScriptError category you want to check. + * @return boolean + * True if the category is allowed to be logged, false otherwise. + */ + isCategoryAllowed: function PEL_isCategoryAllowed(aCategory) + { + switch (aCategory) { + case "XPConnect JavaScript": + case "component javascript": + case "chrome javascript": + case "chrome registration": + case "XBL": + case "XBL Prototype Handler": + case "XBL Content Sink": + case "xbl javascript": + return false; + } + + return true; + }, + + /** + * Get the cached page errors for the current inner window. + * + * @return array + * The array of cached messages. Each element is an nsIScriptError + * with an added _type property so the remote Web Console instance can + * tell the difference between various types of cached messages. + */ + getCachedMessages: function PEL_getCachedMessages() + { + let innerWindowId = WebConsoleUtils.getInnerWindowId(this.window); + let result = []; + let errors = {}; + Services.console.getMessageArray(errors, {}); + + (errors.value || []).forEach(function(aError) { + if (!(aError instanceof Ci.nsIScriptError) || + aError.innerWindowID != innerWindowId || + !this.isCategoryAllowed(aError.category)) { + return; + } + + let remoteMessage = WebConsoleUtils.cloneObject(aError); + result.push(remoteMessage); + }, this); + + return result; + }, + + /** + * Remove the nsIConsoleService listener. + */ + destroy: function PEL_destroy() + { + Services.console.unregisterListener(this); + this.listener = this.window = null; + }, +}; diff --git a/toolkit/devtools/webconsole/dbg-webconsole-actors.js b/toolkit/devtools/webconsole/dbg-webconsole-actors.js new file mode 100644 index 000000000000..a85fd525f747 --- /dev/null +++ b/toolkit/devtools/webconsole/dbg-webconsole-actors.js @@ -0,0 +1,193 @@ +/* -*- Mode: js2; js2-basic-offset: 2; indent-tabs-mode: nil; -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* 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/. */ + +"use strict"; + +let Cc = Components.classes; +let Ci = Components.interfaces; +let Cu = Components.utils; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "Services", + "resource://gre/modules/Services.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "PageErrorListener", + "resource://gre/modules/devtools/WebConsoleUtils.jsm"); + +/** + * The WebConsoleActor implements capabilities needed for the Web Console + * feature. + * + * @constructor + * @param object aConnection + * The connection to the client, DebuggerServerConnection. + * @param object aTabActor + * The parent tab actor. + */ +function WebConsoleActor(aConnection, aTabActor) +{ + this.conn = aConnection; + this._browser = aTabActor.browser; +} + +WebConsoleActor.prototype = +{ + /** + * The xul:browser we work with. + * @private + * @type nsIDOMElement + */ + _browser: null, + + /** + * The debugger server connection instance. + * @type object + */ + conn: null, + + /** + * The content window we work with. + * @type nsIDOMWindow + */ + get window() this._browser.contentWindow, + + /** + * The PageErrorListener instance. + * @type object + */ + pageErrorListener: null, + + actorPrefix: "console", + + grip: function WCA_grip() + { + return { actor: this.actorID }; + }, + + /** + * Destroy the current WebConsoleActor instance. + */ + disconnect: function WCA_disconnect() + { + if (this.pageErrorListener) { + this.pageErrorListener.destroy(); + this.pageErrorListener = null; + } + this.conn = this._browser = null; + }, + + /** + * Handler for the "startListeners" request. + * + * @param object aRequest + * The JSON request object received from the Web Console client. + * @return object + * The response object which holds the startedListeners array. + */ + onStartListeners: function WCA_onStartListeners(aRequest) + { + let startedListeners = []; + + while (aRequest.listeners.length > 0) { + let listener = aRequest.listeners.shift(); + switch (listener) { + case "PageError": + if (!this.pageErrorListener) { + this.pageErrorListener = + new PageErrorListener(this.window, this); + this.pageErrorListener.init(); + } + startedListeners.push(listener); + break; + } + } + return { startedListeners: startedListeners }; + }, + + /** + * Handler for the "stopListeners" request. + * + * @param object aRequest + * The JSON request object received from the Web Console client. + * @return object + * The response packet to send to the client: holds the + * stoppedListeners array. + */ + onStopListeners: function WCA_onStopListeners(aRequest) + { + let stoppedListeners = []; + + // If no specific listeners are requested to be detached, we stop all + // listeners. + let toDetach = aRequest.listeners || ["PageError"]; + + while (toDetach.length > 0) { + let listener = toDetach.shift(); + switch (listener) { + case "PageError": + if (this.pageErrorListener) { + this.pageErrorListener.destroy(); + this.pageErrorListener = null; + } + stoppedListeners.push(listener); + break; + } + } + + return { stoppedListeners: stoppedListeners }; + }, + + /** + * Handler for page errors received from the PageErrorListener. This method + * sends the nsIScriptError to the remote Web Console client. + * + * @param nsIScriptError aPageError + * The page error we need to send to the client. + */ + onPageError: function WCA_onPageError(aPageError) + { + let packet = { + from: this.actorID, + type: "pageError", + pageError: this.preparePageErrorForRemote(aPageError), + }; + this.conn.send(packet); + }, + + /** + * Prepare an nsIScriptError to be sent to the client. + * + * @param nsIScriptError aPageError + * The page error we need to send to the client. + * @return object + * The object you can send to the remote client. + */ + preparePageErrorForRemote: function WCA_preparePageErrorForRemote(aPageError) + { + return { + message: aPageError.message, + errorMessage: aPageError.errorMessage, + sourceName: aPageError.sourceName, + lineText: aPageError.sourceLine, + lineNumber: aPageError.lineNumber, + columnNumber: aPageError.columnNumber, + category: aPageError.category, + timeStamp: aPageError.timeStamp, + warning: !!(aPageError.flags & aPageError.warningFlag), + error: !!(aPageError.flags & aPageError.errorFlag), + exception: !!(aPageError.flags & aPageError.exceptionFlag), + strict: !!(aPageError.flags & aPageError.strictFlag), + }; + }, +}; + +WebConsoleActor.prototype.requestTypes = +{ + startListeners: WebConsoleActor.prototype.onStartListeners, + stopListeners: WebConsoleActor.prototype.onStopListeners, +}; + From f59e844bfe0c7f339453be67536dc210b68af909 Mon Sep 17 00:00:00 2001 From: Mihai Sucan Date: Wed, 26 Sep 2012 18:02:04 +0100 Subject: [PATCH 13/18] Bug 768096 - Web Console remote debugging protocol support - Part 2: window.console API and JS evaluation; r=past,robcee --- browser/devtools/webconsole/HUDService.jsm | 16 +- browser/devtools/webconsole/PropertyPanel.jsm | 220 ++--- ...console_bug_632347_iterators_generators.js | 2 +- ...owser_webconsole_bug_659907_console_dir.js | 2 +- .../test/browser_webconsole_jsterm.js | 10 +- browser/devtools/webconsole/webconsole.js | 620 ++++++++------ toolkit/devtools/debugger/dbg-client.jsm | 15 + .../devtools/webconsole/WebConsoleClient.jsm | 69 ++ .../devtools/webconsole/WebConsoleUtils.jsm | 779 +++++++++++++++++- .../webconsole/dbg-webconsole-actors.js | 491 ++++++++++- 10 files changed, 1828 insertions(+), 396 deletions(-) diff --git a/browser/devtools/webconsole/HUDService.jsm b/browser/devtools/webconsole/HUDService.jsm index 0762942d6a9e..945e21c1b5b7 100644 --- a/browser/devtools/webconsole/HUDService.jsm +++ b/browser/devtools/webconsole/HUDService.jsm @@ -536,12 +536,8 @@ WebConsole.prototype = { * @private * @type array */ - _messageListeners: ["JSTerm:EvalObject", "WebConsole:ConsoleAPI", - "WebConsole:CachedMessages", "WebConsole:Initialized", "JSTerm:EvalResult", - "JSTerm:AutocompleteProperties", "JSTerm:ClearOutput", - "JSTerm:InspectObject", "WebConsole:NetworkActivity", - "WebConsole:FileActivity", "WebConsole:LocationChange", - "JSTerm:NonNativeConsoleAPI"], + _messageListeners: ["WebConsole:Initialized", "WebConsole:NetworkActivity", + "WebConsole:FileActivity", "WebConsole:LocationChange"], /** * The xul:panel that holds the Web Console when it is positioned as a window. @@ -901,10 +897,10 @@ WebConsole.prototype = { /** * The clear output button handler. + * @private */ - onClearButton: function WC_onClearButton() + _onClearButton: function WC__onClearButton() { - this.ui.jsterm.clearOutput(true); this.chromeWindow.DeveloperToolbar.resetErrorsCount(this.tab); }, @@ -924,10 +920,8 @@ WebConsole.prototype = { }, this); let message = { - features: ["ConsoleAPI", "JSTerm", "NetworkMonitor", "LocationChange"], - cachedMessages: ["ConsoleAPI", "PageError"], + features: ["NetworkMonitor", "LocationChange"], NetworkMonitor: { monitorFileActivity: true }, - JSTerm: { notifyNonNativeConsoleAPI: true }, preferences: { "NetworkMonitor.saveRequestAndResponseBodies": this.ui.saveRequestAndResponseBodies, diff --git a/browser/devtools/webconsole/PropertyPanel.jsm b/browser/devtools/webconsole/PropertyPanel.jsm index 6fcbdd124d39..38c363646161 100644 --- a/browser/devtools/webconsole/PropertyPanel.jsm +++ b/browser/devtools/webconsole/PropertyPanel.jsm @@ -27,7 +27,7 @@ var EXPORTED_SYMBOLS = ["PropertyPanel", "PropertyTreeView"]; */ var PropertyTreeView = function() { this._rows = []; - this._objectCache = {}; + this._objectActors = []; }; PropertyTreeView.prototype = { @@ -44,10 +44,24 @@ PropertyTreeView.prototype = { _treeBox: null, /** - * Stores cached information about local objects being inspected. + * Track known object actor IDs. We clean these when the panel is + * destroyed/cleaned up. + * * @private + * @type array */ - _objectCache: null, + _objectActors: null, + + /** + * Map fake object actors to their IDs. This is used when we inspect local + * objects. + * @private + * @type Object + */ + _localObjectActors: null, + + _releaseObject: null, + _objectPropertiesProvider: null, /** * Use this setter to update the content of the tree. @@ -58,54 +72,47 @@ PropertyTreeView.prototype = { * - object: * This is the raw object you want to display. You can only provide * this object if you want the property panel to work in sync mode. - * - remoteObject: + * - objectProperties: * An array that holds information on the remote object being * inspected. Each element in this array describes each property in the - * remote object. See WebConsoleUtils.namesAndValuesOf() for details. - * - rootCacheId: - * The cache ID where the objects referenced in remoteObject are found. - * - panelCacheId: - * The cache ID where any object retrieved by this property panel - * instance should be stored into. - * - remoteObjectProvider: + * remote object. See WebConsoleUtils.inspectObject() for details. + * - objectPropertiesProvider: * A function that is invoked when a new object is needed. This is * called when the user tries to expand an inspectable property. The * callback must take four arguments: - * - fromCacheId: - * Tells from where to retrieve the object the user picked (from - * which cache ID). - * - objectId: - * The object ID the user wants. - * - panelCacheId: - * Tells in which cache ID to store the objects referenced by - * objectId so they can be retrieved later. + * - actorID: + * The object actor ID from which we request the properties. * - callback: * The callback function to be invoked when the remote object is - * received. This function takes one argument: the raw message - * received from the Web Console content script. + * received. This function takes one argument: the array of + * descriptors for each property in the object represented by the + * actor. + * - releaseObject: + * Function to invoke when an object actor should be released. The + * function must take one argument: the object actor ID. */ set data(aData) { let oldLen = this._rows.length; - this._cleanup(); + this.cleanup(); if (!aData) { return; } - if (aData.remoteObject) { - this._rootCacheId = aData.rootCacheId; - this._panelCacheId = aData.panelCacheId; - this._remoteObjectProvider = aData.remoteObjectProvider; - this._rows = [].concat(aData.remoteObject); - this._updateRemoteObject(this._rows, 0); + if (aData.objectPropertiesProvider) { + this._objectPropertiesProvider = aData.objectPropertiesProvider; + this._releaseObject = aData.releaseObject; + this._propertiesToRows(aData.objectProperties, 0); + this._rows = aData.objectProperties; } else if (aData.object) { + this._localObjectActors = Object.create(null); this._rows = this._inspectObject(aData.object); } else { - throw new Error("First argument must have a .remoteObject or " + - "an .object property!"); + throw new Error("First argument must have an objectActor or an " + + "object property!"); } if (this._treeBox) { @@ -128,13 +135,22 @@ PropertyTreeView.prototype = { * @param number aLevel * The level you want to give to each property in the remote object. */ - _updateRemoteObject: function PTV__updateRemoteObject(aObject, aLevel) + _propertiesToRows: function PTV__propertiesToRows(aObject, aLevel) { - aObject.forEach(function(aElement) { - aElement.level = aLevel; - aElement.isOpened = false; - aElement.children = null; - }); + aObject.forEach(function(aItem) { + aItem._level = aLevel; + aItem._open = false; + aItem._children = null; + + if (this._releaseObject) { + ["value", "get", "set"].forEach(function(aProp) { + let val = aItem[aProp]; + if (val && val.actor) { + this._objectActors.push(val.actor); + } + }, this); + } + }, this); }, /** @@ -143,42 +159,53 @@ PropertyTreeView.prototype = { * @private * @param object aObject * The object you want to inspect. + * @return array + * The array of properties, each being described in a way that is + * usable by the tree view. */ _inspectObject: function PTV__inspectObject(aObject) { - this._objectCache = {}; - this._remoteObjectProvider = this._localObjectProvider.bind(this); - let children = WebConsoleUtils.namesAndValuesOf(aObject, this._objectCache); - this._updateRemoteObject(children, 0); + this._objectPropertiesProvider = this._localPropertiesProvider.bind(this); + let children = + WebConsoleUtils.inspectObject(aObject, this._localObjectGrip.bind(this)); + this._propertiesToRows(children, 0); return children; }, /** - * An object provider for when the user inspects local objects (not remote + * Make a local fake object actor for the given object. + * + * @private + * @param object aObject + * The object to make an actor for. + * @return object + * The fake actor grip that represents the given object. + */ + _localObjectGrip: function PTV__localObjectGrip(aObject) + { + let grip = WebConsoleUtils.getObjectGrip(aObject); + grip.actor = "obj" + gSequenceId(); + this._localObjectActors[grip.actor] = aObject; + return grip; + }, + + /** + * A properties provider for when the user inspects local objects (not remote * ones). * * @private - * @param string aFromCacheId - * The cache ID from where to retrieve the desired object. - * @param string aObjectId - * The ID of the object you want. - * @param string aDestCacheId - * The ID of the cache where to store any objects referenced by the - * desired object. + * @param string aActor + * The ID of the object actor you want. * @param function aCallback - * The function you want to receive the object. + * The function you want to receive the list of properties. */ - _localObjectProvider: - function PTV__localObjectProvider(aFromCacheId, aObjectId, aDestCacheId, - aCallback) + _localPropertiesProvider: + function PTV__localPropertiesProvider(aActor, aCallback) { - let object = WebConsoleUtils.namesAndValuesOf(this._objectCache[aObjectId], - this._objectCache); - aCallback({cacheId: aFromCacheId, - objectId: aObjectId, - object: object, - childrenCacheId: aDestCacheId || aFromCacheId, - }); + let object = this._localObjectActors[aActor]; + let properties = + WebConsoleUtils.inspectObject(object, this._localObjectGrip.bind(this)); + aCallback(properties); }, /** nsITreeView interface implementation **/ @@ -187,18 +214,20 @@ PropertyTreeView.prototype = { get rowCount() { return this._rows.length; }, setTree: function(treeBox) { this._treeBox = treeBox; }, - getCellText: function(idx, column) { + getCellText: function PTV_getCellText(idx, column) + { let row = this._rows[idx]; - return row.name + ": " + row.value; + return row.name + ": " + WebConsoleUtils.getPropertyPanelValue(row); }, getLevel: function(idx) { - return this._rows[idx].level; + return this._rows[idx]._level; }, isContainer: function(idx) { - return !!this._rows[idx].inspectable; + return typeof this._rows[idx].value == "object" && this._rows[idx].value && + this._rows[idx].value.inspectable; }, isContainerOpen: function(idx) { - return this._rows[idx].isOpened; + return this._rows[idx]._open; }, isContainerEmpty: function(idx) { return false; }, isSeparator: function(idx) { return false; }, @@ -221,22 +250,22 @@ PropertyTreeView.prototype = { hasNextSibling: function(idx, after) { - var thisLevel = this.getLevel(idx); - return this._rows.slice(after + 1).some(function (r) r.level == thisLevel); + let thisLevel = this.getLevel(idx); + return this._rows.slice(after + 1).some(function (r) r._level == thisLevel); }, toggleOpenState: function(idx) { let item = this._rows[idx]; - if (!item.inspectable) { + if (!this.isContainer(idx)) { return; } - if (item.isOpened) { + if (item._open) { this._treeBox.beginUpdateBatch(); - item.isOpened = false; + item._open = false; - var thisLevel = item.level; + var thisLevel = item._level; var t = idx + 1, deleteCount = 0; while (t < this._rows.length && this.getLevel(t++) > thisLevel) { deleteCount++; @@ -251,31 +280,27 @@ PropertyTreeView.prototype = { } else { let levelUpdate = true; - let callback = function _onRemoteResponse(aResponse) { + let callback = function _onRemoteResponse(aProperties) { this._treeBox.beginUpdateBatch(); - item.isOpened = true; - if (levelUpdate) { - this._updateRemoteObject(aResponse.object, item.level + 1); - item.children = aResponse.object; + this._propertiesToRows(aProperties, item._level + 1); + item._children = aProperties; } - this._rows.splice.apply(this._rows, [idx + 1, 0].concat(item.children)); + this._rows.splice.apply(this._rows, [idx + 1, 0].concat(item._children)); - this._treeBox.rowCountChanged(idx + 1, item.children.length); + this._treeBox.rowCountChanged(idx + 1, item._children.length); this._treeBox.invalidateRow(idx); this._treeBox.endUpdateBatch(); + item._open = true; }.bind(this); - if (!item.children) { - let fromCacheId = item.level > 0 ? this._panelCacheId : - this._rootCacheId; - this._remoteObjectProvider(fromCacheId, item.objectId, - this._panelCacheId, callback); + if (!item._children) { + this._objectPropertiesProvider(item.value.actor, callback); } else { levelUpdate = false; - callback({object: item.children}); + callback(item._children); } } }, @@ -298,18 +323,23 @@ PropertyTreeView.prototype = { drop: function(index, orientation, dataTransfer) { }, canDrop: function(index, orientation, dataTransfer) { return false; }, - _cleanup: function PTV__cleanup() + /** + * Cleanup the property tree view. + */ + cleanup: function PTV_cleanup() { - if (this._rows.length) { - // Reset the existing _rows children to the initial state. - this._updateRemoteObject(this._rows, 0); - this._rows = []; + if (this._releaseObject) { + this._objectActors.forEach(this._releaseObject); + delete this._objectPropertiesProvider; + delete this._releaseObject; + } + if (this._localObjectActors) { + delete this._localObjectActors; + delete this._objectPropertiesProvider; } - delete this._objectCache; - delete this._rootCacheId; - delete this._panelCacheId; - delete this._remoteObjectProvider; + this._rows = []; + this._objectActors = []; }, }; @@ -459,3 +489,9 @@ PropertyPanel.prototype.destroy = function PP_destroy() this.tree = null; } + +function gSequenceId() +{ + return gSequenceId.n++; +} +gSequenceId.n = 0; diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_632347_iterators_generators.js b/browser/devtools/webconsole/test/browser_webconsole_bug_632347_iterators_generators.js index 589f7aff57e3..2859de032067 100644 --- a/browser/devtools/webconsole/test/browser_webconsole_bug_632347_iterators_generators.js +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_632347_iterators_generators.js @@ -117,7 +117,7 @@ function testPropertyPanel(aPanel) { ok(find("iter1: Iterator", false), "iter1 is correctly displayed in the Property Panel"); - ok(find("iter2: Iterator", false), + ok(find("iter2: Object", false), "iter2 is correctly displayed in the Property Panel"); executeSoon(finishTest); diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_659907_console_dir.js b/browser/devtools/webconsole/test/browser_webconsole_bug_659907_console_dir.js index 365797b552e7..46ea40497f6e 100644 --- a/browser/devtools/webconsole/test/browser_webconsole_bug_659907_console_dir.js +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_659907_console_dir.js @@ -41,7 +41,7 @@ function testConsoleDir(outputNode) { if (text == "querySelectorAll: function querySelectorAll()") { foundQSA = true; } - else if (text == "location: Object") { + else if (text == "location: Location") { foundLocation = true; } else if (text == "write: function write()") { diff --git a/browser/devtools/webconsole/test/browser_webconsole_jsterm.js b/browser/devtools/webconsole/test/browser_webconsole_jsterm.js index 0c1fe6e2d0bc..323f9402147c 100644 --- a/browser/devtools/webconsole/test/browser_webconsole_jsterm.js +++ b/browser/devtools/webconsole/test/browser_webconsole_jsterm.js @@ -111,10 +111,10 @@ function testJSTerm(hud) let foundTab = null; waitForSuccess({ - name: "help tab opened", + name: "help tabs opened", validatorFn: function() { - let newTabOpen = gBrowser.tabs.length == tabs + 1; + let newTabOpen = gBrowser.tabs.length == tabs + 3; if (!newTabOpen) { return false; } @@ -124,7 +124,9 @@ function testJSTerm(hud) }, successFn: function() { - gBrowser.removeTab(foundTab); + gBrowser.removeTab(gBrowser.tabs[gBrowser.tabs.length - 1]); + gBrowser.removeTab(gBrowser.tabs[gBrowser.tabs.length - 1]); + gBrowser.removeTab(gBrowser.tabs[gBrowser.tabs.length - 1]); nextTest(); }, failureFn: nextTest, @@ -176,7 +178,7 @@ function testJSTerm(hud) jsterm.clearOutput(); jsterm.execute("pprint(print)"); checkResult(function(nodes) { - return nodes[0].textContent.indexOf("aJSTerm.") > -1; + return nodes[0].textContent.indexOf("aOwner.helperResult") > -1; }, "pprint(function) shows source", 1); yield; diff --git a/browser/devtools/webconsole/webconsole.js b/browser/devtools/webconsole/webconsole.js index b034661fb824..ab9742080f91 100644 --- a/browser/devtools/webconsole/webconsole.js +++ b/browser/devtools/webconsole/webconsole.js @@ -49,6 +49,8 @@ const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; const MIXED_CONTENT_LEARN_MORE = "https://developer.mozilla.org/en/Security/MixedContent"; +const HELP_URL = "https://developer.mozilla.org/docs/Tools/Web_Console/Helpers"; + // The amount of time in milliseconds that must pass between messages to // trigger the display of a new group. const NEW_GROUP_DELAY = 5000; @@ -307,6 +309,12 @@ WebConsoleFrame.prototype = { */ filterBox: null, + /** + * Getter for the debugger WebConsoleClient. + * @type object + */ + get webConsoleClient() this.proxy ? this.proxy.webConsoleClient : null, + _saveRequestAndResponseBodies: false, /** @@ -413,8 +421,10 @@ WebConsoleFrame.prototype = { this.owner.onCloseButton.bind(this.owner)); let clearButton = doc.getElementsByClassName("webconsole-clear-console-button")[0]; - clearButton.addEventListener("command", - this.owner.onClearButton.bind(this.owner)); + clearButton.addEventListener("command", function WCF__onClearButton() { + this.owner._onClearButton(); + this.jsterm.clearOutput(true); + }.bind(this)); }, /** @@ -657,27 +667,9 @@ WebConsoleFrame.prototype = { } switch (aMessage.name) { - case "JSTerm:EvalResult": - case "JSTerm:EvalObject": - case "JSTerm:AutocompleteProperties": - this.owner._receiveMessageWithCallback(aMessage.json); - break; - case "JSTerm:ClearOutput": - this.jsterm.clearOutput(); - break; - case "JSTerm:InspectObject": - this.jsterm.handleInspectObject(aMessage.json); - break; - case "WebConsole:ConsoleAPI": - this.outputMessage(CATEGORY_WEBDEV, this.logConsoleAPIMessage, - [aMessage.json]); - break; case "WebConsole:Initialized": this._onMessageManagerInitComplete(); break; - case "WebConsole:CachedMessages": - this._displayCachedConsoleMessages(aMessage.json.messages); - break; case "WebConsole:NetworkActivity": this.handleNetworkActivity(aMessage.json); break; @@ -688,9 +680,6 @@ WebConsoleFrame.prototype = { case "WebConsole:LocationChange": this.owner.onLocationChange(aMessage.json); break; - case "JSTerm:NonNativeConsoleAPI": - this.outputMessage(CATEGORY_JS, this.logWarningAboutReplacedAPI); - break; } }, @@ -1033,13 +1022,11 @@ WebConsoleFrame.prototype = { * Display cached messages that may have been collected before the UI is * displayed. * - * @private * @param array aRemoteMessages * Array of cached messages coming from the remote Web Console * content instance. */ - _displayCachedConsoleMessages: - function WCF__displayCachedConsoleMessages(aRemoteMessages) + displayCachedMessages: function WCF_displayCachedMessages(aRemoteMessages) { if (!aRemoteMessages.length) { return; @@ -1062,19 +1049,11 @@ WebConsoleFrame.prototype = { }, /** - * Logs a message to the Web Console that originates from the remote Web - * Console instance. + * Logs a message to the Web Console that originates from the Web Console + * server. * * @param object aMessage - * The message received from the remote Web Console instance. - * console service. This object needs to hold: - * - hudId - the Web Console ID. - * - apiMessage - a representation of the object sent by the console - * storage service. This object holds the console message level, the - * arguments that were passed to the console method and other - * information. - * - argumentsToString - the array of arguments passed to the console - * method, each converted to a string. + * The message received from the server. * @return nsIDOMElement|undefined * The message element to display in the Web Console output. */ @@ -1084,9 +1063,9 @@ WebConsoleFrame.prototype = { let clipboardText = null; let sourceURL = null; let sourceLine = 0; - let level = aMessage.apiMessage.level; - let args = aMessage.apiMessage.arguments; - let argsToString = aMessage.argumentsToString; + let level = aMessage.level; + let args = aMessage.arguments; + let objectActors = []; switch (level) { case "log": @@ -1094,17 +1073,36 @@ WebConsoleFrame.prototype = { case "warn": case "error": case "debug": - body = { - cacheId: aMessage.objectsCacheId, - remoteObjects: args, - argsToString: argsToString, - }; - clipboardText = argsToString.join(" "); - sourceURL = aMessage.apiMessage.filename; - sourceLine = aMessage.apiMessage.lineNumber; - break; + case "dir": + case "groupEnd": { + body = { arguments: args }; + let clipboardArray = []; + args.forEach(function(aValue) { + clipboardArray.push(WebConsoleUtils.objectActorGripToString(aValue)); + if (aValue && typeof aValue == "object" && aValue.actor) { + objectActors.push(aValue.actor); + } + }, this); + clipboardText = clipboardArray.join(" "); + sourceURL = aMessage.filename; + sourceLine = aMessage.lineNumber; - case "trace": + if (level == "dir") { + body.objectProperties = aMessage.objectProperties; + } + else if (level == "groupEnd") { + objectActors.forEach(this._releaseObject, this); + + if (this.groupDepth > 0) { + this.groupDepth--; + } + return; // no need to continue + } + + break; + } + + case "trace": { let filename = WebConsoleUtils.abbreviateSourceURL(args[0].filename); let functionName = args[0].functionName || l10n.getStr("stacktrace.anonymousFunction"); @@ -1126,34 +1124,16 @@ WebConsoleFrame.prototype = { clipboardText = clipboardText.trimRight(); break; - - case "dir": - body = { - cacheId: aMessage.objectsCacheId, - resultString: argsToString[0], - remoteObject: args[0], - remoteObjectProvider: - this.jsterm.remoteObjectProvider.bind(this.jsterm), - }; - clipboardText = body.resultString; - sourceURL = aMessage.apiMessage.filename; - sourceLine = aMessage.apiMessage.lineNumber; - break; + } case "group": case "groupCollapsed": clipboardText = body = args; - sourceURL = aMessage.apiMessage.filename; - sourceLine = aMessage.apiMessage.lineNumber; + sourceURL = aMessage.filename; + sourceLine = aMessage.lineNumber; this.groupDepth++; break; - case "groupEnd": - if (this.groupDepth > 0) { - this.groupDepth--; - } - return; - case "time": if (!args) { return; @@ -1164,8 +1144,8 @@ WebConsoleFrame.prototype = { } body = l10n.getFormatStr("timerStarted", [args.name]); clipboardText = body; - sourceURL = aMessage.apiMessage.filename; - sourceLine = aMessage.apiMessage.lineNumber; + sourceURL = aMessage.filename; + sourceLine = aMessage.lineNumber; break; case "timeEnd": @@ -1174,8 +1154,8 @@ WebConsoleFrame.prototype = { } body = l10n.getFormatStr("timeEnd", [args.name, args.duration]); clipboardText = body; - sourceURL = aMessage.apiMessage.filename; - sourceLine = aMessage.apiMessage.lineNumber; + sourceURL = aMessage.filename; + sourceLine = aMessage.lineNumber; break; default: @@ -1187,6 +1167,10 @@ WebConsoleFrame.prototype = { sourceURL, sourceLine, clipboardText, level, aMessage.timeStamp); + if (objectActors.length) { + node._objectActors = objectActors; + } + // Make the node bring up the property panel, to allow the user to inspect // the stack trace. if (level == "trace") { @@ -1208,10 +1192,6 @@ WebConsoleFrame.prototype = { } if (level == "dir") { - // Make sure the cached evaluated object will be purged when the node is - // removed. - node._evalCacheId = aMessage.objectsCacheId; - // Initialize the inspector message node, by setting the PropertyTreeView // object on the tree view. This has to be done *after* the node is // shown, because the tree binding must be attached first. @@ -1223,6 +1203,18 @@ WebConsoleFrame.prototype = { return node; }, + /** + * Handle ConsoleAPICall objects received from the server. This method outputs + * the window.console API call. + * + * @param object aMessage + * The console API message received from the server. + */ + handleConsoleAPICall: function WCF_handleConsoleAPICall(aMessage) + { + this.outputMessage(CATEGORY_WEBDEV, this.logConsoleAPIMessage, [aMessage]); + }, + /** * The click event handler for objects shown inline coming from the * window.console API. @@ -1233,11 +1225,11 @@ WebConsoleFrame.prototype = { * @param nsIDOMNode aAnchor * The object inspector anchor element. This is the clickable element * in the console.log message we display. - * @param array aRemoteObject - * The remote object representation. + * @param object aObjectActor + * The object actor grip. */ _consoleLogClick: - function WCF__consoleLogClick(aMessage, aAnchor, aRemoteObject) + function WCF__consoleLogClick(aMessage, aAnchor, aObjectActor) { if (aAnchor._panelOpen) { return; @@ -1249,29 +1241,28 @@ WebConsoleFrame.prototype = { // Data to inspect. data: { - // This is where the resultObject children are cached. - rootCacheId: aMessage._evalCacheId, - remoteObject: aRemoteObject, - // This is where all objects retrieved by the panel will be cached. - panelCacheId: "HUDPanel-" + gSequenceId(), - remoteObjectProvider: this.jsterm.remoteObjectProvider.bind(this.jsterm), + objectPropertiesProvider: this.objectPropertiesProvider.bind(this), + releaseObject: this._releaseObject.bind(this), }, }; - let propPanel = this.jsterm.openPropertyPanel(options); - propPanel.panel.setAttribute("hudId", this.hudId); - - let onPopupHide = function JST__evalInspectPopupHide() { + let propPanel; + let onPopupHide = function _onPopupHide() { propPanel.panel.removeEventListener("popuphiding", onPopupHide, false); - this.jsterm.clearObjectCache(options.data.panelCacheId); - - if (!aMessage.parentNode && aMessage._evalCacheId) { - this.jsterm.clearObjectCache(aMessage._evalCacheId); + if (!aMessage.parentNode && aMessage._objectActors) { + aMessage._objectActors.forEach(this._releaseObject, this); + aMessage._objectActors = null; } }.bind(this); - propPanel.panel.addEventListener("popuphiding", onPopupHide, false); + this.objectPropertiesProvider(aObjectActor.actor, + function _onObjectProperties(aProperties) { + options.data.objectProperties = aProperties; + propPanel = this.jsterm.openPropertyPanel(options); + propPanel.panel.setAttribute("hudId", this.hudId); + propPanel.panel.addEventListener("popuphiding", onPopupHide, false); + }.bind(this)); }, /** @@ -1453,14 +1444,12 @@ WebConsoleFrame.prototype = { /** * Inform user that the Web Console API has been replaced by a script * in a content page. - * - * @return nsIDOMElement|undefined - * The message element to display in the Web Console output. */ logWarningAboutReplacedAPI: function WCF_logWarningAboutReplacedAPI() { - return this.createMessageNode(CATEGORY_JS, SEVERITY_WARNING, - l10n.getStr("ConsoleAPIDisabled")); + let node = this.createMessageNode(CATEGORY_JS, SEVERITY_WARNING, + l10n.getStr("ConsoleAPIDisabled")); + this.outputMessage(CATEGORY_JS, node); }, /** @@ -1852,8 +1841,8 @@ WebConsoleFrame.prototype = { { let [category, methodOrNode, args] = aItem; if (typeof methodOrNode != "function" && - methodOrNode._evalCacheId && !methodOrNode._panelOpen) { - this.jsterm.clearObjectCache(methodOrNode._evalCacheId); + methodOrNode._objectActors && !methodOrNode._panelOpen) { + methodOrNode._objectActors.forEach(this._releaseObject, this); } if (category == CATEGORY_NETWORK) { @@ -1870,9 +1859,29 @@ WebConsoleFrame.prototype = { } else if (category == CATEGORY_WEBDEV && methodOrNode == this.logConsoleAPIMessage) { - let level = args[0].apiMessage.level; - if (level == "dir") { - this.jsterm.clearObjectCache(args[0].objectsCacheId); + let level = args[0].level; + let releaseObject = function _releaseObject(aValue) { + if (aValue && typeof aValue == "object" && aValue.actor) { + this._releaseObject(aValue.actor); + } + }.bind(this); + switch (level) { + case "log": + case "info": + case "warn": + case "error": + case "debug": + case "dir": + case "groupEnd": { + args[0].arguments.forEach(releaseObject); + if (level == "dir") { + args[0].objectProperties.forEach(function(aObject) { + ["value", "get", "set"].forEach(function(aProp) { + releaseObject(aObject[aProp]); + }); + }); + } + } } } }, @@ -1916,10 +1925,8 @@ WebConsoleFrame.prototype = { let tree = aMessageNode.querySelector("tree"); tree.parentNode.removeChild(tree); + aMessageNode.propertyTreeView.data = null; aMessageNode.propertyTreeView = null; - if (tree.view) { - tree.view.data = null; - } tree.view = null; }, @@ -1931,8 +1938,8 @@ WebConsoleFrame.prototype = { */ removeOutputMessage: function WCF_removeOutputMessage(aNode) { - if (aNode._evalCacheId && !aNode._panelOpen) { - this.jsterm.clearObjectCache(aNode._evalCacheId); + if (aNode._objectActors && !aNode._panelOpen) { + aNode._objectActors.forEach(this._releaseObject, this); } if (aNode.classList.contains("webconsole-msg-cssparser")) { @@ -2057,7 +2064,7 @@ WebConsoleFrame.prototype = { else { let str = undefined; if (aLevel == "dir") { - str = aBody.resultString; + str = WebConsoleUtils.objectActorGripToString(aBody.arguments[0]); } else if (["log", "info", "warn", "error", "debug"].indexOf(aLevel) > -1 && typeof aBody == "object") { @@ -2132,10 +2139,9 @@ WebConsoleFrame.prototype = { let treeView = node.propertyTreeView = new PropertyTreeView(); treeView.data = { - rootCacheId: body.cacheId, - panelCacheId: body.cacheId, - remoteObject: Array.isArray(body.remoteObject) ? body.remoteObject : [], - remoteObjectProvider: body.remoteObjectProvider, + objectPropertiesProvider: this.objectPropertiesProvider.bind(this), + releaseObject: this._releaseObject.bind(this), + objectProperties: body.objectProperties, }; tree.setAttribute("rows", treeView.rowCount); @@ -2164,13 +2170,12 @@ WebConsoleFrame.prototype = { * output. * @param object aBody * The object given by this.logConsoleAPIMessage(). This object holds - * the call information that we need to display. + * the call information that we need to display - mainly the arguments + * array of the given API call. */ _makeConsoleLogMessageBody: function WCF__makeConsoleLogMessageBody(aMessage, aContainer, aBody) { - aMessage._evalCacheId = aBody.cacheId; - Object.defineProperty(aMessage, "_panelOpen", { get: function() { let nodes = aContainer.querySelectorAll(".hud-clickable"); @@ -2182,17 +2187,19 @@ WebConsoleFrame.prototype = { configurable: false }); - aBody.remoteObjects.forEach(function(aItem, aIndex) { + aBody.arguments.forEach(function(aItem) { if (aContainer.firstChild) { aContainer.appendChild(this.document.createTextNode(" ")); } - let text = aBody.argsToString[aIndex]; - if (!Array.isArray(aItem)) { + let text = WebConsoleUtils.objectActorGripToString(aItem); + + if (aItem && typeof aItem != "object" || !aItem.inspectable) { aContainer.appendChild(this.document.createTextNode(text)); return; } + // For inspectable objects. let elem = this.document.createElement("description"); elem.classList.add("hud-clickable"); elem.setAttribute("aria-haspopup", "true"); @@ -2393,6 +2400,41 @@ WebConsoleFrame.prototype = { clipboardHelper.copyString(strings.join("\n"), this.document); }, + /** + * Object properties provider. This function gives you the properties of the + * remote object you want. + * + * @param string aActor + * The object actor ID from which you want the properties. + * @param function aCallback + * Function you want invoked once the properties are received. + */ + objectPropertiesProvider: + function WCF_objectPropertiesProvider(aActor, aCallback) + { + this.webConsoleClient.inspectObjectProperties(aActor, + function(aResponse) { + if (aResponse.error) { + Cu.reportError("Failed to retrieve the object properties from the " + + "server. Error: " + aResponse.error); + return; + } + aCallback(aResponse.properties); + }); + }, + + /** + * Release an object actor. + * + * @private + * @param string aActor + * The object actor ID you want to release. + */ + _releaseObject: function WCF__releaseObject(aActor) + { + this.proxy.releaseActor(aActor); + }, + /** * Open the selected item's URL in a new tab. */ @@ -2478,6 +2520,12 @@ JSTerm.prototype = { */ get outputNode() this.hud.outputNode, + /** + * Getter for the debugger WebConsoleClient. + * @type object + */ + get webConsoleClient() this.hud.webConsoleClient, + COMPLETE_FORWARD: 0, COMPLETE_BACKWARD: 1, COMPLETE_HINT_ONLY: 2, @@ -2499,59 +2547,59 @@ JSTerm.prototype = { }, /** - * Asynchronously evaluate a string in the content process sandbox. - * - * @param string aString - * String to evaluate in the content process JavaScript sandbox. - * @param function [aCallback] - * Optional function to be invoked when the evaluation result is - * received. - */ - evalInContentSandbox: function JST_evalInContentSandbox(aString, aCallback) - { - let message = { - str: aString, - resultCacheId: "HUDEval-" + gSequenceId(), - }; - - this.hud.owner.sendMessageToContent("JSTerm:EvalRequest", message, aCallback); - - return message; - }, - - /** - * The "JSTerm:EvalResult" message handler. This is the JSTerm execution - * result callback which is invoked whenever JavaScript code evaluation - * results come from the content process. + * The JavaScript evaluation response handler. * * @private + * @param nsIDOMElement [aAfterNode] + * Optional DOM element after which the evaluation result will be + * inserted. * @param function [aCallback] * Optional function to invoke when the evaluation result is added to * the output. * @param object aResponse - * The JSTerm:EvalResult message received from the content process. See - * JSTerm.handleEvalRequest() in HUDService-content.js for further - * details. - * @param object aRequest - * The JSTerm:EvalRequest message we sent to the content process. - * @see JSTerm.handleEvalRequest() in HUDService-content.js + * The message received from the server. */ _executeResultCallback: - function JST__executeResultCallback(aCallback, aResponse, aRequest) + function JST__executeResultCallback(aAfterNode, aCallback, aResponse) { let errorMessage = aResponse.errorMessage; - let resultString = aResponse.resultString; + let result = aResponse.result; + let inspectable = result && typeof result == "object" && result.inspectable; + let helperResult = aResponse.helperResult; + let helperHasRawOutput = !!(helperResult || {}).rawOutput; + let resultString = + WebConsoleUtils.objectActorGripToString(result, + !helperHasRawOutput); - // Hide undefined results coming from JSTerm helper functions. - if (!errorMessage && - resultString == "undefined" && - aResponse.helperResult && - !aResponse.inspectable && - !aResponse.helperRawOutput) { - return; + if (helperResult && helperResult.type) { + switch (helperResult.type) { + case "clearOutput": + this.clearOutput(); + break; + case "inspectObject": + this.handleInspectObject(helperResult.input, helperResult.object); + break; + case "error": + try { + errorMessage = l10n.getStr(helperResult.message); + } + catch (ex) { + errorMessage = helperResult.message; + } + break; + case "help": + this.hud.owner.openLink(HELP_URL); + break; + } } - let afterNode = aRequest.outputNode; + // Hide undefined results coming from JSTerm helper functions. + if (!errorMessage && result && typeof result == "object" && + result.type == "undefined" && + helperResult && !helperHasRawOutput) { + aCallback && aCallback(); + return; + } if (aCallback) { let oldFlushCallback = this.hud._flushCallback; @@ -2562,19 +2610,24 @@ JSTerm.prototype = { }.bind(this); } - if (aResponse.errorMessage) { - this.writeOutput(aResponse.errorMessage, CATEGORY_OUTPUT, SEVERITY_ERROR, - afterNode, aResponse.timestamp); + let node; + + if (errorMessage) { + node = this.writeOutput(errorMessage, CATEGORY_OUTPUT, SEVERITY_ERROR, + aAfterNode, aResponse.timestamp); } - else if (aResponse.inspectable) { - let node = this.writeOutputJS(aResponse.resultString, - this._evalOutputClick.bind(this, aResponse), - afterNode, aResponse.timestamp); - node._evalCacheId = aResponse.childrenCacheId; + else if (inspectable) { + node = this.writeOutputJS(resultString, + this._evalOutputClick.bind(this, aResponse), + aAfterNode, aResponse.timestamp); } else { - this.writeOutput(aResponse.resultString, CATEGORY_OUTPUT, SEVERITY_LOG, - afterNode, aResponse.timestamp); + node = this.writeOutput(resultString, CATEGORY_OUTPUT, SEVERITY_LOG, + aAfterNode, aResponse.timestamp); + } + + if (result && typeof result == "object" && result.actor) { + node._objectActors = [result.actor]; } }, @@ -2597,10 +2650,9 @@ JSTerm.prototype = { } let node = this.writeOutput(aExecuteString, CATEGORY_INPUT, SEVERITY_LOG); + let onResult = this._executeResultCallback.bind(this, node, aCallback); - let onResult = this._executeResultCallback.bind(this, aCallback); - let messageToContent = this.evalInContentSandbox(aExecuteString, onResult); - messageToContent.outputNode = node; + this.webConsoleClient.evaluateJS(aExecuteString, onResult); this.history.push(aExecuteString); this.historyIndex++; @@ -2751,7 +2803,7 @@ JSTerm.prototype = { hud._cssNodes = {}; if (aClearStorage) { - hud.owner.sendMessageToContent("ConsoleAPI:ClearCache", {}); + this.webConsoleClient.clearMessagesCache(); } }, @@ -3078,25 +3130,30 @@ JSTerm.prototype = { return; } - let message = { - id: "HUDComplete-" + gSequenceId(), - input: this.inputNode.value, - }; + let requestId = gSequenceId(); + let input = this.inputNode.value; + let cursor = this.inputNode.selectionStart; + // TODO: Bug 787986 - throttle/disable updates, deal with slow/high latency + // network connections. this.lastCompletion = { - requestId: message.id, + requestId: requestId, completionType: aType, value: null, }; - let callback = this._receiveAutocompleteProperties.bind(this, aCallback); - this.hud.owner.sendMessageToContent("JSTerm:Autocomplete", message, callback); + + let callback = this._receiveAutocompleteProperties.bind(this, requestId, + aCallback); + this.webConsoleClient.autocomplete(input, cursor, callback); }, /** - * Handler for the "JSTerm:AutocompleteProperties" message. This method takes - * the completion result received from the content process and updates the UI + * Handler for the autocompletion results. This method takes + * the completion result received from the server and updates the UI * accordingly. * + * @param number aRequestId + * Request ID. * @param function [aCallback=null] * Optional, function to invoke when the completion result is received. * @param object aMessage @@ -3104,13 +3161,12 @@ JSTerm.prototype = { * the content process. */ _receiveAutocompleteProperties: - function JST__receiveAutocompleteProperties(aCallback, aMessage) + function JST__receiveAutocompleteProperties(aRequestId, aCallback, aMessage) { let inputNode = this.inputNode; let inputValue = inputNode.value; - if (aMessage.input != inputValue || - this.lastCompletion.value == inputValue || - aMessage.id != this.lastCompletion.requestId) { + if (this.lastCompletion.value == inputValue || + aRequestId != this.lastCompletion.requestId) { return; } @@ -3266,7 +3322,7 @@ JSTerm.prototype = { }, /** - * The "JSTerm:InspectObject" remote message handler. This allows the content + * The JSTerm InspectObject remote message handler. This allows the remote * process to open the Property Panel for a given object. * * @param object aRequest @@ -3274,29 +3330,31 @@ JSTerm.prototype = { * the user input string that was evaluated to inspect an object and * the result object which is to be inspected. */ - handleInspectObject: function JST_handleInspectObject(aRequest) + handleInspectObject: function JST_handleInspectObject(aInput, aActor) { let options = { - title: aRequest.input, + title: aInput, data: { - rootCacheId: aRequest.objectCacheId, - panelCacheId: aRequest.objectCacheId, - remoteObject: aRequest.resultObject, - remoteObjectProvider: this.remoteObjectProvider.bind(this), + objectPropertiesProvider: this.hud.objectPropertiesProvider.bind(this.hud), + releaseObject: this.hud._releaseObject.bind(this.hud), }, }; - let propPanel = this.openPropertyPanel(options); - propPanel.panel.setAttribute("hudId", this.hudId); + let propPanel; let onPopupHide = function JST__onPopupHide() { propPanel.panel.removeEventListener("popuphiding", onPopupHide, false); - - this.clearObjectCache(options.data.panelCacheId); + this.hud._releaseObject(aActor.actor); }.bind(this); - propPanel.panel.addEventListener("popuphiding", onPopupHide, false); + this.hud.objectPropertiesProvider(aActor.actor, + function _onObjectProperties(aProperties) { + options.data.objectProperties = aProperties; + propPanel = this.openPropertyPanel(options); + propPanel.panel.setAttribute("hudId", this.hudId); + propPanel.panel.addEventListener("popuphiding", onPopupHide, false); + }.bind(this)); }, /** @@ -3304,7 +3362,7 @@ JSTerm.prototype = { * * @private * @param object aResponse - * The JSTerm:EvalResult message received from the content process. + * The JavaScript evaluation response received from the server. * @param nsIDOMNode aLink * The message node for which we are handling events. */ @@ -3320,35 +3378,36 @@ JSTerm.prototype = { // Data to inspect. data: { - // This is where the resultObject children are cached. - rootCacheId: aResponse.childrenCacheId, - remoteObject: aResponse.resultObject, - // This is where all objects retrieved by the panel will be cached. - panelCacheId: "HUDPanel-" + gSequenceId(), - remoteObjectProvider: this.remoteObjectProvider.bind(this), + objectPropertiesProvider: this.hud.objectPropertiesProvider.bind(this.hud), + releaseObject: this.hud._releaseObject.bind(this.hud), }, }; - options.updateButtonCallback = function JST__evalUpdateButton() { - this.evalInContentSandbox(aResponse.input, - this._evalOutputUpdatePanelCallback.bind(this, options, propPanel, - aResponse)); - }.bind(this); + let propPanel; - let propPanel = this.openPropertyPanel(options); - propPanel.panel.setAttribute("hudId", this.hudId); + options.updateButtonCallback = function JST__evalUpdateButton() { + let onResult = + this._evalOutputUpdatePanelCallback.bind(this, options, propPanel, + aResponse); + this.webConsoleClient.evaluateJS(aResponse.input, onResult); + }.bind(this); let onPopupHide = function JST__evalInspectPopupHide() { propPanel.panel.removeEventListener("popuphiding", onPopupHide, false); - this.clearObjectCache(options.data.panelCacheId); - - if (!aLinkNode.parentNode && aLinkNode._evalCacheId) { - this.clearObjectCache(aLinkNode._evalCacheId); + if (!aLinkNode.parentNode && aLinkNode._objectActors) { + aLinkNode._objectActors.forEach(this.hud._releaseObject, this.hud); + aLinkNode._objectActors = null; } }.bind(this); - propPanel.panel.addEventListener("popuphiding", onPopupHide, false); + this.hud.objectPropertiesProvider(aResponse.result.actor, + function _onObjectProperties(aProperties) { + options.data.objectProperties = aProperties; + propPanel = this.openPropertyPanel(options); + propPanel.panel.setAttribute("hudId", this.hudId); + propPanel.panel.addEventListener("popuphiding", onPopupHide, false); + }.bind(this)); }, /** @@ -3377,32 +3436,40 @@ JSTerm.prototype = { return; } - if (!aNewResponse.inspectable) { + let result = aNewResponse.result; + let inspectable = result && typeof result == "object" && result.inspectable; + let newActor = result && typeof result == "object" ? result.actor : null; + + let anchor = aOptions.anchor; + if (anchor && newActor) { + if (!anchor._objectActors) { + anchor._objectActors = []; + } + if (anchor._objectActors.indexOf(newActor) == -1) { + anchor._objectActors.push(newActor); + } + } + + if (!inspectable) { this.writeOutput(l10n.getStr("JSTerm.updateNotInspectable"), CATEGORY_OUTPUT, SEVERITY_ERROR); return; } - this.clearObjectCache(aOptions.data.panelCacheId); - this.clearObjectCache(aOptions.data.rootCacheId); - - if (aOptions.anchor && aOptions.anchor._evalCacheId) { - aOptions.anchor._evalCacheId = aNewResponse.childrenCacheId; - } - // Update the old response object such that when the panel is reopen, the // user sees the new response. - aOldResponse.id = aNewResponse.id; - aOldResponse.childrenCacheId = aNewResponse.childrenCacheId; - aOldResponse.resultObject = aNewResponse.resultObject; - aOldResponse.resultString = aNewResponse.resultString; + aOldResponse.result = aNewResponse.result; + aOldResponse.error = aNewResponse.error; + aOldResponse.errorMessage = aNewResponse.errorMessage; + aOldResponse.timestamp = aNewResponse.timestamp; - aOptions.data.rootCacheId = aNewResponse.childrenCacheId; - aOptions.data.remoteObject = aNewResponse.resultObject; - - // TODO: This updates the value of the tree. - // However, the states of open nodes is not saved. - // See bug 586246. - aPropPanel.treeView.data = aOptions.data; + this.hud.objectPropertiesProvider(newActor, + function _onObjectProperties(aProperties) { + aOptions.data.objectProperties = aProperties; + // TODO: This updates the value of the tree. + // However, the states of open nodes is not saved. + // See bug 586246. + aPropPanel.treeView.data = aOptions.data; + }.bind(this)); }, /** @@ -3633,6 +3700,7 @@ function WebConsoleConnectionProxy(aWebConsole) this.owner = aWebConsole; this._onPageError = this._onPageError.bind(this); + this._onConsoleAPICall = this._onConsoleAPICall.bind(this); } WebConsoleConnectionProxy.prototype = { @@ -3666,6 +3734,14 @@ WebConsoleConnectionProxy.prototype = { */ _consoleActor: null, + /** + * Tells if the window.console object of the remote web page is the native + * object or not. + * @private + * @type boolean + */ + _hasNativeConsoleAPI: false, + /** * Initialize the debugger server. */ @@ -3689,8 +3765,9 @@ WebConsoleConnectionProxy.prototype = { let client = this.client = new DebuggerClient(transport); client.addListener("pageError", this._onPageError); + client.addListener("consoleAPICall", this._onConsoleAPICall); - let listeners = ["PageError"]; + let listeners = ["PageError", "ConsoleAPI"]; client.connect(function(aType, aTraits) { client.listTabs(function(aResponse) { @@ -3725,6 +3802,36 @@ WebConsoleConnectionProxy.prototype = { this.webConsoleClient = aWebConsoleClient; + this._hasNativeConsoleAPI = aResponse.nativeConsoleAPI; + + let msgs = ["PageError", "ConsoleAPI"]; + this.webConsoleClient.getCachedMessages(msgs, + this._onCachedMessages.bind(this, aCallback)); + }, + + /** + * The "cachedMessages" response handler. + * + * @private + * @param function [aCallback] + * Optional function to invoke once the connection is established. + * @param object aResponse + * The JSON response object received from the server. + */ + _onCachedMessages: function WCCP__onCachedMessages(aCallback, aResponse) + { + if (aResponse.error) { + Cu.reportError("Web Console getCachedMessages error: " + aResponse.error + + " " + aResponse.message); + return; + } + + this.owner.displayCachedMessages(aResponse.messages); + + if (!this._hasNativeConsoleAPI) { + this.owner.logWarningAboutReplacedAPI(); + } + this.connected = true; aCallback && aCallback(); }, @@ -3746,6 +3853,36 @@ WebConsoleConnectionProxy.prototype = { } }, + /** + * The "consoleAPICall" message type handler. We redirect any message to + * the UI for displaying. + * + * @private + * @param string aType + * Message type. + * @param object aPacket + * The message received from the server. + */ + _onConsoleAPICall: function WCCP__onConsoleAPICall(aType, aPacket) + { + if (this.owner && aPacket.from == this._consoleActor) { + this.owner.handleConsoleAPICall(aPacket.message); + } + }, + + /** + * Release an object actor. + * + * @param string aActor + * The actor ID to send the request to. + */ + releaseActor: function WCCP_releaseActor(aActor) + { + if (this.client) { + this.client.release(aActor); + } + }, + /** * Disconnect the Web Console from the remote server. * @@ -3760,6 +3897,7 @@ WebConsoleConnectionProxy.prototype = { } this.client.removeListener("pageError", this._onPageError); + this.client.removeListener("consoleAPICall", this._onConsoleAPICall); this.client.close(aOnDisconnect); this.client = null; diff --git a/toolkit/devtools/debugger/dbg-client.jsm b/toolkit/devtools/debugger/dbg-client.jsm index adf95367c3c8..48df9c180cf1 100644 --- a/toolkit/devtools/debugger/dbg-client.jsm +++ b/toolkit/devtools/debugger/dbg-client.jsm @@ -167,6 +167,7 @@ const ThreadStateTypes = { * by the client. */ const UnsolicitedNotifications = { + "consoleAPICall": "consoleAPICall", "newScript": "newScript", "tabDetached": "tabDetached", "tabNavigated": "tabNavigated", @@ -371,6 +372,20 @@ DebuggerClient.prototype = { }); }, + /** + * Release an object actor. + * + * @param string aActor + * The actor ID to send the request to. + */ + release: function DC_release(aActor) { + let packet = { + to: aActor, + type: "release", + }; + this.request(packet); + }, + /** * Send a request to the debugging server. * diff --git a/toolkit/devtools/webconsole/WebConsoleClient.jsm b/toolkit/devtools/webconsole/WebConsoleClient.jsm index cf4fd743411b..45e7301118fd 100644 --- a/toolkit/devtools/webconsole/WebConsoleClient.jsm +++ b/toolkit/devtools/webconsole/WebConsoleClient.jsm @@ -48,6 +48,75 @@ WebConsoleClient.prototype = { this._client.request(packet, aOnResponse); }, + /** + * Inspect the properties of an object. + * + * @param string aActor + * The WebConsoleObjectActor ID to send the request to. + * @param function aOnResponse + * The function invoked when the response is received. + */ + inspectObjectProperties: + function WCC_inspectObjectProperties(aActor, aOnResponse) + { + let packet = { + to: aActor, + type: "inspectProperties", + }; + this._client.request(packet, aOnResponse); + }, + + /** + * Evaluate a JavaScript expression. + * + * @param string aString + * The code you want to evaluate. + * @param function aOnResponse + * The function invoked when the response is received. + */ + evaluateJS: function WCC_evaluateJS(aString, aOnResponse) + { + let packet = { + to: this._actor, + type: "evaluateJS", + text: aString, + }; + this._client.request(packet, aOnResponse); + }, + + /** + * Autocomplete a JavaScript expression. + * + * @param string aString + * The code you want to autocomplete. + * @param number aCursor + * Cursor location inside the string. Index starts from 0. + * @param function aOnResponse + * The function invoked when the response is received. + */ + autocomplete: function WCC_autocomplete(aString, aCursor, aOnResponse) + { + let packet = { + to: this._actor, + type: "autocomplete", + text: aString, + cursor: aCursor, + }; + this._client.request(packet, aOnResponse); + }, + + /** + * Clear the cache of messages (page errors and console API calls). + */ + clearMessagesCache: function WCC_clearMessagesCache() + { + let packet = { + to: this._actor, + type: "clearMessagesCache", + }; + this._client.request(packet); + }, + /** * Start the given Web Console listeners. * diff --git a/toolkit/devtools/webconsole/WebConsoleUtils.jsm b/toolkit/devtools/webconsole/WebConsoleUtils.jsm index c600eabf18ee..992f0aa9e412 100644 --- a/toolkit/devtools/webconsole/WebConsoleUtils.jsm +++ b/toolkit/devtools/webconsole/WebConsoleUtils.jsm @@ -15,8 +15,22 @@ Cu.import("resource://gre/modules/XPCOMUtils.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "Services", "resource://gre/modules/Services.jsm"); -var EXPORTED_SYMBOLS = ["WebConsoleUtils", "JSPropertyProvider", - "PageErrorListener"]; +XPCOMUtils.defineLazyModuleGetter(this, "ConsoleAPIStorage", + "resource://gre/modules/ConsoleAPIStorage.jsm"); + +var EXPORTED_SYMBOLS = ["WebConsoleUtils", "JSPropertyProvider", "JSTermHelpers", + "PageErrorListener", "ConsoleAPIListener"]; + +// Match the function name from the result of toString() or toSource(). +// +// Examples: +// (function foobar(a, b) { ... +// function foobar2(a) { ... +// function() { ... +const REGEX_MATCH_FUNCTION_NAME = /^\(?function\s+([^(\s]+)\s*\(/; + +// Match the function arguments from the result of toString() or toSource(). +const REGEX_MATCH_FUNCTION_ARGS = /^\(?function\s*[^\s(]*\s*\((.+?)\)/; const TYPES = { OBJECT: 0, FUNCTION: 1, @@ -213,7 +227,12 @@ var WebConsoleUtils = { case "error": case "number": case "regexp": - output = aResult.toString(); + try { + output = aResult + ""; + } + catch (ex) { + output = ex; + } break; case "null": case "undefined": @@ -309,8 +328,15 @@ var WebConsoleUtils = { getResultType: function WCU_getResultType(aResult) { let type = aResult === null ? "null" : typeof aResult; - if (type == "object" && aResult.constructor && aResult.constructor.name) { - type = aResult.constructor.name; + try { + if (type == "object" && aResult.constructor && aResult.constructor.name) { + type = aResult.constructor.name + ""; + } + } + catch (ex) { + // Prevent potential exceptions in page-provided objects from taking down + // the Web Console. If the constructor.name is a getter that throws, or + // something else bad happens. } return type.toLowerCase(); @@ -442,27 +468,43 @@ var WebConsoleUtils = { if (typeof aObject != "object") { return false; } - let desc; + let desc = this.getPropertyDescriptor(aObject, aProp); + return desc && desc.get && !this.isNativeFunction(desc.get); + }, + + /** + * Get the property descriptor for the given object. + * + * @param object aObject + * The object that contains the property. + * @param string aProp + * The property you want to get the descriptor for. + * @return object + * Property descriptor. + */ + getPropertyDescriptor: function WCU_getPropertyDescriptor(aObject, aProp) + { + let desc = null; while (aObject) { try { if (desc = Object.getOwnPropertyDescriptor(aObject, aProp)) { break; } } - catch (ex) { + catch (ex if (ex.name == "NS_ERROR_XPC_BAD_CONVERT_JS" || + ex.name == "NS_ERROR_XPC_BAD_OP_ON_WN_PROTO" || + ex.name == "TypeError")) { // Native getters throw here. See bug 520882. - if (ex.name == "NS_ERROR_XPC_BAD_CONVERT_JS" || - ex.name == "NS_ERROR_XPC_BAD_OP_ON_WN_PROTO") { - return false; - } - throw ex; + // null throws TypeError. + } + try { + aObject = Object.getPrototypeOf(aObject); + } + catch (ex if (ex.name == "TypeError")) { + return desc; } - aObject = Object.getPrototypeOf(aObject); } - if (desc && desc.get && !this.isNativeFunction(desc.get)) { - return true; - } - return false; + return desc; }, /** @@ -549,37 +591,209 @@ var WebConsoleUtils = { pairs.push(pair); } - pairs.sort(function(a, b) - { - // Convert the pair.name to a number for later sorting. - let aNumber = parseFloat(a.name); - let bNumber = parseFloat(b.name); - - // Sort numbers. - if (!isNaN(aNumber) && isNaN(bNumber)) { - return -1; - } - else if (isNaN(aNumber) && !isNaN(bNumber)) { - return 1; - } - else if (!isNaN(aNumber) && !isNaN(bNumber)) { - return aNumber - bNumber; - } - // Sort string. - else if (a.name < b.name) { - return -1; - } - else if (a.name > b.name) { - return 1; - } - else { - return 0; - } - }); + pairs.sort(this.propertiesSort); return pairs; }, + /** + * Sort function for object properties. + * + * @param object a + * Property descriptor. + * @param object b + * Property descriptor. + * @return integer + * -1 if a.name < b.name, + * 1 if a.name > b.name, + * 0 otherwise. + */ + propertiesSort: function WCU_propertiesSort(a, b) + { + // Convert the pair.name to a number for later sorting. + let aNumber = parseFloat(a.name); + let bNumber = parseFloat(b.name); + + // Sort numbers. + if (!isNaN(aNumber) && isNaN(bNumber)) { + return -1; + } + else if (isNaN(aNumber) && !isNaN(bNumber)) { + return 1; + } + else if (!isNaN(aNumber) && !isNaN(bNumber)) { + return aNumber - bNumber; + } + // Sort string. + else if (a.name < b.name) { + return -1; + } + else if (a.name > b.name) { + return 1; + } + else { + return 0; + } + }, + + /** + * Inspect the properties of the given object. For each property a descriptor + * object is created. The descriptor gives you information about the property + * name, value, type, getter and setter. When the property value references + * another object you get a wrapper that holds information about that object. + * + * @see this.inspectObjectProperty + * @param object aObject + * The object you want to inspect. + * @param function aObjectWrapper + * The function that creates wrappers for property values which + * reference other objects. This function must take one argument, the + * object to wrap, and it must return an object grip that gives + * information about the referenced object. + * @return array + * An array of property descriptors. + */ + inspectObject: function WCU_inspectObject(aObject, aObjectWrapper) + { + let properties = []; + let isDOMDocument = aObject instanceof Ci.nsIDOMDocument; + let deprecated = ["width", "height", "inputEncoding"]; + + for (let name in aObject) { + // See bug 632275: skip deprecated properties. + if (isDOMDocument && deprecated.indexOf(name) > -1) { + continue; + } + + properties.push(this.inspectObjectProperty(aObject, name, aObjectWrapper)); + } + + return properties.sort(this.propertiesSort); + }, + + /** + * A helper method that creates a property descriptor for the provided object, + * properly formatted for sending in a protocol response. + * + * The property value can reference other objects. Since actual objects cannot + * be sent to the client, we need to send simple object grips - descriptors + * for those objects. This is why you need to give an object wrapper function + * that creates object grips. + * + * @param string aProperty + * Property name for which we have the descriptor. + * @param object aObject + * The object that the descriptor is generated for. + * @param function aObjectWrapper + * This function is given the property value. Whatever the function + * returns is used as the representation of the property value. + * @return object + * The property descriptor formatted for sending to the client. + */ + inspectObjectProperty: + function WCU_inspectObjectProperty(aObject, aProperty, aObjectWrapper) + { + let descriptor = this.getPropertyDescriptor(aObject, aProperty) || {}; + + let result = { name: aProperty }; + result.configurable = descriptor.configurable; + result.enumerable = descriptor.enumerable; + result.writable = descriptor.writable; + if (descriptor.value !== undefined) { + result.value = this.createValueGrip(descriptor.value, aObjectWrapper); + } + else if (descriptor.get) { + if (this.isNativeFunction(descriptor.get)) { + result.value = this.createValueGrip(aObject[aProperty], aObjectWrapper); + } + else { + result.get = this.createValueGrip(descriptor.get, aObjectWrapper); + result.set = this.createValueGrip(descriptor.set, aObjectWrapper); + } + } + + // There are cases with properties that have no value and no getter. For + // example window.screen.width. + if (result.value === undefined && result.get === undefined) { + result.value = this.createValueGrip(aObject[aProperty], aObjectWrapper); + } + + return result; + }, + + /** + * Make an object grip for the given object. An object grip of the simplest + * form with minimal information about the given object is returned. This + * method is usually combined with other functions that add further state + * information and object ID such that, later, the client is able to retrieve + * more information about the object being represented by this grip. + * + * @param object aObject + * The object you want to create a grip for. + * @return object + * The object grip. + */ + getObjectGrip: function WCU_getObjectGrip(aObject) + { + let className = null; + let type = typeof aObject; + + let result = { + "type": type, + "className": this.getObjectClassName(aObject), + "displayString": this.formatResult(aObject), + "inspectable": this.isObjectInspectable(aObject), + }; + + if (type == "function") { + result.functionName = this.getFunctionName(aObject); + result.functionArguments = this.getFunctionArguments(aObject); + } + + return result; + }, + + /** + * Create a grip for the given value. If the value is an object, + * an object wrapper will be created. + * + * @param mixed aValue + * The value you want to create a grip for, before sending it to the + * client. + * @param function aObjectWrapper + * If the value is an object then the aObjectWrapper function is + * invoked to give us an object grip. See this.getObjectGrip(). + * @return mixed + * The value grip. + */ + createValueGrip: function WCU_createValueGrip(aValue, aObjectWrapper) + { + let type = typeof(aValue); + switch (type) { + case "boolean": + case "string": + case "number": + return aValue; + case "object": + case "function": + if (aValue) { + return aObjectWrapper(aValue); + } + default: + if (aValue === null) { + return { type: "null" }; + } + + if (aValue === undefined) { + return { type: "undefined" }; + } + + Cu.reportError("Failed to provide a grip for value of " + type + ": " + + aValue); + return null; + } + }, + /** * Check if the given object is an iterator or a generator. * @@ -638,6 +852,159 @@ var WebConsoleUtils = { return false; } }, + + /** + * Make a string representation for an object actor grip. + * + * @param object aGrip + * The object grip received from the server. + * @param boolean [aFormatString=false] + * Optional boolean that tells if you want strings to be unevaled or + * not. + * @return string + * The object grip converted to a string. + */ + objectActorGripToString: function WCU_objectActorGripToString(aGrip, aFormatString) + { + // Primitives like strings and numbers are not sent as objects. + // But null and undefined are sent as objects with the type property + // telling which type of value we have. + let type = typeof(aGrip); + if (aGrip && type == "object") { + return aGrip.displayString || aGrip.className || aGrip.type || type; + } + return type == "string" && aFormatString ? + this.formatResultString(aGrip) : aGrip + ""; + }, + + /** + * Helper function to deduce the name of the provided function. + * + * @param funtion aFunction + * The function whose name will be returned. + * @return string + * Function name. + */ + getFunctionName: function WCF_getFunctionName(aFunction) + { + let name = null; + if (aFunction.name) { + name = aFunction.name; + } + else { + let desc; + try { + desc = aFunction.getOwnPropertyDescriptor("displayName"); + } + catch (ex) { } + if (desc && typeof desc.value == "string") { + name = desc.value; + } + } + if (!name) { + try { + let str = (aFunction.toString() || aFunction.toSource()) + ""; + name = (str.match(REGEX_MATCH_FUNCTION_NAME) || [])[1]; + } + catch (ex) { } + } + return name; + }, + + /** + * Helper function to deduce the arguments of the provided function. + * + * @param funtion aFunction + * The function whose name will be returned. + * @return array + * Function arguments. + */ + getFunctionArguments: function WCF_getFunctionArguments(aFunction) + { + let args = []; + try { + let str = (aFunction.toString() || aFunction.toSource()) + ""; + let argsString = (str.match(REGEX_MATCH_FUNCTION_ARGS) || [])[1]; + if (argsString) { + args = argsString.split(/\s*,\s*/); + } + } + catch (ex) { } + return args; + }, + + /** + * Get the object class name. For example, the |window| object has the Window + * class name (based on [object Window]). + * + * @param object aObject + * The object you want to get the class name for. + * @return string + * The object class name. + */ + getObjectClassName: function WCF_getObjectClassName(aObject) + { + if (aObject === null) { + return "null"; + } + if (aObject === undefined) { + return "undefined"; + } + + let type = typeof aObject; + if (type != "object") { + return type; + } + + let className; + + try { + className = ((aObject + "").match(/^\[object (\S+)\]$/) || [])[1]; + if (!className) { + className = ((aObject.constructor + "").match(/^\[object (\S+)\]$/) || [])[1]; + } + if (!className && typeof aObject.constructor == "function") { + className = this.getFunctionName(aObject.constructor); + } + } + catch (ex) { } + + return className; + }, + + /** + * Determine the string to display as a property value in the property panel. + * + * @param object aActor + * Object actor grip. + * @return string + * Property value as suited for the property panel. + */ + getPropertyPanelValue: function WCU_getPropertyPanelValue(aActor) + { + if (aActor.get) { + return "Getter"; + } + + let val = aActor.value; + if (typeof val == "string") { + return this.formatResultString(val); + } + + if (typeof val != "object" || !val) { + return val; + } + + if (val.type == "function" && val.functionName) { + return "function " + val.functionName + "(" + + val.functionArguments.join(", ") + ")"; + } + if (val.type == "object" && val.className) { + return val.className; + } + + return val.displayString || val.type; + }, }; ////////////////////////////////////////////////////////////////////////// @@ -1163,3 +1530,327 @@ PageErrorListener.prototype = this.listener = this.window = null; }, }; + + +/////////////////////////////////////////////////////////////////////////////// +// The window.console API observer +/////////////////////////////////////////////////////////////////////////////// + +/** + * The window.console API observer. This allows the window.console API messages + * to be sent to the remote Web Console instance. + * + * @constructor + * @param nsIDOMWindow aWindow + * The window object for which we are created. + * @param object aOwner + * The owner object must have the following methods: + * - onConsoleAPICall(). This method is invoked with one argument, the + * Console API message that comes from the observer service, whenever + * a relevant console API call is received. + */ +function ConsoleAPIListener(aWindow, aOwner) +{ + this.window = aWindow; + this.owner = aOwner; +} + +ConsoleAPIListener.prototype = +{ + QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]), + + /** + * The content window for which we listen to window.console API calls. + * @type nsIDOMWindow + */ + window: null, + + /** + * The owner object which is notified of window.console API calls. It must + * have a onConsoleAPICall method which is invoked with one argument: the + * console API call object that comes from the observer service. + * + * @type object + * @see WebConsoleActor + */ + owner: null, + + /** + * Initialize the window.console API observer. + */ + init: function CAL_init() + { + // Note that the observer is process-wide. We will filter the messages as + // needed, see CAL_observe(). + Services.obs.addObserver(this, "console-api-log-event", false); + }, + + /** + * The console API message observer. When messages are received from the + * observer service we forward them to the remote Web Console instance. + * + * @param object aMessage + * The message object receives from the observer service. + * @param string aTopic + * The message topic received from the observer service. + */ + observe: function CAL_observe(aMessage, aTopic) + { + if (!this.owner || !this.window) { + return; + } + + let apiMessage = aMessage.wrappedJSObject; + let msgWindow = WebConsoleUtils.getWindowByOuterId(apiMessage.ID, + this.window); + if (!msgWindow || msgWindow.top != this.window) { + // Not the same window! + return; + } + + this.owner.onConsoleAPICall(apiMessage); + }, + + /** + * Get the cached messages for the current inner window. + * + * @return array + * The array of cached messages. Each element is a Console API + * prepared to be sent to the remote Web Console instance. + */ + getCachedMessages: function CAL_getCachedMessages() + { + let innerWindowId = WebConsoleUtils.getInnerWindowId(this.window); + let messages = ConsoleAPIStorage.getEvents(innerWindowId); + return messages; + }, + + /** + * Destroy the console API listener. + */ + destroy: function CAL_destroy() + { + Services.obs.removeObserver(this, "console-api-log-event"); + this.window = this.owner = null; + }, +}; + + + +/** + * JSTerm helper functions. + * + * Defines a set of functions ("helper functions") that are available from the + * Web Console but not from the web page. + * + * A list of helper functions used by Firebug can be found here: + * http://getfirebug.com/wiki/index.php/Command_Line_API + * + * @param object aOwner + * The owning object. + */ +function JSTermHelpers(aOwner) +{ + /** + * Find a node by ID. + * + * @param string aId + * The ID of the element you want. + * @return nsIDOMNode or null + * The result of calling document.querySelector(aSelector). + */ + aOwner.sandbox.$ = function JSTH_$(aSelector) + { + return aOwner.window.document.querySelector(aSelector); + }; + + /** + * Find the nodes matching a CSS selector. + * + * @param string aSelector + * A string that is passed to window.document.querySelectorAll. + * @return nsIDOMNodeList + * Returns the result of document.querySelectorAll(aSelector). + */ + aOwner.sandbox.$$ = function JSTH_$$(aSelector) + { + return aOwner.window.document.querySelectorAll(aSelector); + }; + + /** + * Runs an xPath query and returns all matched nodes. + * + * @param string aXPath + * xPath search query to execute. + * @param [optional] nsIDOMNode aContext + * Context to run the xPath query on. Uses window.document if not set. + * @return array of nsIDOMNode + */ + aOwner.sandbox.$x = function JSTH_$x(aXPath, aContext) + { + let nodes = []; + let doc = aOwner.window.document; + let aContext = aContext || doc; + + try { + let results = doc.evaluate(aXPath, aContext, null, + Ci.nsIDOMXPathResult.ANY_TYPE, null); + let node; + while (node = results.iterateNext()) { + nodes.push(node); + } + } + catch (ex) { + aOwner.window.console.error(ex.message); + } + + return nodes; + }; + + /** + * Returns the currently selected object in the highlighter. + * + * TODO: this implementation crosses the client/server boundaries! This is not + * usable within a remote browser. To implement this feature correctly we need + * support for remote inspection capabilities within the Inspector as well. + * See bug 787975. + * + * @return nsIDOMElement|null + * The DOM element currently selected in the highlighter. + */ + Object.defineProperty(aOwner.sandbox, "$0", { + get: function() { + try { + return aOwner.chromeWindow().InspectorUI.selection; + } + catch (ex) { + aOwner.window.console.error(ex.message); + } + }, + enumerable: true, + configurable: false + }); + + /** + * Clears the output of the JSTerm. + */ + aOwner.sandbox.clear = function JSTH_clear() + { + aOwner.helperResult = { + type: "clearOutput", + }; + }; + + /** + * Returns the result of Object.keys(aObject). + * + * @param object aObject + * Object to return the property names from. + * @return array of strings + */ + aOwner.sandbox.keys = function JSTH_keys(aObject) + { + return Object.keys(WebConsoleUtils.unwrap(aObject)); + }; + + /** + * Returns the values of all properties on aObject. + * + * @param object aObject + * Object to display the values from. + * @return array of string + */ + aOwner.sandbox.values = function JSTH_values(aObject) + { + let arrValues = []; + let obj = WebConsoleUtils.unwrap(aObject); + + try { + for (let prop in obj) { + arrValues.push(obj[prop]); + } + } + catch (ex) { + aOwner.window.console.error(ex.message); + } + + return arrValues; + }; + + /** + * Opens a help window in MDN. + */ + aOwner.sandbox.help = function JSTH_help() + { + aOwner.helperResult = { type: "help" }; + }; + + /** + * Inspects the passed aObject. This is done by opening the PropertyPanel. + * + * @param object aObject + * Object to inspect. + */ + aOwner.sandbox.inspect = function JSTH_inspect(aObject) + { + let obj = WebConsoleUtils.unwrap(aObject); + if (!WebConsoleUtils.isObjectInspectable(obj)) { + return aObject; + } + + aOwner.helperResult = { + type: "inspectObject", + input: aOwner.evalInput, + object: aOwner.createValueGrip(obj), + }; + }; + + /** + * Prints aObject to the output. + * + * @param object aObject + * Object to print to the output. + * @return string + */ + aOwner.sandbox.pprint = function JSTH_pprint(aObject) + { + if (aObject === null || aObject === undefined || aObject === true || + aObject === false) { + aOwner.helperResult = { + type: "error", + message: "helperFuncUnsupportedTypeError", + }; + return; + } + + aOwner.helperResult = { rawOutput: true }; + + if (typeof aObject == "function") { + return aObject + "\n"; + } + + let output = []; + let getObjectGrip = WebConsoleUtils.getObjectGrip.bind(WebConsoleUtils); + let obj = WebConsoleUtils.unwrap(aObject); + let props = WebConsoleUtils.inspectObject(obj, getObjectGrip); + props.forEach(function(aProp) { + output.push(aProp.name + ": " + + WebConsoleUtils.getPropertyPanelValue(aProp)); + }); + + return " " + output.join("\n "); + }; + + /** + * Print a string to the output, as-is. + * + * @param string aString + * A string you want to output. + * @return void + */ + aOwner.sandbox.print = function JSTH_print(aString) + { + aOwner.helperResult = { rawOutput: true }; + return String(aString); + }; +} diff --git a/toolkit/devtools/webconsole/dbg-webconsole-actors.js b/toolkit/devtools/webconsole/dbg-webconsole-actors.js index a85fd525f747..dec4f6754d4a 100644 --- a/toolkit/devtools/webconsole/dbg-webconsole-actors.js +++ b/toolkit/devtools/webconsole/dbg-webconsole-actors.js @@ -15,9 +15,25 @@ Cu.import("resource://gre/modules/XPCOMUtils.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "Services", "resource://gre/modules/Services.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "WebConsoleUtils", + "resource://gre/modules/devtools/WebConsoleUtils.jsm"); + XPCOMUtils.defineLazyModuleGetter(this, "PageErrorListener", "resource://gre/modules/devtools/WebConsoleUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "ConsoleAPIListener", + "resource://gre/modules/devtools/WebConsoleUtils.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "JSTermHelpers", + "resource://gre/modules/devtools/WebConsoleUtils.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "JSPropertyProvider", + "resource://gre/modules/devtools/WebConsoleUtils.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "ConsoleAPIStorage", + "resource://gre/modules/ConsoleAPIStorage.jsm"); + + /** * The WebConsoleActor implements capabilities needed for the Web Console * feature. @@ -32,6 +48,9 @@ function WebConsoleActor(aConnection, aTabActor) { this.conn = aConnection; this._browser = aTabActor.browser; + + this._objectActorsPool = new ActorPool(this.conn); + this.conn.addActorPool(this._objectActorsPool); } WebConsoleActor.prototype = @@ -43,6 +62,29 @@ WebConsoleActor.prototype = */ _browser: null, + /** + * Actor pool for all of the object actors for objects we send to the client. + * @private + * @type object + * @see ActorPool + * @see this.objectGrip() + */ + _objectActorsPool: null, + + /** + * Tells the current page location associated to the sandbox. When the page + * location is changed, we recreate the sandbox. + * @private + * @type object + */ + _sandboxLocation: null, + + /** + * The JavaScript Sandbox where code is evaluated. + * @type object + */ + sandbox: null, + /** * The debugger server connection instance. * @type object @@ -61,6 +103,11 @@ WebConsoleActor.prototype = */ pageErrorListener: null, + /** + * The ConsoleAPIListener instance. + */ + consoleAPIListener: null, + actorPrefix: "console", grip: function WCA_grip() @@ -68,6 +115,24 @@ WebConsoleActor.prototype = return { actor: this.actorID }; }, + /** + * Tells if the window.console object is native or overwritten by script in + * the page. + * + * @return boolean + * True if the window.console object is native, or false otherwise. + */ + hasNativeConsoleAPI: function WCA_hasNativeConsoleAPI() + { + let isNative = false; + try { + let consoleObject = WebConsoleUtils.unwrap(this.window).console; + isNative = "__mozillaConsole__" in consoleObject; + } + catch (ex) { } + return isNative; + }, + /** * Destroy the current WebConsoleActor instance. */ @@ -77,9 +142,69 @@ WebConsoleActor.prototype = this.pageErrorListener.destroy(); this.pageErrorListener = null; } + if (this.consoleAPIListener) { + this.consoleAPIListener.destroy(); + this.consoleAPIListener = null; + } + this.conn.removeActorPool(this._objectActorsPool); + this._objectActorsPool = null; + this._sandboxLocation = this.sandbox = null; this.conn = this._browser = null; }, + /** + * Create a grip for the given value. If the value is an object, + * a WebConsoleObjectActor will be created. + * + * @param mixed aValue + * @return object + */ + createValueGrip: function WCA_createValueGrip(aValue) + { + return WebConsoleUtils.createValueGrip(aValue, + this.createObjectActor.bind(this)); + }, + + /** + * Create a grip for the given object. + * + * @param object aObject + * The object you want. + * @param object + * The object grip. + */ + createObjectActor: function WCA_createObjectActor(aObject) + { + // We need to unwrap the object, otherwise we cannot access the properties + // and methods added by the content scripts. + let obj = WebConsoleUtils.unwrap(aObject); + let actor = new WebConsoleObjectActor(obj, this); + this._objectActorsPool.addActor(actor); + return actor.grip(); + }, + + /** + * Get an object actor by its ID. + * + * @param string aActorID + * @return object + */ + getObjectActorByID: function WCA_getObjectActorByID(aActorID) + { + return this._objectActorsPool.get(aActorID); + }, + + /** + * Release an object grip for the given object actor. + * + * @param object aActor + * The WebConsoleObjectActor instance you want to release. + */ + releaseObject: function WCA_releaseObject(aActor) + { + this._objectActorsPool.removeActor(aActor.actorID); + }, + /** * Handler for the "startListeners" request. * @@ -103,9 +228,20 @@ WebConsoleActor.prototype = } startedListeners.push(listener); break; + case "ConsoleAPI": + if (!this.consoleAPIListener) { + this.consoleAPIListener = + new ConsoleAPIListener(this.window, this); + this.consoleAPIListener.init(); + } + startedListeners.push(listener); + break; } } - return { startedListeners: startedListeners }; + return { + startedListeners: startedListeners, + nativeConsoleAPI: this.hasNativeConsoleAPI(), + }; }, /** @@ -123,7 +259,7 @@ WebConsoleActor.prototype = // If no specific listeners are requested to be detached, we stop all // listeners. - let toDetach = aRequest.listeners || ["PageError"]; + let toDetach = aRequest.listeners || ["PageError", "ConsoleAPI"]; while (toDetach.length > 0) { let listener = toDetach.shift(); @@ -135,12 +271,209 @@ WebConsoleActor.prototype = } stoppedListeners.push(listener); break; + case "ConsoleAPI": + if (this.consoleAPIListener) { + this.consoleAPIListener.destroy(); + this.consoleAPIListener = null; + } + stoppedListeners.push(listener); + break; } } return { stoppedListeners: stoppedListeners }; }, + /** + * Handler for the "getCachedMessages" request. This method sends the cached + * error messages and the window.console API calls to the client. + * + * @param object aRequest + * The JSON request object received from the Web Console client. + * @return object + * The response packet to send to the client: it holds the cached + * messages array. + */ + onGetCachedMessages: function WCA_onGetCachedMessages(aRequest) + { + let types = aRequest.messageTypes; + if (!types) { + return { + error: "missingParameter", + message: "The messageTypes parameter is missing.", + }; + } + + let messages = []; + + while (types.length > 0) { + let type = types.shift(); + switch (type) { + case "ConsoleAPI": + if (this.consoleAPIListener) { + let cache = this.consoleAPIListener.getCachedMessages(); + cache.forEach(function(aMessage) { + let message = this.prepareConsoleMessageForRemote(aMessage); + message._type = type; + messages.push(message); + }, this); + } + break; + case "PageError": + if (this.pageErrorListener) { + let cache = this.pageErrorListener.getCachedMessages(); + cache.forEach(function(aMessage) { + let message = this.preparePageErrorForRemote(aMessage); + message._type = type; + messages.push(message); + }, this); + } + break; + } + } + + messages.sort(function(a, b) { return a.timeStamp - b.timeStamp; }); + + return { + from: this.actorID, + messages: messages, + }; + }, + + /** + * Handler for the "evaluateJS" request. This method evaluates the given + * JavaScript string and sends back the result. + * + * @param object aRequest + * The JSON request object received from the Web Console client. + * @return object + * The evaluation response packet. + */ + onEvaluateJS: function WCA_onEvaluateJS(aRequest) + { + let input = aRequest.text; + let result, error = null; + let timestamp; + + this.helperResult = null; + this.evalInput = input; + try { + timestamp = Date.now(); + result = this.evalInSandbox(input); + } + catch (ex) { + error = ex; + } + + let helperResult = this.helperResult; + delete this.helperResult; + delete this.evalInput; + + return { + from: this.actorID, + input: input, + result: this.createValueGrip(result), + timestamp: timestamp, + error: error, + errorMessage: error ? String(error) : null, + helperResult: helperResult, + }; + }, + + /** + * The Autocomplete request handler. + * + * @param object aRequest + * The request message - what input to autocomplete. + * @return object + * The response message - matched properties. + */ + onAutocomplete: function WCA_onAutocomplete(aRequest) + { + let result = JSPropertyProvider(this.window, aRequest.text) || {}; + return { + from: this.actorID, + matches: result.matches || [], + matchProp: result.matchProp, + }; + }, + + /** + * The "clearMessagesCache" request handler. + */ + onClearMessagesCache: function WCA_onClearMessagesCache() + { + // TODO: Bug 717611 - Web Console clear button does not clear cached errors + let windowId = WebConsoleUtils.getInnerWindowId(this.window); + ConsoleAPIStorage.clearEvents(windowId); + return {}; + }, + + /** + * Create the JavaScript sandbox where user input is evaluated. + * @private + */ + _createSandbox: function WCA__createSandbox() + { + this._sandboxLocation = this.window.location; + this.sandbox = new Cu.Sandbox(this.window, { + sandboxPrototype: this.window, + wantXrays: false, + }); + + this.sandbox.console = this.window.console; + + JSTermHelpers(this); + }, + + /** + * Evaluates a string in the sandbox. + * + * @param string aString + * String to evaluate in the sandbox. + * @return mixed + * The result of the evaluation. + */ + evalInSandbox: function WCA_evalInSandbox(aString) + { + // If the user changed to a different location, we need to update the + // sandbox. + if (this._sandboxLocation !== this.window.location) { + this._createSandbox(); + } + + // The help function needs to be easy to guess, so we make the () optional + if (aString.trim() == "help" || aString.trim() == "?") { + aString = "help()"; + } + + let window = WebConsoleUtils.unwrap(this.sandbox.window); + let $ = null, $$ = null; + + // We prefer to execute the page-provided implementations for the $() and + // $$() functions. + if (typeof window.$ == "function") { + $ = this.sandbox.$; + delete this.sandbox.$; + } + if (typeof window.$$ == "function") { + $$ = this.sandbox.$$; + delete this.sandbox.$$; + } + + let result = Cu.evalInSandbox(aString, this.sandbox, "1.8", + "Web Console", 1); + + if ($) { + this.sandbox.$ = $; + } + if ($$) { + this.sandbox.$$ = $$; + } + + return result; + }, + /** * Handler for page errors received from the PageErrorListener. This method * sends the nsIScriptError to the remote Web Console client. @@ -183,11 +516,165 @@ WebConsoleActor.prototype = strict: !!(aPageError.flags & aPageError.strictFlag), }; }, + + /** + * Handler for window.console API calls received from the ConsoleAPIListener. + * This method sends the object to the remote Web Console client. + * + * @param object aMessage + * The console API call we need to send to the remote client. + */ + onConsoleAPICall: function WCA_onConsoleAPICall(aMessage) + { + let packet = { + from: this.actorID, + type: "consoleAPICall", + message: this.prepareConsoleMessageForRemote(aMessage), + }; + this.conn.send(packet); + }, + + /** + * Prepare a message from the console API to be sent to the remote Web Console + * instance. + * + * @param object aMessage + * The original message received from console-api-log-event. + * @return object + * The object that can be sent to the remote client. + */ + prepareConsoleMessageForRemote: + function WCA_prepareConsoleMessageForRemote(aMessage) + { + let result = { + level: aMessage.level, + filename: aMessage.filename, + lineNumber: aMessage.lineNumber, + functionName: aMessage.functionName, + timeStamp: aMessage.timeStamp, + }; + + switch (result.level) { + case "trace": + case "group": + case "groupCollapsed": + case "time": + case "timeEnd": + result.arguments = aMessage.arguments; + break; + default: + result.arguments = Array.map(aMessage.arguments || [], + function(aObj) { + return this.createValueGrip(aObj); + }, this); + + if (result.level == "dir") { + result.objectProperties = []; + let first = result.arguments[0]; + if (typeof first == "object" && first && first.inspectable) { + let actor = this.getObjectActorByID(first.actor); + result.objectProperties = actor.onInspectProperties().properties; + } + } + break; + } + + return result; + }, + + /** + * Find the XUL window that owns the content window. + * + * @return Window + * The XUL window that owns the content window. + */ + chromeWindow: function WCA_chromeWindow() + { + return this.window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation).QueryInterface(Ci.nsIDocShell) + .chromeEventHandler.ownerDocument.defaultView; + }, }; WebConsoleActor.prototype.requestTypes = { startListeners: WebConsoleActor.prototype.onStartListeners, stopListeners: WebConsoleActor.prototype.onStopListeners, + getCachedMessages: WebConsoleActor.prototype.onGetCachedMessages, + evaluateJS: WebConsoleActor.prototype.onEvaluateJS, + autocomplete: WebConsoleActor.prototype.onAutocomplete, + clearMessagesCache: WebConsoleActor.prototype.onClearMessagesCache, +}; + +/** + * Creates an actor for the specified object. + * + * @constructor + * @param object aObj + * The object you want. + * @param object aWebConsoleActor + * The parent WebConsoleActor instance for this object. + */ +function WebConsoleObjectActor(aObj, aWebConsoleActor) +{ + this.obj = aObj; + this.parent = aWebConsoleActor; +} + +WebConsoleObjectActor.prototype = +{ + actorPrefix: "consoleObj", + + /** + * Returns a grip for this actor for returning in a protocol message. + */ + grip: function WCOA_grip() + { + let grip = WebConsoleUtils.getObjectGrip(this.obj); + grip.actor = this.actorID; + return grip; + }, + + /** + * Releases this actor from the pool. + */ + release: function WCOA_release() + { + this.parent.releaseObject(this); + this.parent = this.obj = null; + }, + + /** + * Handle a protocol request to inspect the properties of the object. + * + * @return object + * Message to send to the client. This holds the 'properties' property + * - an array with a descriptor for each property in the object. + */ + onInspectProperties: function WCOA_onInspectProperties() + { + // TODO: Bug 787981 - use LongStringActor for strings that are too long. + let createObjectActor = this.parent.createObjectActor.bind(this.parent); + let props = WebConsoleUtils.inspectObject(this.obj, createObjectActor); + return { + from: this.actorID, + properties: props, + }; + }, + + /** + * Handle a protocol request to release a grip. + */ + onRelease: function WCOA_onRelease() + { + this.release(); + return {}; + }, +}; + +WebConsoleObjectActor.prototype.requestTypes = +{ + "inspectProperties": WebConsoleObjectActor.prototype.onInspectProperties, + "release": WebConsoleObjectActor.prototype.onRelease, }; From f068e8fc8bce9862d8221dbca1ff46127b395bdf Mon Sep 17 00:00:00 2001 From: Mihai Sucan Date: Fri, 5 Oct 2012 14:54:43 +0300 Subject: [PATCH 14/18] Bug 768096 - Web Console remote debugging protocol support - Part 3: network logging; r=past,robcee --HG-- rename : browser/devtools/webconsole/NetworkHelper.jsm => toolkit/devtools/webconsole/NetworkHelper.jsm --- .../devtools/webconsole/HUDService-content.js | 2 +- browser/devtools/webconsole/HUDService.jsm | 39 +- browser/devtools/webconsole/Makefile.in | 1 - browser/devtools/webconsole/NetworkPanel.jsm | 110 +- ..._webconsole_bug_599725_response_headers.js | 17 +- .../browser_webconsole_bug_600183_charset.js | 46 +- ...bconsole_bug_602572_log_bodies_checkbox.js | 26 +- ...le_bug_630733_response_redirect_headers.js | 102 +- .../test/browser_webconsole_bug_632817.js | 2 +- .../test/browser_webconsole_netlogging.js | 67 +- .../test/browser_webconsole_network_panel.js | 172 +-- browser/devtools/webconsole/webconsole.js | 481 +++++-- toolkit/devtools/debugger/dbg-client.jsm | 4 + .../devtools/webconsole/NetworkHelper.jsm | 22 +- .../devtools/webconsole/WebConsoleClient.jsm | 137 ++ .../devtools/webconsole/WebConsoleUtils.jsm | 1135 ++++++++++++++++- .../webconsole/dbg-webconsole-actors.js | 593 ++++++++- 17 files changed, 2577 insertions(+), 379 deletions(-) rename {browser => toolkit}/devtools/webconsole/NetworkHelper.jsm (95%) diff --git a/browser/devtools/webconsole/HUDService-content.js b/browser/devtools/webconsole/HUDService-content.js index 05bb00241cd3..d4aa54ff2dd4 100644 --- a/browser/devtools/webconsole/HUDService-content.js +++ b/browser/devtools/webconsole/HUDService-content.js @@ -18,7 +18,7 @@ Cu.import("resource://gre/modules/XPCOMUtils.jsm", tempScope); Cu.import("resource://gre/modules/Services.jsm", tempScope); Cu.import("resource://gre/modules/ConsoleAPIStorage.jsm", tempScope); Cu.import("resource://gre/modules/devtools/WebConsoleUtils.jsm", tempScope); -Cu.import("resource:///modules/NetworkHelper.jsm", tempScope); +Cu.import("resource://gre/modules/devtools/NetworkHelper.jsm", tempScope); Cu.import("resource://gre/modules/NetUtil.jsm", tempScope); let XPCOMUtils = tempScope.XPCOMUtils; diff --git a/browser/devtools/webconsole/HUDService.jsm b/browser/devtools/webconsole/HUDService.jsm index 945e21c1b5b7..02fd3452c531 100644 --- a/browser/devtools/webconsole/HUDService.jsm +++ b/browser/devtools/webconsole/HUDService.jsm @@ -529,28 +529,12 @@ WebConsole.prototype = { */ _asyncRequests: null, - /** - * Message names that the HUD listens for. These messages come from the remote - * Web Console content script. - * - * @private - * @type array - */ - _messageListeners: ["WebConsole:Initialized", "WebConsole:NetworkActivity", - "WebConsole:FileActivity", "WebConsole:LocationChange"], - /** * The xul:panel that holds the Web Console when it is positioned as a window. * @type nsIDOMElement */ consolePanel: null, - /** - * The current tab location. - * @type string - */ - contentLocation: "", - /** * Getter for the xul:popupset that holds any popups we open. * @type nsIDOMElement @@ -621,7 +605,6 @@ WebConsole.prototype = { this.iframeWindow = this.iframe.contentWindow.wrappedJSObject; this.ui = new this.iframeWindow.WebConsoleFrame(this, position); - this._setupMessageManager(); }, /** @@ -766,8 +749,8 @@ WebConsole.prototype = { */ getPanelTitle: function WC_getPanelTitle() { - return l10n.getFormatStr("webConsoleWindowTitleAndURL", - [this.contentLocation]); + let url = this.ui ? this.ui.contentLocation : ""; + return l10n.getFormatStr("webConsoleWindowTitleAndURL", [url]); }, positions: { @@ -991,16 +974,16 @@ WebConsole.prototype = { }, /** - * Handler for the "WebConsole:LocationChange" message. If the Web Console is + * Handler for page location changes. If the Web Console is * opened in a panel the panel title is updated. * - * @param object aMessage - * The message received from the content script. It needs to hold two - * properties: location and title. + * @param string aURI + * New page location. + * @param string aTitle + * New page title. */ - onLocationChange: function WC_onLocationChange(aMessage) + onLocationChange: function WC_onLocationChange(aURI, aTitle) { - this.contentLocation = aMessage.location; if (this.consolePanel) { this.consolePanel.label = this.getPanelTitle(); } @@ -1036,12 +1019,6 @@ WebConsole.prototype = { */ destroy: function WC_destroy(aOnDestroy) { - this.sendMessageToContent("WebConsole:Destroy", {}); - - this._messageListeners.forEach(function(aName) { - this.messageManager.removeMessageListener(aName, this.ui); - }, this); - // Make sure that the console panel does not try to call // deactivateHUDForContext() again. this.consoleWindowUnregisterOnHide = false; diff --git a/browser/devtools/webconsole/Makefile.in b/browser/devtools/webconsole/Makefile.in index 2fa5c1f0962c..ed66e5a9fb71 100644 --- a/browser/devtools/webconsole/Makefile.in +++ b/browser/devtools/webconsole/Makefile.in @@ -13,7 +13,6 @@ include $(DEPTH)/config/autoconf.mk EXTRA_JS_MODULES = \ HUDService.jsm \ PropertyPanel.jsm \ - NetworkHelper.jsm \ NetworkPanel.jsm \ AutocompletePopup.jsm \ $(NULL) diff --git a/browser/devtools/webconsole/NetworkPanel.jsm b/browser/devtools/webconsole/NetworkPanel.jsm index 91ce3fff4fe8..9c5d26faf588 100644 --- a/browser/devtools/webconsole/NetworkPanel.jsm +++ b/browser/devtools/webconsole/NetworkPanel.jsm @@ -16,7 +16,7 @@ XPCOMUtils.defineLazyServiceGetter(this, "mimeService", "@mozilla.org/mime;1", "nsIMIMEService"); XPCOMUtils.defineLazyModuleGetter(this, "NetworkHelper", - "resource:///modules/NetworkHelper.jsm"); + "resource://gre/modules/devtools/NetworkHelper.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", "resource://gre/modules/NetUtil.jsm"); @@ -66,7 +66,6 @@ function NetworkPanel(aParent, aHttpActivity) self.panel.parentNode.removeChild(self.panel); self.panel = null; self.iframe = null; - self.document = null; self.httpActivity = null; if (self.linkNode) { @@ -76,9 +75,17 @@ function NetworkPanel(aParent, aHttpActivity) }, false); // Set the document object and update the content once the panel is loaded. - this.panel.addEventListener("load", function onLoad() { - self.panel.removeEventListener("load", onLoad, true); - self.document = self.iframe.contentWindow.document; + this.iframe.addEventListener("load", function onLoad() { + if (!self.iframe) { + return; + } + + self.iframe.removeEventListener("load", onLoad, true); + self.update(); + }, true); + + this.panel.addEventListener("popupshown", function onPopupShown() { + self.panel.removeEventListener("popupshown", onPopupShown, true); self.update(); }, true); @@ -94,12 +101,6 @@ function NetworkPanel(aParent, aHttpActivity) NetworkPanel.prototype = { - /** - * Callback is called once the NetworkPanel is processed completely. Used by - * unit tests. - */ - isDoneCallback: null, - /** * The current state of the output. */ @@ -118,6 +119,20 @@ NetworkPanel.prototype = _contentType: null, + /** + * Function callback invoked whenever the panel content is updated. This is + * used only by tests. + * + * @private + * @type function + */ + _onUpdate: null, + + get document() { + return this.iframe && this.iframe.contentWindow ? + this.iframe.contentWindow.document : null; + }, + /** * Small helper function that is nearly equal to l10n.getFormatStr * except that it prefixes aName with "NetworkPanel.". @@ -150,9 +165,8 @@ NetworkPanel.prototype = return this._contentType; } - let entry = this.httpActivity.log.entries[0]; - let request = entry.request; - let response = entry.response; + let request = this.httpActivity.request; + let response = this.httpActivity.response; let contentType = ""; let types = response.content ? @@ -236,7 +250,7 @@ NetworkPanel.prototype = */ get _isResponseCached() { - return this.httpActivity.log.entries[0].response.status == 304; + return this.httpActivity.response.status == 304; }, /** @@ -247,7 +261,7 @@ NetworkPanel.prototype = */ get _isRequestBodyFormData() { - let requestBody = this.httpActivity.log.entries[0].request.postData.text; + let requestBody = this.httpActivity.request.postData.text; return this._fromDataRegExp.test(requestBody); }, @@ -341,9 +355,8 @@ NetworkPanel.prototype = */ _displayRequestHeader: function NP__displayRequestHeader() { - let entry = this.httpActivity.log.entries[0]; - let request = entry.request; - let requestTime = new Date(entry.startedDateTime); + let request = this.httpActivity.request; + let requestTime = new Date(this.httpActivity.startedDateTime); this._appendTextNode("headUrl", request.url); this._appendTextNode("headMethod", request.method); @@ -364,8 +377,9 @@ NetworkPanel.prototype = * * @returns void */ - _displayRequestBody: function NP__displayRequestBody() { - let postData = this.httpActivity.log.entries[0].request.postData; + _displayRequestBody: function NP__displayRequestBody() + { + let postData = this.httpActivity.request.postData; this._displayNode("requestBody"); this._appendTextNode("requestBodyContent", postData.text); }, @@ -376,8 +390,9 @@ NetworkPanel.prototype = * * @returns void */ - _displayRequestForm: function NP__processRequestForm() { - let postData = this.httpActivity.log.entries[0].request.postData.text; + _displayRequestForm: function NP__processRequestForm() + { + let postData = this.httpActivity.request.postData.text; let requestBodyLines = postData.split("\n"); let formData = requestBodyLines[requestBodyLines.length - 1]. replace(/\+/g, " ").split("&"); @@ -417,9 +432,8 @@ NetworkPanel.prototype = */ _displayResponseHeader: function NP__displayResponseHeader() { - let entry = this.httpActivity.log.entries[0]; - let timing = entry.timings; - let response = entry.response; + let timing = this.httpActivity.timings; + let response = this.httpActivity.response; this._appendTextNode("headStatus", [response.httpVersion, response.status, @@ -453,16 +467,16 @@ NetworkPanel.prototype = _displayResponseImage: function NP__displayResponseImage() { let self = this; - let entry = this.httpActivity.log.entries[0]; - let timing = entry.timings; - let request = entry.request; + let timing = this.httpActivity.timings; + let request = this.httpActivity.request; let cached = ""; if (this._isResponseCached) { cached = "Cached"; } - let imageNode = this.document.getElementById("responseImage" + cached +"Node"); + let imageNode = this.document.getElementById("responseImage" + + cached + "Node"); imageNode.setAttribute("src", request.url); // This function is called to set the imageInfo. @@ -498,9 +512,8 @@ NetworkPanel.prototype = */ _displayResponseBody: function NP__displayResponseBody() { - let entry = this.httpActivity.log.entries[0]; - let timing = entry.timings; - let response = entry.response; + let timing = this.httpActivity.timings; + let response = this.httpActivity.response; let cached = this._isResponseCached ? "Cached" : ""; this._appendTextNode("responseBody" + cached + "Info", @@ -519,7 +532,7 @@ NetworkPanel.prototype = */ _displayResponseBodyUnknownType: function NP__displayResponseBodyUnknownType() { - let timing = this.httpActivity.log.entries[0].timings; + let timing = this.httpActivity.timings; this._displayNode("responseBodyUnknownType"); this._appendTextNode("responseBodyUnknownTypeInfo", @@ -537,7 +550,7 @@ NetworkPanel.prototype = */ _displayNoResponseBody: function NP_displayNoResponseBody() { - let timing = this.httpActivity.log.entries[0].timings; + let timing = this.httpActivity.timings; this._displayNode("responseNoBody"); this._appendTextNode("responseNoBodyInfo", @@ -553,15 +566,14 @@ NetworkPanel.prototype = { // After the iframe's contentWindow is ready, the document object is set. // If the document object is not available yet nothing needs to be updated. - if (!this.document) { + if (!this.document || !this.document.getElementById("headUrl")) { return; } - let stages = this.httpActivity.meta.stages; - let entry = this.httpActivity.log.entries[0]; - let timing = entry.timings; - let request = entry.request; - let response = entry.response; + let updates = this.httpActivity.updates; + let timing = this.httpActivity.timings; + let request = this.httpActivity.request; + let response = this.httpActivity.response; switch (this._state) { case this._INIT: @@ -571,7 +583,7 @@ NetworkPanel.prototype = case this._DISPLAYED_REQUEST_HEADER: // Process the request body if there is one. - if (!this.httpActivity.meta.discardRequestBody && request.postData) { + if (!this.httpActivity.discardRequestBody && request.postData.text) { // Check if we send some form data. If so, display the form data special. if (this._isRequestBodyFormData) { this._displayRequestForm(); @@ -584,9 +596,6 @@ NetworkPanel.prototype = // FALL THROUGH case this._DISPLAYED_REQUEST_BODY: - // There is always a response header. Therefore we can skip here if - // we don't have a response header yet and don't have to try updating - // anything else in the NetworkPanel. if (!response.headers.length || !Object.keys(timing).length) { break; } @@ -595,13 +604,13 @@ NetworkPanel.prototype = // FALL THROUGH case this._DISPLAYED_RESPONSE_HEADER: - if (stages.indexOf("REQUEST_STOP") == -1 || - stages.indexOf("TRANSACTION_CLOSE") == -1) { + if (updates.indexOf("responseContent") == -1 || + updates.indexOf("eventTimings") == -1) { break; } this._state = this._TRANSITION_CLOSED; - if (this.httpActivity.meta.discardResponseBody) { + if (this.httpActivity.discardResponseBody) { break; } @@ -617,9 +626,12 @@ NetworkPanel.prototype = else if (response.content.text) { this._displayResponseBody(); } - break; } + + if (this._onUpdate) { + this._onUpdate(); + } } } diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_599725_response_headers.js b/browser/devtools/webconsole/test/browser_webconsole_bug_599725_response_headers.js index 6aa629ceea85..bc8b371fad34 100644 --- a/browser/devtools/webconsole/test/browser_webconsole_bug_599725_response_headers.js +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_599725_response_headers.js @@ -10,10 +10,12 @@ const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-bug-599725-response-headers.sjs"; -function performTest(lastFinishedRequest) +function performTest(lastFinishedRequest, aConsole) { ok(lastFinishedRequest, "page load was logged"); + let headers = null; + function readHeader(aName) { for (let header of headers) { @@ -24,13 +26,16 @@ function performTest(lastFinishedRequest) return null; } - let headers = lastFinishedRequest.log.entries[0].response.headers; - ok(headers, "we have the response headers"); - ok(!readHeader("Content-Type"), "we do not have the Content-Type header"); - isnot(readHeader("Content-Length"), 60, "Content-Length != 60"); + aConsole.webConsoleClient.getResponseHeaders(lastFinishedRequest.actor, + function (aResponse) { + headers = aResponse.headers; + ok(headers, "we have the response headers"); + ok(!readHeader("Content-Type"), "we do not have the Content-Type header"); + isnot(readHeader("Content-Length"), 60, "Content-Length != 60"); + executeSoon(finishTest); + }); HUDService.lastFinishedRequestCallback = null; - executeSoon(finishTest); } function test() diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_600183_charset.js b/browser/devtools/webconsole/test/browser_webconsole_bug_600183_charset.js index bc1b25b027b2..a65c4dfcd1dc 100644 --- a/browser/devtools/webconsole/test/browser_webconsole_bug_600183_charset.js +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_600183_charset.js @@ -10,39 +10,49 @@ const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-bug-600183-charset.html"; -function performTest(lastFinishedRequest) +function performTest(lastFinishedRequest, aConsole) { ok(lastFinishedRequest, "charset test page was loaded and logged"); - let body = lastFinishedRequest.log.entries[0].response.content.text; - ok(body, "we have the response body"); + aConsole.webConsoleClient.getResponseContent(lastFinishedRequest.actor, + function (aResponse) { + ok(!aResponse.contentDiscarded, "response body was not discarded"); - let chars = "\u7684\u95ee\u5019!"; // 的问候! - isnot(body.indexOf("

" + chars + "

"), -1, - "found the chinese simplified string"); + let body = aResponse.content.text; + ok(body, "we have the response body"); + + let chars = "\u7684\u95ee\u5019!"; // 的问候! + isnot(body.indexOf("

" + chars + "

"), -1, + "found the chinese simplified string"); + executeSoon(finishTest); + }); HUDService.lastFinishedRequestCallback = null; - executeSoon(finishTest); } function test() { addTab("data:text/html;charset=utf-8,Web Console - bug 600183 test"); - let initialLoad = true; - browser.addEventListener("load", function onLoad() { - if (initialLoad) { - openConsole(null, function(hud) { + browser.removeEventListener("load", onLoad, true); - hud.ui.saveRequestAndResponseBodies = true; - HUDService.lastFinishedRequestCallback = performTest; + openConsole(null, function(hud) { + hud.ui.saveRequestAndResponseBodies = true; - content.location = TEST_URI; + waitForSuccess({ + name: "saveRequestAndResponseBodies update", + validatorFn: function() + { + return hud.ui.saveRequestAndResponseBodies; + }, + successFn: function() + { + HUDService.lastFinishedRequestCallback = performTest; + content.location = TEST_URI; + }, + failureFn: finishTest, }); - initialLoad = false; - } else { - browser.removeEventListener("load", onLoad, true); - } + }); }, true); } diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_602572_log_bodies_checkbox.js b/browser/devtools/webconsole/test/browser_webconsole_bug_602572_log_bodies_checkbox.js index 9115014843af..5623f52b2cb2 100644 --- a/browser/devtools/webconsole/test/browser_webconsole_bug_602572_log_bodies_checkbox.js +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_602572_log_bodies_checkbox.js @@ -86,8 +86,17 @@ function onpopupshown2(aEvent) }); }, false); - executeSoon(function() { - menupopups[1].hidePopup(); + waitForSuccess({ + name: "saveRequestAndResponseBodies update", + validatorFn: function() + { + return huds[1].ui.saveRequestAndResponseBodies; + }, + successFn: function() + { + menupopups[1].hidePopup(); + }, + failureFn: finishTest, }); } @@ -147,8 +156,17 @@ function onpopupshown1(aEvent) }, tabs[runCount*2 + 1].linkedBrowser.contentWindow); }, false); - executeSoon(function() { - menupopups[0].hidePopup(); + waitForSuccess({ + name: "saveRequestAndResponseBodies update", + validatorFn: function() + { + return huds[0].ui.saveRequestAndResponseBodies; + }, + successFn: function() + { + menupopups[0].hidePopup(); + }, + failureFn: finishTest, }); } diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_630733_response_redirect_headers.js b/browser/devtools/webconsole/test/browser_webconsole_bug_630733_response_redirect_headers.js index ef77af4bb6b9..ef84dcadce9d 100644 --- a/browser/devtools/webconsole/test/browser_webconsole_bug_630733_response_redirect_headers.js +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_630733_response_redirect_headers.js @@ -10,20 +10,86 @@ const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-bug-630733-response-redirect-headers.sjs"; let lastFinishedRequests = {}; +let webConsoleClient; -function requestDoneCallback(aHttpRequest) +function requestDoneCallback(aHttpRequest ) { - let status = aHttpRequest.log.entries[0].response.status; + let status = aHttpRequest.response.status; lastFinishedRequests[status] = aHttpRequest; } -function performTest(aEvent) +function consoleOpened(hud) +{ + webConsoleClient = hud.ui.webConsoleClient; + hud.ui.saveRequestAndResponseBodies = true; + + waitForSuccess({ + name: "saveRequestAndResponseBodies update", + validatorFn: function() + { + return hud.ui.saveRequestAndResponseBodies; + }, + successFn: function() + { + HUDService.lastFinishedRequestCallback = requestDoneCallback; + waitForSuccess(waitForResponses); + content.location = TEST_URI; + }, + failureFn: finishTest, + }); + + let waitForResponses = { + name: "301 and 404 responses", + validatorFn: function() + { + return "301" in lastFinishedRequests && + "404" in lastFinishedRequests; + }, + successFn: getHeaders, + failureFn: finishTest, + }; +} + +function getHeaders() { HUDService.lastFinishedRequestCallback = null; ok("301" in lastFinishedRequests, "request 1: 301 Moved Permanently"); ok("404" in lastFinishedRequests, "request 2: 404 Not found"); + webConsoleClient.getResponseHeaders(lastFinishedRequests["301"].actor, + function (aResponse) { + lastFinishedRequests["301"].response.headers = aResponse.headers; + + webConsoleClient.getResponseHeaders(lastFinishedRequests["404"].actor, + function (aResponse) { + lastFinishedRequests["404"].response.headers = aResponse.headers; + executeSoon(getContent); + }); + }); +} + +function getContent() +{ + webConsoleClient.getResponseContent(lastFinishedRequests["301"].actor, + function (aResponse) { + lastFinishedRequests["301"].response.content = aResponse.content; + lastFinishedRequests["301"].discardResponseBody = aResponse.contentDiscarded; + + webConsoleClient.getResponseContent(lastFinishedRequests["404"].actor, + function (aResponse) { + lastFinishedRequests["404"].response.content = aResponse.content; + lastFinishedRequests["404"].discardResponseBody = + aResponse.contentDiscarded; + + webConsoleClient = null; + executeSoon(performTest); + }); + }); +} + +function performTest() +{ function readHeader(aName) { for (let header of headers) { @@ -34,7 +100,7 @@ function performTest(aEvent) return null; } - let headers = lastFinishedRequests["301"].log.entries[0].response.headers; + let headers = lastFinishedRequests["301"].response.headers; is(readHeader("Content-Type"), "text/html", "we do have the Content-Type header"); is(readHeader("Content-Length"), 71, "Content-Length is correct"); @@ -42,14 +108,17 @@ function performTest(aEvent) "Content-Length is correct"); is(readHeader("x-foobar-bug630733"), "bazbaz", "X-Foobar-bug630733 is correct"); - let body = lastFinishedRequests["301"].log.entries[0].response.content; - ok(!body.text, "body discarded for request 1"); - headers = lastFinishedRequests["404"].log.entries[0].response.headers; + let body = lastFinishedRequests["301"].response.content; + ok(!body.text, "body discarded for request 1"); + ok(lastFinishedRequests["301"].discardResponseBody, + "body discarded for request 1 (confirmed)"); + + headers = lastFinishedRequests["404"].response.headers; ok(!readHeader("Location"), "no Location header"); ok(!readHeader("x-foobar-bug630733"), "no X-Foobar-bug630733 header"); - body = lastFinishedRequests["404"].log.entries[0].response.content.text; + body = lastFinishedRequests["404"].response.content.text; isnot(body.indexOf("404"), -1, "body is correct for request 2"); @@ -61,19 +130,8 @@ function test() { addTab("data:text/html;charset=utf-8,

Web Console test for bug 630733"); - browser.addEventListener("load", function onLoad1(aEvent) { - browser.removeEventListener(aEvent.type, onLoad1, true); - - openConsole(null, function(hud) { - hud.ui.saveRequestAndResponseBodies = true; - HUDService.lastFinishedRequestCallback = requestDoneCallback; - - browser.addEventListener("load", function onLoad2(aEvent) { - browser.removeEventListener(aEvent.type, onLoad2, true); - executeSoon(performTest); - }, true); - - content.location = TEST_URI; - }); + browser.addEventListener("load", function onLoad(aEvent) { + browser.removeEventListener(aEvent.type, onLoad, true); + openConsole(null, consoleOpened); }, true); } diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_632817.js b/browser/devtools/webconsole/test/browser_webconsole_bug_632817.js index f717d329cc93..ba4b5e628e25 100644 --- a/browser/devtools/webconsole/test/browser_webconsole_bug_632817.js +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_632817.js @@ -25,7 +25,7 @@ function test() hud = aHud; HUDService.lastFinishedRequestCallback = function(aRequest) { - lastRequest = aRequest.log.entries[0]; + lastRequest = aRequest; if (requestCallback) { requestCallback(); } diff --git a/browser/devtools/webconsole/test/browser_webconsole_netlogging.js b/browser/devtools/webconsole/test/browser_webconsole_netlogging.js index be698f41aaa5..62dc1995247b 100644 --- a/browser/devtools/webconsole/test/browser_webconsole_netlogging.js +++ b/browser/devtools/webconsole/test/browser_webconsole_netlogging.js @@ -21,7 +21,6 @@ const TEST_DATA_JSON_CONTENT = let lastRequest = null; let requestCallback = null; -let lastActivity = null; function test() { @@ -33,19 +32,34 @@ function test() openConsole(null, function(aHud) { hud = aHud; - HUDService.lastFinishedRequestCallback = function(aRequest) { - lastRequest = aRequest.log.entries[0]; - lastActivity = aRequest; - if (requestCallback) { - requestCallback(); - } - }; + HUDService.lastFinishedRequestCallback = requestCallbackWrapper; executeSoon(testPageLoad); }); }, true); } +function requestCallbackWrapper(aRequest) +{ + lastRequest = aRequest; + + hud.ui.webConsoleClient.getResponseContent(lastRequest.actor, + function(aResponse) { + lastRequest.response.content = aResponse.content; + lastRequest.discardResponseBody = aResponse.contentDiscarded; + + hud.ui.webConsoleClient.getRequestPostData(lastRequest.actor, + function(aResponse) { + lastRequest.request.postData = aResponse.postData; + lastRequest.discardRequestBody = aResponse.postDataDiscarded; + + if (requestCallback) { + requestCallback(); + } + }); + }); +} + function testPageLoad() { requestCallback = function() { @@ -55,8 +69,10 @@ function testPageLoad() is(lastRequest.request.url, TEST_NETWORK_REQUEST_URI, "Logged network entry is page load"); is(lastRequest.request.method, "GET", "Method is correct"); - ok(!lastRequest.request.postData, "No request body was stored"); + ok(!lastRequest.request.postData.text, "No request body was stored"); + ok(lastRequest.discardRequestBody, "Request body was discarded"); ok(!lastRequest.response.content.text, "No response body was stored"); + ok(lastRequest.discardResponseBody, "Response body was discarded"); lastRequest = null; requestCallback = null; @@ -67,14 +83,29 @@ function testPageLoad() } function testPageLoadBody() +{ + // Turn on logging of request bodies and check again. + hud.ui.saveRequestAndResponseBodies = true; + + waitForSuccess({ + name: "saveRequestAndResponseBodies update", + validatorFn: function() + { + return hud.ui.saveRequestAndResponseBodies; + }, + successFn: testPageLoadBodyAfterSettingUpdate, + failureFn: finishTest, + }); +} + +function testPageLoadBodyAfterSettingUpdate() { let loaded = false; let requestCallbackInvoked = false; - // Turn on logging of request bodies and check again. - hud.ui.saveRequestAndResponseBodies = true; requestCallback = function() { ok(lastRequest, "Page load was logged again"); + ok(!lastRequest.discardResponseBody, "Response body was not discarded"); is(lastRequest.response.content.text.indexOf(""), 0, "Response body's beginning is okay"); @@ -104,7 +135,8 @@ function testXhrGet() requestCallback = function() { ok(lastRequest, "testXhrGet() was logged"); is(lastRequest.request.method, "GET", "Method is correct"); - ok(!lastRequest.request.postData, "No request body was sent"); + ok(!lastRequest.request.postData.text, "No request body was sent"); + ok(!lastRequest.discardRequestBody, "Request body was not discarded"); is(lastRequest.response.content.text, TEST_DATA_JSON_CONTENT, "Response is correct"); @@ -165,19 +197,18 @@ function testNetworkPanel() { // Open the NetworkPanel. The functionality of the NetworkPanel is tested // within separate test files. - let networkPanel = hud.ui.openNetworkPanel(hud.ui.filterBox, lastActivity); - is(networkPanel, hud.ui.filterBox._netPanel, - "Network panel stored on anchor node"); + let networkPanel = hud.ui.openNetworkPanel(hud.ui.filterBox, lastRequest); - networkPanel.panel.addEventListener("load", function onLoad(aEvent) { - networkPanel.panel.removeEventListener(aEvent.type, onLoad, true); + networkPanel.panel.addEventListener("popupshown", function onPopupShown() { + networkPanel.panel.removeEventListener("popupshown", onPopupShown, true); + is(hud.ui.filterBox._netPanel, networkPanel, + "Network panel stored on anchor node"); ok(true, "NetworkPanel was opened"); // All tests are done. Shutdown. networkPanel.panel.hidePopup(); lastRequest = null; - lastActivity = null; HUDService.lastFinishedRequestCallback = null; executeSoon(finishTest); }, true); diff --git a/browser/devtools/webconsole/test/browser_webconsole_network_panel.js b/browser/devtools/webconsole/test/browser_webconsole_network_panel.js index d0c1a5b37f87..138285b3b687 100644 --- a/browser/devtools/webconsole/test/browser_webconsole_network_panel.js +++ b/browser/devtools/webconsole/test/browser_webconsole_network_panel.js @@ -65,42 +65,36 @@ function testGen() { let filterBox = hud.ui.filterBox; let httpActivity = { - meta: { - stages: [], - discardRequestBody: true, - discardResponseBody: true, + updates: [], + discardRequestBody: true, + discardResponseBody: true, + startedDateTime: (new Date()).toISOString(), + request: { + url: "http://www.testpage.com", + method: "GET", + cookies: [], + headers: [ + { name: "foo", value: "bar" }, + ], }, - log: { - entries: [{ - startedDateTime: (new Date()).toISOString(), - request: { - url: "http://www.testpage.com", - method: "GET", - cookies: [], - headers: [ - { name: "foo", value: "bar" }, - ], - }, - response: { - headers: [], - content: {}, - }, - timings: {}, - }], + response: { + headers: [], + content: {}, }, + timings: {}, }; - let entry = httpActivity.log.entries[0]; - let networkPanel = hud.ui.openNetworkPanel(filterBox, httpActivity); is(filterBox._netPanel, networkPanel, "Network panel stored on the anchor object"); - networkPanel.panel.addEventListener("load", function onLoad() { - networkPanel.panel.removeEventListener("load", onLoad, true); - testDriver.next(); - }, true); + networkPanel._onUpdate = function() { + networkPanel._onUpdate = null; + executeSoon(function() { + testDriver.next(); + }); + }; yield; @@ -123,8 +117,8 @@ function testGen() { // Test request body. info("test 2: request body"); - httpActivity.meta.discardRequestBody = false; - entry.request.postData = { text: "hello world" }; + httpActivity.discardRequestBody = false; + httpActivity.request.postData = { text: "hello world" }; networkPanel.update(); checkIsVisible(networkPanel, { @@ -141,12 +135,12 @@ function testGen() { // Test response header. info("test 3: response header"); - entry.timings.wait = 10; - entry.response.httpVersion = "HTTP/3.14"; - entry.response.status = 999; - entry.response.statusText = "earthquake win"; - entry.response.content.mimeType = "text/html"; - entry.response.headers.push( + httpActivity.timings.wait = 10; + httpActivity.response.httpVersion = "HTTP/3.14"; + httpActivity.response.status = 999; + httpActivity.response.statusText = "earthquake win"; + httpActivity.response.content.mimeType = "text/html"; + httpActivity.response.headers.push( { name: "Content-Type", value: "text/html" }, { name: "leaveHouses", value: "true" } ); @@ -170,8 +164,8 @@ function testGen() { info("test 4"); - httpActivity.meta.discardResponseBody = false; - entry.timings.receive = 2; + httpActivity.discardResponseBody = false; + httpActivity.timings.receive = 2; networkPanel.update(); checkIsVisible(networkPanel, { @@ -187,7 +181,7 @@ function testGen() { info("test 5"); - httpActivity.meta.stages.push("REQUEST_STOP", "TRANSACTION_CLOSE"); + httpActivity.updates.push("responseContent", "eventTimings"); networkPanel.update(); checkNodeContent(networkPanel, "responseNoBodyInfo", "2ms"); @@ -205,20 +199,22 @@ function testGen() { // Second run: Test for cookies and response body. info("test 6: cookies and response body"); - entry.request.cookies.push( + httpActivity.request.cookies.push( { name: "foo", value: "bar" }, { name: "hello", value: "world" } ); - entry.response.content.text = "get out here"; + httpActivity.response.content.text = "get out here"; networkPanel = hud.ui.openNetworkPanel(filterBox, httpActivity); is(filterBox._netPanel, networkPanel, "Network panel stored on httpActivity object"); - networkPanel.panel.addEventListener("load", function onLoad() { - networkPanel.panel.removeEventListener("load", onLoad, true); - testDriver.next(); - }, true); + networkPanel._onUpdate = function() { + networkPanel._onUpdate = null; + executeSoon(function() { + testDriver.next(); + }); + }; yield; @@ -242,15 +238,17 @@ function testGen() { // Check image request. info("test 7: image request"); - entry.response.headers[1].value = "image/png"; - entry.response.content.mimeType = "image/png"; - entry.request.url = TEST_IMG; + httpActivity.response.headers[1].value = "image/png"; + httpActivity.response.content.mimeType = "image/png"; + httpActivity.request.url = TEST_IMG; networkPanel = hud.ui.openNetworkPanel(filterBox, httpActivity); - networkPanel.panel.addEventListener("load", function onLoad() { - networkPanel.panel.removeEventListener("load", onLoad, true); - testDriver.next(); - }, true); + networkPanel._onUpdate = function() { + networkPanel._onUpdate = null; + executeSoon(function() { + testDriver.next(); + }); + }; yield; @@ -291,15 +289,17 @@ function testGen() { // Check cached image request. info("test 8: cached image request"); - entry.response.httpVersion = "HTTP/1.1"; - entry.response.status = 304; - entry.response.statusText = "Not Modified"; + httpActivity.response.httpVersion = "HTTP/1.1"; + httpActivity.response.status = 304; + httpActivity.response.statusText = "Not Modified"; networkPanel = hud.ui.openNetworkPanel(filterBox, httpActivity); - networkPanel.panel.addEventListener("load", function onLoad() { - networkPanel.panel.removeEventListener("load", onLoad, true); - testDriver.next(); - }, true); + networkPanel._onUpdate = function() { + networkPanel._onUpdate = null; + executeSoon(function() { + testDriver.next(); + }); + }; yield; @@ -321,17 +321,19 @@ function testGen() { // Test sent form data. info("test 9: sent form data"); - entry.request.postData.text = [ + httpActivity.request.postData.text = [ "Content-Type: application/x-www-form-urlencoded", "Content-Length: 59", "name=rob&age=20" ].join("\n"); networkPanel = hud.ui.openNetworkPanel(filterBox, httpActivity); - networkPanel.panel.addEventListener("load", function onLoad() { - networkPanel.panel.removeEventListener("load", onLoad, true); - testDriver.next(); - }, true); + networkPanel._onUpdate = function() { + networkPanel._onUpdate = null; + executeSoon(function() { + testDriver.next(); + }); + }; yield; @@ -352,13 +354,15 @@ function testGen() { // Test no space after Content-Type: info("test 10: no space after Content-Type header in post data"); - entry.request.postData.text = "Content-Type:application/x-www-form-urlencoded\n"; + httpActivity.request.postData.text = "Content-Type:application/x-www-form-urlencoded\n"; networkPanel = hud.ui.openNetworkPanel(filterBox, httpActivity); - networkPanel.panel.addEventListener("load", function onLoad() { - networkPanel.panel.removeEventListener("load", onLoad, true); - testDriver.next(); - }, true); + networkPanel._onUpdate = function() { + networkPanel._onUpdate = null; + executeSoon(function() { + testDriver.next(); + }); + }; yield; @@ -379,16 +383,18 @@ function testGen() { info("test 11: cached data"); - entry.request.url = TEST_ENCODING_ISO_8859_1; - entry.response.headers[1].value = "application/json"; - entry.response.content.mimeType = "application/json"; - entry.response.content.text = "my cached data is here!"; + httpActivity.request.url = TEST_ENCODING_ISO_8859_1; + httpActivity.response.headers[1].value = "application/json"; + httpActivity.response.content.mimeType = "application/json"; + httpActivity.response.content.text = "my cached data is here!"; networkPanel = hud.ui.openNetworkPanel(filterBox, httpActivity); - networkPanel.panel.addEventListener("load", function onLoad() { - networkPanel.panel.removeEventListener("load", onLoad, true); - testDriver.next(); - }, true); + networkPanel._onUpdate = function() { + networkPanel._onUpdate = null; + executeSoon(function() { + testDriver.next(); + }); + }; yield; @@ -412,14 +418,16 @@ function testGen() { // Test a response with a content type that can't be displayed in the // NetworkPanel. info("test 12: unknown content type"); - entry.response.headers[1].value = "application/x-shockwave-flash"; - entry.response.content.mimeType = "application/x-shockwave-flash"; + httpActivity.response.headers[1].value = "application/x-shockwave-flash"; + httpActivity.response.content.mimeType = "application/x-shockwave-flash"; networkPanel = hud.ui.openNetworkPanel(filterBox, httpActivity); - networkPanel.panel.addEventListener("load", function onLoad() { - networkPanel.panel.removeEventListener("load", onLoad, true); - testDriver.next(); - }, true); + networkPanel._onUpdate = function() { + networkPanel._onUpdate = null; + executeSoon(function() { + testDriver.next(); + }); + }; yield; diff --git a/browser/devtools/webconsole/webconsole.js b/browser/devtools/webconsole/webconsole.js index ab9742080f91..703b9e4ed4cb 100644 --- a/browser/devtools/webconsole/webconsole.js +++ b/browser/devtools/webconsole/webconsole.js @@ -216,13 +216,6 @@ WebConsoleFrame.prototype = { */ proxy: null, - /** - * Tells if the Web Console initialization via message manager completed. - * @private - * @type boolean - */ - _messageManagerInitComplete: false, - /** * Getter for the xul:popupset that holds any popups we open. * @type nsIDOMElement @@ -290,6 +283,12 @@ WebConsoleFrame.prototype = { */ groupDepth: 0, + /** + * The current tab location. + * @type string + */ + contentLocation: "", + /** * The JSTerm object that manage the console's input. * @see JSTerm @@ -331,16 +330,16 @@ WebConsoleFrame.prototype = { * The new value you want to set. */ set saveRequestAndResponseBodies(aValue) { - this._saveRequestAndResponseBodies = aValue; - - let message = { - preferences: { - "NetworkMonitor.saveRequestAndResponseBodies": - this._saveRequestAndResponseBodies, - }, + let newValue = !!aValue; + let preferences = { + "NetworkMonitor.saveRequestAndResponseBodies": newValue, }; - this.owner.sendMessageToContent("WebConsole:SetPreferences", message); + this.webConsoleClient.setPreferences(preferences, function(aResponse) { + if (!aResponse.error) { + this._saveRequestAndResponseBodies = newValue; + } + }.bind(this)); }, /** @@ -352,9 +351,8 @@ WebConsoleFrame.prototype = { this.proxy = new WebConsoleConnectionProxy(this); this.proxy.initServer(); this.proxy.connect(function() { - if (this._messageManagerInitComplete) { - this._onInitComplete(); - } + this.saveRequestAndResponseBodies = this._saveRequestAndResponseBodies; + this._onInitComplete(); }.bind(this)); }, @@ -653,50 +651,6 @@ WebConsoleFrame.prototype = { } }, - /** - * Handler for all of the messages coming from the Web Console content script. - * - * @private - * @param object aMessage - * A MessageManager object that holds the remote message. - */ - receiveMessage: function WCF_receiveMessage(aMessage) - { - if (!aMessage.json || aMessage.json.hudId != this.hudId) { - return; - } - - switch (aMessage.name) { - case "WebConsole:Initialized": - this._onMessageManagerInitComplete(); - break; - case "WebConsole:NetworkActivity": - this.handleNetworkActivity(aMessage.json); - break; - case "WebConsole:FileActivity": - this.outputMessage(CATEGORY_NETWORK, this.logFileActivity, - [aMessage.json.uri]); - break; - case "WebConsole:LocationChange": - this.owner.onLocationChange(aMessage.json); - break; - } - }, - - /** - * Callback method used to track the Web Console initialization via message - * manager. - * - * @private - */ - _onMessageManagerInitComplete: function WCF__onMessageManagerInitComplete() - { - this._messageManagerInitComplete = true; - if (this.proxy.connected) { - this._onInitComplete(); - } - }, - /** * The event handler that is called whenever a user switches a filter on or * off. @@ -1304,22 +1258,21 @@ WebConsoleFrame.prototype = { }, /** - * Log network activity. + * Log network event. * - * @param object aHttpActivity - * The HTTP activity to log. + * @param object aActorId + * The network event actor ID to log. * @return nsIDOMElement|undefined * The message element to display in the Web Console output. */ - logNetActivity: function WCF_logNetActivity(aConnectionId) + logNetEvent: function WCF_logNetEvent(aActorId) { - let networkInfo = this._networkRequests[aConnectionId]; + let networkInfo = this._networkRequests[aActorId]; if (!networkInfo) { return; } - let entry = networkInfo.httpActivity.log.entries[0]; - let request = entry.request; + let request = networkInfo.request; let msgNode = this.document.createElementNS(XUL_NS, "hbox"); @@ -1347,8 +1300,7 @@ WebConsoleFrame.prototype = { let severity = SEVERITY_LOG; let mixedRequest = - WebConsoleUtils.isMixedHTTPSRequest(request.url, - this.owner.contentLocation); + WebConsoleUtils.isMixedHTTPSRequest(request.url, this.contentLocation); if (mixedRequest) { urlNode.classList.add("webconsole-mixed-content"); this.makeMixedContentNode(linkNode); @@ -1369,18 +1321,18 @@ WebConsoleFrame.prototype = { let messageNode = this.createMessageNode(CATEGORY_NETWORK, severity, msgNode, null, null, clipboardText); - messageNode._connectionId = entry.connection; + messageNode._connectionId = aActorId; messageNode.url = request.url; this.makeOutputMessageLink(messageNode, function WCF_net_message_link() { if (!messageNode._panelOpen) { - this.openNetworkPanel(messageNode, networkInfo.httpActivity); + this.openNetworkPanel(messageNode, networkInfo); } }.bind(this)); networkInfo.node = messageNode; - this._updateNetMessage(entry.connection); + this._updateNetMessage(aActorId); return messageNode; }, @@ -1441,6 +1393,17 @@ WebConsoleFrame.prototype = { return outputNode; }, + /** + * Handle the file activity messages coming from the remote Web Console. + * + * @param string aFileURI + * The file URI that was requested. + */ + handleFileActivity: function WCF_handleFileActivity(aFileURI) + { + this.outputMessage(CATEGORY_NETWORK, this.logFileActivity, [aFileURI]); + }, + /** * Inform user that the Web Console API has been replaced by a script * in a content page. @@ -1453,86 +1416,122 @@ WebConsoleFrame.prototype = { }, /** - * Handle the "WebConsole:NetworkActivity" message coming from the remote Web - * Console. + * Handle the network events coming from the remote Web Console. * - * @param object aMessage - * The HTTP activity object. This object needs to hold two properties: - * - meta - some metadata about the request log: - * - stages - the stages the network request went through. - * - discardRequestBody and discardResponseBody - booleans that tell - * if the network request/response body was discarded or not. - * - log - the request and response information. This is a HAR-like - * object. See HUDService-content.js - * NetworkMonitor.createActivityObject(). + * @param object aActor + * The NetworkEventActor grip. */ - handleNetworkActivity: function WCF_handleNetworkActivity(aMessage) + handleNetworkEvent: function WCF_handleNetworkEvent(aActor) { - let stage = aMessage.meta.stages[aMessage.meta.stages.length - 1]; - let entry = aMessage.log.entries[0]; + let networkInfo = { + node: null, + actor: aActor.actor, + discardRequestBody: true, + discardResponseBody: true, + startedDateTime: aActor.startedDateTime, + request: { + url: aActor.url, + method: aActor.method, + }, + response: {}, + timings: {}, + updates: [], // track the list of network event updates + }; - if (stage == "REQUEST_HEADER") { - let networkInfo = { - node: null, - httpActivity: aMessage, - }; + this._networkRequests[aActor.actor] = networkInfo; + this.outputMessage(CATEGORY_NETWORK, this.logNetEvent, [aActor.actor]); + }, - this._networkRequests[entry.connection] = networkInfo; - this.outputMessage(CATEGORY_NETWORK, this.logNetActivity, - [entry.connection]); - return; - } - else if (!(entry.connection in this._networkRequests)) { + /** + * Handle network event updates coming from the server. + * + * @param string aActorId + * The network event actor ID. + * @param string aType + * Update type. + * @param object aPacket + * Update details. + */ + handleNetworkEventUpdate: + function WCF_handleNetworkEventUpdate(aActorId, aType, aPacket) + { + let networkInfo = this._networkRequests[aActorId]; + if (!networkInfo) { return; } - let networkInfo = this._networkRequests[entry.connection]; - networkInfo.httpActivity = aMessage; + networkInfo.updates.push(aType); + + switch (aType) { + case "requestHeaders": + networkInfo.request.headersSize = aPacket.headersSize; + break; + case "requestPostData": + networkInfo.discardRequestBody = aPacket.discardRequestBody; + networkInfo.request.bodySize = aPacket.dataSize; + break; + case "responseStart": + networkInfo.response.httpVersion = aPacket.response.httpVersion; + networkInfo.response.status = aPacket.response.status; + networkInfo.response.statusText = aPacket.response.statusText; + networkInfo.response.headersSize = aPacket.response.headersSize; + networkInfo.discardResponseBody = aPacket.response.discardResponseBody; + break; + case "responseContent": + networkInfo.response.content = { + mimeType: aPacket.mimeType, + }; + networkInfo.response.bodySize = aPacket.contentSize; + networkInfo.discardResponseBody = aPacket.discardResponseBody; + break; + case "eventTimings": + networkInfo.totalTime = aPacket.totalTime; + break; + } if (networkInfo.node) { - this._updateNetMessage(entry.connection); + this._updateNetMessage(aActorId); } // For unit tests we pass the HTTP activity object to the test callback, // once requests complete. if (this.owner.lastFinishedRequestCallback && - aMessage.meta.stages.indexOf("REQUEST_STOP") > -1 && - aMessage.meta.stages.indexOf("TRANSACTION_CLOSE") > -1) { - this.owner.lastFinishedRequestCallback(aMessage); + networkInfo.updates.indexOf("responseContent") > -1 && + networkInfo.updates.indexOf("eventTimings") > -1) { + this.owner.lastFinishedRequestCallback(networkInfo, this); } }, /** * Update an output message to reflect the latest state of a network request, - * given a network connection ID. + * given a network event actor ID. * * @private - * @param string aConnectionId - * The connection ID to update. + * @param string aActorId + * The network event actor ID for which you want to update the message. */ - _updateNetMessage: function WCF__updateNetMessage(aConnectionId) + _updateNetMessage: function WCF__updateNetMessage(aActorId) { - let networkInfo = this._networkRequests[aConnectionId]; + let networkInfo = this._networkRequests[aActorId]; if (!networkInfo || !networkInfo.node) { return; } let messageNode = networkInfo.node; - let httpActivity = networkInfo.httpActivity; - let stages = httpActivity.meta.stages; - let hasTransactionClose = stages.indexOf("TRANSACTION_CLOSE") > -1; - let hasResponseHeader = stages.indexOf("RESPONSE_HEADER") > -1; - let entry = httpActivity.log.entries[0]; - let request = entry.request; - let response = entry.response; + let updates = networkInfo.updates; + let hasEventTimings = updates.indexOf("eventTimings") > -1; + let hasResponseStart = updates.indexOf("responseStart") > -1; + let request = networkInfo.request; + let response = networkInfo.response; - if (hasTransactionClose || hasResponseHeader) { + if (hasEventTimings || hasResponseStart) { let status = []; if (response.httpVersion && response.status) { status = [response.httpVersion, response.status, response.statusText]; } - if (hasTransactionClose) { - status.push(l10n.getFormatStr("NetworkPanel.durationMS", [entry.time])); + if (hasEventTimings) { + status.push(l10n.getFormatStr("NetworkPanel.durationMS", + [networkInfo.totalTime])); } let statusText = "[" + status.join(" ") + "]"; @@ -1543,7 +1542,7 @@ WebConsoleFrame.prototype = { messageNode.clipboardText = [request.method, request.url, statusText] .join(" "); - if (hasResponseHeader && response.status >= MIN_HTTP_ERROR_CODE && + if (hasResponseStart && response.status >= MIN_HTTP_ERROR_CODE && response.status <= MAX_HTTP_ERROR_CODE) { this.setMessageType(messageNode, CATEGORY_NETWORK, SEVERITY_ERROR); } @@ -1567,27 +1566,138 @@ WebConsoleFrame.prototype = { */ openNetworkPanel: function WCF_openNetworkPanel(aNode, aHttpActivity) { + let actor = aHttpActivity.actor; + + if (actor) { + this.webConsoleClient.getRequestHeaders(actor, function(aResponse) { + if (aResponse.error) { + Cu.reportError("WCF_openNetworkPanel getRequestHeaders:" + + aResponse.error); + return; + } + + aHttpActivity.request.headers = aResponse.headers; + + this.webConsoleClient.getRequestCookies(actor, onRequestCookies); + }.bind(this)); + } + + let onRequestCookies = function(aResponse) { + if (aResponse.error) { + Cu.reportError("WCF_openNetworkPanel getRequestCookies:" + + aResponse.error); + return; + } + + aHttpActivity.request.cookies = aResponse.cookies; + + this.webConsoleClient.getResponseHeaders(actor, onResponseHeaders); + }.bind(this); + + let onResponseHeaders = function(aResponse) { + if (aResponse.error) { + Cu.reportError("WCF_openNetworkPanel getResponseHeaders:" + + aResponse.error); + return; + } + + aHttpActivity.response.headers = aResponse.headers; + + this.webConsoleClient.getResponseCookies(actor, onResponseCookies); + }.bind(this); + + let onResponseCookies = function(aResponse) { + if (aResponse.error) { + Cu.reportError("WCF_openNetworkPanel getResponseCookies:" + + aResponse.error); + return; + } + + aHttpActivity.response.cookies = aResponse.cookies; + + this.webConsoleClient.getRequestPostData(actor, onRequestPostData); + }.bind(this); + + let onRequestPostData = function(aResponse) { + if (aResponse.error) { + Cu.reportError("WCF_openNetworkPanel getRequestPostData:" + + aResponse.error); + return; + } + + aHttpActivity.request.postData = aResponse.postData; + aHttpActivity.discardRequestBody = aResponse.postDataDiscarded; + + this.webConsoleClient.getResponseContent(actor, onResponseContent); + }.bind(this); + + let onResponseContent = function(aResponse) { + if (aResponse.error) { + Cu.reportError("WCF_openNetworkPanel getResponseContent:" + + aResponse.error); + return; + } + + aHttpActivity.response.content = aResponse.content; + aHttpActivity.discardResponseBody = aResponse.contentDiscarded; + + this.webConsoleClient.getEventTimings(actor, onEventTimings); + }.bind(this); + + let onEventTimings = function(aResponse) { + if (aResponse.error) { + Cu.reportError("WCF_openNetworkPanel getEventTimings:" + + aResponse.error); + return; + } + + aHttpActivity.timings = aResponse.timings; + + openPanel(); + }.bind(this); + + let openPanel = function() { + aNode._netPanel = netPanel; + + let panel = netPanel.panel; + panel.openPopup(aNode, "after_pointer", 0, 0, false, false); + panel.sizeTo(450, 500); + panel.setAttribute("hudId", this.hudId); + + panel.addEventListener("popuphiding", function WCF_netPanel_onHide() { + panel.removeEventListener("popuphiding", WCF_netPanel_onHide); + + aNode._panelOpen = false; + aNode._netPanel = null; + }); + + aNode._panelOpen = true; + }.bind(this); + let netPanel = new NetworkPanel(this.popupset, aHttpActivity); netPanel.linkNode = aNode; - aNode._netPanel = netPanel; - let panel = netPanel.panel; - panel.openPopup(aNode, "after_pointer", 0, 0, false, false); - panel.sizeTo(450, 500); - panel.setAttribute("hudId", aHttpActivity.hudId); - - panel.addEventListener("popuphiding", function WCF_netPanel_onHide() { - panel.removeEventListener("popuphiding", WCF_netPanel_onHide); - - aNode._panelOpen = false; - aNode._netPanel = null; - }); - - aNode._panelOpen = true; + if (!actor) { + openPanel(); + } return netPanel; }, + /** + * Handler for page location changes. + * + * @param string aURI + * New page location. + * @param string aTitle + * New page title. + */ + onLocationChange: function WCF_onLocationChange(aURI, aTitle) + { + this.contentLocation = aURI; + this.owner.onLocationChange(aURI, aTitle); + }, + /** * Output a message node. This filters a node appropriately, then sends it to * the output, regrouping and pruning output as necessary. @@ -1847,7 +1957,7 @@ WebConsoleFrame.prototype = { if (category == CATEGORY_NETWORK) { let connectionId = null; - if (methodOrNode == this.logNetActivity) { + if (methodOrNode == this.logNetEvent) { connectionId = args[0]; } else if (typeof methodOrNode != "function") { @@ -1855,6 +1965,7 @@ WebConsoleFrame.prototype = { } if (connectionId && connectionId in this._networkRequests) { delete this._networkRequests[connectionId]; + this._releaseObject(connectionId); } } else if (category == CATEGORY_WEBDEV && @@ -1950,8 +2061,10 @@ WebConsoleFrame.prototype = { } delete this._cssNodes[desc + location]; } - else if (aNode.classList.contains("webconsole-msg-network")) { + else if (aNode._connectionId && + aNode.classList.contains("webconsole-msg-network")) { delete this._networkRequests[aNode._connectionId]; + this._releaseObject(aNode._connectionId); } else if (aNode.classList.contains("webconsole-msg-inspector")) { this.pruneConsoleDirNode(aNode); @@ -2424,11 +2537,11 @@ WebConsoleFrame.prototype = { }, /** - * Release an object actor. + * Release an actor. * * @private * @param string aActor - * The object actor ID you want to release. + * The actor ID you want to release. */ _releaseObject: function WCF__releaseObject(aActor) { @@ -3701,6 +3814,10 @@ function WebConsoleConnectionProxy(aWebConsole) this._onPageError = this._onPageError.bind(this); this._onConsoleAPICall = this._onConsoleAPICall.bind(this); + this._onNetworkEvent = this._onNetworkEvent.bind(this); + this._onNetworkEventUpdate = this._onNetworkEventUpdate.bind(this); + this._onFileActivity = this._onFileActivity.bind(this); + this._onLocationChange = this._onLocationChange.bind(this); } WebConsoleConnectionProxy.prototype = { @@ -3766,13 +3883,19 @@ WebConsoleConnectionProxy.prototype = { client.addListener("pageError", this._onPageError); client.addListener("consoleAPICall", this._onConsoleAPICall); + client.addListener("networkEvent", this._onNetworkEvent); + client.addListener("networkEventUpdate", this._onNetworkEventUpdate); + client.addListener("fileActivity", this._onFileActivity); + client.addListener("locationChange", this._onLocationChange); - let listeners = ["PageError", "ConsoleAPI"]; + let listeners = ["PageError", "ConsoleAPI", "NetworkActivity", + "FileActivity", "LocationChange"]; client.connect(function(aType, aTraits) { client.listTabs(function(aResponse) { let tab = aResponse.tabs[aResponse.selected]; this._consoleActor = tab.consoleActor; + this.owner.onLocationChange(tab.url, tab.title); client.attachConsole(tab.consoleActor, listeners, this._onAttachConsole.bind(this, aCallback)); }.bind(this)); @@ -3870,6 +3993,80 @@ WebConsoleConnectionProxy.prototype = { } }, + /** + * The "networkEvent" message type handler. We redirect any message to + * the UI for displaying. + * + * @private + * @param string aType + * Message type. + * @param object aPacket + * The message received from the server. + */ + _onNetworkEvent: function WCCP__onNetworkEvent(aType, aPacket) + { + if (this.owner && aPacket.from == this._consoleActor) { + this.owner.handleNetworkEvent(aPacket.eventActor); + } + }, + + /** + * The "networkEventUpdate" message type handler. We redirect any message to + * the UI for displaying. + * + * @private + * @param string aType + * Message type. + * @param object aPacket + * The message received from the server. + */ + _onNetworkEventUpdate: function WCCP__onNetworkEvenUpdatet(aType, aPacket) + { + if (this.owner) { + this.owner.handleNetworkEventUpdate(aPacket.from, aPacket.updateType, + aPacket); + } + }, + + /** + * The "fileActivity" message type handler. We redirect any message to + * the UI for displaying. + * + * @private + * @param string aType + * Message type. + * @param object aPacket + * The message received from the server. + */ + _onFileActivity: function WCCP__onFileActivity(aType, aPacket) + { + if (this.owner && aPacket.from == this._consoleActor) { + this.owner.handleFileActivity(aPacket.uri); + } + }, + + /** + * The "locationChange" message type handler. We redirect any message to + * the UI for displaying. + * + * @private + * @param string aType + * Message type. + * @param object aPacket + * The message received from the server. + */ + _onLocationChange: function WCCP__onLocationChange(aType, aPacket) + { + if (!this.owner || aPacket.from != this._consoleActor) { + return; + } + + this.owner.onLocationChange(aPacket.uri, aPacket.title); + if (aPacket.state == "stop" && !aPacket.nativeConsoleAPI) { + this.owner.logWarningAboutReplacedAPI(); + } + }, + /** * Release an object actor. * @@ -3898,6 +4095,10 @@ WebConsoleConnectionProxy.prototype = { this.client.removeListener("pageError", this._onPageError); this.client.removeListener("consoleAPICall", this._onConsoleAPICall); + this.client.removeListener("networkEvent", this._onNetworkEvent); + this.client.removeListener("networkEventUpdate", this._onNetworkEventUpdate); + this.client.removeListener("fileActivity", this._onFileActivity); + this.client.removeListener("locationChange", this._onLocationChange); this.client.close(aOnDisconnect); this.client = null; diff --git a/toolkit/devtools/debugger/dbg-client.jsm b/toolkit/devtools/debugger/dbg-client.jsm index 48df9c180cf1..513407f92648 100644 --- a/toolkit/devtools/debugger/dbg-client.jsm +++ b/toolkit/devtools/debugger/dbg-client.jsm @@ -168,6 +168,10 @@ const ThreadStateTypes = { */ const UnsolicitedNotifications = { "consoleAPICall": "consoleAPICall", + "fileActivity": "fileActivity", + "locationChange": "locationChange", + "networkEvent": "networkEvent", + "networkEventUpdate": "networkEventUpdate", "newScript": "newScript", "tabDetached": "tabDetached", "tabNavigated": "tabNavigated", diff --git a/browser/devtools/webconsole/NetworkHelper.jsm b/toolkit/devtools/webconsole/NetworkHelper.jsm similarity index 95% rename from browser/devtools/webconsole/NetworkHelper.jsm rename to toolkit/devtools/webconsole/NetworkHelper.jsm index 4db6b03b5717..a2dd3bb077c2 100644 --- a/browser/devtools/webconsole/NetworkHelper.jsm +++ b/toolkit/devtools/webconsole/NetworkHelper.jsm @@ -99,8 +99,6 @@ var NetworkHelper = return conv.ConvertToUnicode(aText); } catch (ex) { - Cu.reportError("NH_convertToUnicode(aText, '" + - aCharset + "') exception: " + ex); return aText; } }, @@ -177,8 +175,24 @@ var NetworkHelper = readPostTextFromPage: function NH_readPostTextFromPage(aDocShell, aCharset) { let webNav = aDocShell.QueryInterface(Ci.nsIWebNavigation); - if (webNav instanceof Ci.nsIWebPageDescriptor) { - let descriptor = webNav.currentDescriptor; + return this.readPostTextFromPageViaWebNav(webNav, aCharset); + }, + + /** + * Reads the posted text from the page's cache, given an nsIWebNavigation + * object. + * + * @param nsIWebNavigation aWebNav + * @param string aCharset + * @returns string or null + * Returns the posted string if it was possible to read from + * aWebNav, otherwise null. + */ + readPostTextFromPageViaWebNav: + function NH_readPostTextFromPageViaWebNav(aWebNav, aCharset) + { + if (aWebNav instanceof Ci.nsIWebPageDescriptor) { + let descriptor = aWebNav.currentDescriptor; if (descriptor instanceof Ci.nsISHEntry && descriptor.postData && descriptor instanceof Ci.nsISeekableStream) { diff --git a/toolkit/devtools/webconsole/WebConsoleClient.jsm b/toolkit/devtools/webconsole/WebConsoleClient.jsm index 45e7301118fd..da27e34f1e07 100644 --- a/toolkit/devtools/webconsole/WebConsoleClient.jsm +++ b/toolkit/devtools/webconsole/WebConsoleClient.jsm @@ -117,6 +117,143 @@ WebConsoleClient.prototype = { this._client.request(packet); }, + /** + * Set Web Console-related preferences on the server. + * + * @param object aPreferences + * An object with the preferences you want to change. + * @param function [aOnResponse] + * Optional function to invoke when the response is received. + */ + setPreferences: function WCC_setPreferences(aPreferences, aOnResponse) + { + let packet = { + to: this._actor, + type: "setPreferences", + preferences: aPreferences, + }; + this._client.request(packet, aOnResponse); + }, + + /** + * Retrieve the request headers from the given NetworkEventActor. + * + * @param string aActor + * The NetworkEventActor ID. + * @param function aOnResponse + * The function invoked when the response is received. + */ + getRequestHeaders: function WCC_getRequestHeaders(aActor, aOnResponse) + { + let packet = { + to: aActor, + type: "getRequestHeaders", + }; + this._client.request(packet, aOnResponse); + }, + + /** + * Retrieve the request cookies from the given NetworkEventActor. + * + * @param string aActor + * The NetworkEventActor ID. + * @param function aOnResponse + * The function invoked when the response is received. + */ + getRequestCookies: function WCC_getRequestCookies(aActor, aOnResponse) + { + let packet = { + to: aActor, + type: "getRequestCookies", + }; + this._client.request(packet, aOnResponse); + }, + + /** + * Retrieve the request post data from the given NetworkEventActor. + * + * @param string aActor + * The NetworkEventActor ID. + * @param function aOnResponse + * The function invoked when the response is received. + */ + getRequestPostData: function WCC_getRequestPostData(aActor, aOnResponse) + { + let packet = { + to: aActor, + type: "getRequestPostData", + }; + this._client.request(packet, aOnResponse); + }, + + /** + * Retrieve the response headers from the given NetworkEventActor. + * + * @param string aActor + * The NetworkEventActor ID. + * @param function aOnResponse + * The function invoked when the response is received. + */ + getResponseHeaders: function WCC_getResponseHeaders(aActor, aOnResponse) + { + let packet = { + to: aActor, + type: "getResponseHeaders", + }; + this._client.request(packet, aOnResponse); + }, + + /** + * Retrieve the response cookies from the given NetworkEventActor. + * + * @param string aActor + * The NetworkEventActor ID. + * @param function aOnResponse + * The function invoked when the response is received. + */ + getResponseCookies: function WCC_getResponseCookies(aActor, aOnResponse) + { + let packet = { + to: aActor, + type: "getResponseCookies", + }; + this._client.request(packet, aOnResponse); + }, + + /** + * Retrieve the response content from the given NetworkEventActor. + * + * @param string aActor + * The NetworkEventActor ID. + * @param function aOnResponse + * The function invoked when the response is received. + */ + getResponseContent: function WCC_getResponseContent(aActor, aOnResponse) + { + let packet = { + to: aActor, + type: "getResponseContent", + }; + this._client.request(packet, aOnResponse); + }, + + /** + * Retrieve the timing information for the given NetworkEventActor. + * + * @param string aActor + * The NetworkEventActor ID. + * @param function aOnResponse + * The function invoked when the response is received. + */ + getEventTimings: function WCC_getEventTimings(aActor, aOnResponse) + { + let packet = { + to: aActor, + type: "getEventTimings", + }; + this._client.request(packet, aOnResponse); + }, + /** * Start the given Web Console listeners. * diff --git a/toolkit/devtools/webconsole/WebConsoleUtils.jsm b/toolkit/devtools/webconsole/WebConsoleUtils.jsm index 992f0aa9e412..c8a64b42efae 100644 --- a/toolkit/devtools/webconsole/WebConsoleUtils.jsm +++ b/toolkit/devtools/webconsole/WebConsoleUtils.jsm @@ -18,8 +18,20 @@ XPCOMUtils.defineLazyModuleGetter(this, "Services", XPCOMUtils.defineLazyModuleGetter(this, "ConsoleAPIStorage", "resource://gre/modules/ConsoleAPIStorage.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", + "resource://gre/modules/NetUtil.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "NetworkHelper", + "resource://gre/modules/devtools/NetworkHelper.jsm"); + +XPCOMUtils.defineLazyServiceGetter(this, "gActivityDistributor", + "@mozilla.org/network/http-activity-distributor;1", + "nsIHttpActivityDistributor"); + var EXPORTED_SYMBOLS = ["WebConsoleUtils", "JSPropertyProvider", "JSTermHelpers", - "PageErrorListener", "ConsoleAPIListener"]; + "PageErrorListener", "ConsoleAPIListener", + "NetworkResponseListener", "NetworkMonitor", + "ConsoleProgressListener"]; // Match the function name from the result of toString() or toSource(). // @@ -1854,3 +1866,1124 @@ function JSTermHelpers(aOwner) return String(aString); }; } + + +(function(_global, WCU) { +/////////////////////////////////////////////////////////////////////////////// +// Network logging +/////////////////////////////////////////////////////////////////////////////// + +// The maximum uint32 value. +const PR_UINT32_MAX = 4294967295; + +// HTTP status codes. +const HTTP_MOVED_PERMANENTLY = 301; +const HTTP_FOUND = 302; +const HTTP_SEE_OTHER = 303; +const HTTP_TEMPORARY_REDIRECT = 307; + +// The maximum number of bytes a NetworkResponseListener can hold. +const RESPONSE_BODY_LIMIT = 1048576; // 1 MB + +/** + * The network response listener implements the nsIStreamListener and + * nsIRequestObserver interfaces. This is used within the NetworkMonitor feature + * to get the response body of the request. + * + * The code is mostly based on code listings from: + * + * http://www.softwareishard.com/blog/firebug/ + * nsitraceablechannel-intercept-http-traffic/ + * + * @constructor + * @param object aOwner + * The response listener owner. This object needs to hold the + * |openResponses| object. + * @param object aHttpActivity + * HttpActivity object associated with this request. See NetworkMonitor + * for more information. + */ +function NetworkResponseListener(aOwner, aHttpActivity) +{ + this.owner = aOwner; + this.receivedData = ""; + this.httpActivity = aHttpActivity; + this.bodySize = 0; +} + +NetworkResponseListener.prototype = { + QueryInterface: + XPCOMUtils.generateQI([Ci.nsIStreamListener, Ci.nsIInputStreamCallback, + Ci.nsIRequestObserver, Ci.nsISupports]), + + /** + * This NetworkResponseListener tracks the NetworkMonitor.openResponses object + * to find the associated uncached headers. + * @private + */ + _foundOpenResponse: false, + + /** + * The response listener owner. + */ + owner: null, + + /** + * The response will be written into the outputStream of this nsIPipe. + * Both ends of the pipe must be blocking. + */ + sink: null, + + /** + * The HttpActivity object associated with this response. + */ + httpActivity: null, + + /** + * Stores the received data as a string. + */ + receivedData: null, + + /** + * The network response body size. + */ + bodySize: null, + + /** + * The nsIRequest we are started for. + */ + request: null, + + /** + * Set the async listener for the given nsIAsyncInputStream. This allows us to + * wait asynchronously for any data coming from the stream. + * + * @param nsIAsyncInputStream aStream + * The input stream from where we are waiting for data to come in. + * @param nsIInputStreamCallback aListener + * The input stream callback you want. This is an object that must have + * the onInputStreamReady() method. If the argument is null, then the + * current callback is removed. + * @return void + */ + setAsyncListener: function NRL_setAsyncListener(aStream, aListener) + { + // Asynchronously wait for the stream to be readable or closed. + aStream.asyncWait(aListener, 0, 0, Services.tm.mainThread); + }, + + /** + * Stores the received data, if request/response body logging is enabled. It + * also does limit the number of stored bytes, based on the + * RESPONSE_BODY_LIMIT constant. + * + * Learn more about nsIStreamListener at: + * https://developer.mozilla.org/en/XPCOM_Interface_Reference/nsIStreamListener + * + * @param nsIRequest aRequest + * @param nsISupports aContext + * @param nsIInputStream aInputStream + * @param unsigned long aOffset + * @param unsigned long aCount + */ + onDataAvailable: + function NRL_onDataAvailable(aRequest, aContext, aInputStream, aOffset, aCount) + { + this._findOpenResponse(); + let data = NetUtil.readInputStreamToString(aInputStream, aCount); + + this.bodySize += aCount; + + if (!this.httpActivity.discardResponseBody && + this.receivedData.length < RESPONSE_BODY_LIMIT) { + this.receivedData += NetworkHelper. + convertToUnicode(data, aRequest.contentCharset); + } + }, + + /** + * See documentation at + * https://developer.mozilla.org/En/NsIRequestObserver + * + * @param nsIRequest aRequest + * @param nsISupports aContext + */ + onStartRequest: function NRL_onStartRequest(aRequest) + { + this.request = aRequest; + this._findOpenResponse(); + // Asynchronously wait for the data coming from the request. + this.setAsyncListener(this.sink.inputStream, this); + }, + + /** + * Handle the onStopRequest by closing the sink output stream. + * + * For more documentation about nsIRequestObserver go to: + * https://developer.mozilla.org/En/NsIRequestObserver + */ + onStopRequest: function NRL_onStopRequest() + { + this._findOpenResponse(); + this.sink.outputStream.close(); + }, + + /** + * Find the open response object associated to the current request. The + * NetworkMonitor._httpResponseExaminer() method saves the response headers in + * NetworkMonitor.openResponses. This method takes the data from the open + * response object and puts it into the HTTP activity object, then sends it to + * the remote Web Console instance. + * + * @private + */ + _findOpenResponse: function NRL__findOpenResponse() + { + if (!this.owner || this._foundOpenResponse) { + return; + } + + let openResponse = null; + + for each (let item in this.owner.openResponses) { + if (item.channel === this.httpActivity.channel) { + openResponse = item; + break; + } + } + + if (!openResponse) { + return; + } + this._foundOpenResponse = true; + + delete this.owner.openResponses[openResponse.id]; + + this.httpActivity.owner.addResponseHeaders(openResponse.headers); + this.httpActivity.owner.addResponseCookies(openResponse.cookies); + }, + + /** + * Clean up the response listener once the response input stream is closed. + * This is called from onStopRequest() or from onInputStreamReady() when the + * stream is closed. + * @return void + */ + onStreamClose: function NRL_onStreamClose() + { + if (!this.httpActivity) { + return; + } + // Remove our listener from the request input stream. + this.setAsyncListener(this.sink.inputStream, null); + + this._findOpenResponse(); + + if (!this.httpActivity.discardResponseBody && this.receivedData.length) { + this._onComplete(this.receivedData); + } + else if (!this.httpActivity.discardResponseBody && + this.httpActivity.responseStatus == 304) { + // Response is cached, so we load it from cache. + let charset = this.request.contentCharset || this.httpActivity.charset; + NetworkHelper.loadFromCache(this.httpActivity.url, charset, + this._onComplete.bind(this)); + } + else { + this._onComplete(); + } + }, + + /** + * Handler for when the response completes. This function cleans up the + * response listener. + * + * @param string [aData] + * Optional, the received data coming from the response listener or + * from the cache. + */ + _onComplete: function NRL__onComplete(aData) + { + let response = { + mimeType: "", + text: aData || "", + }; + + // TODO: Bug 787981 - use LongStringActor for strings that are too long. + + try { + response.mimeType = this.request.contentType; + } + catch (ex) { } + + if (response.mimeType && this.request.contentCharset) { + response.mimeType += "; charset=" + this.request.contentCharset; + } + + this.receivedData = ""; + + this.httpActivity.owner. + addResponseContent(response, this.httpActivity.discardResponseBody); + + this.httpActivity.channel = null; + this.httpActivity.owner = null; + this.httpActivity = null; + this.sink = null; + this.inputStream = null; + this.request = null; + this.owner = null; + }, + + /** + * The nsIInputStreamCallback for when the request input stream is ready - + * either it has more data or it is closed. + * + * @param nsIAsyncInputStream aStream + * The sink input stream from which data is coming. + * @returns void + */ + onInputStreamReady: function NRL_onInputStreamReady(aStream) + { + if (!(aStream instanceof Ci.nsIAsyncInputStream) || !this.httpActivity) { + return; + } + + let available = -1; + try { + // This may throw if the stream is closed normally or due to an error. + available = aStream.available(); + } + catch (ex) { } + + if (available != -1) { + if (available != 0) { + // Note that passing 0 as the offset here is wrong, but the + // onDataAvailable() method does not use the offset, so it does not + // matter. + this.onDataAvailable(this.request, null, aStream, 0, available); + } + this.setAsyncListener(aStream, this); + } + else { + this.onStreamClose(); + } + }, +}; + +/** + * The network monitor uses the nsIHttpActivityDistributor to monitor network + * requests. The nsIObserverService is also used for monitoring + * http-on-examine-response notifications. All network request information is + * routed to the remote Web Console. + * + * @constructor + * @param nsIDOMWindow aWindow + * The window that we monitor network requests for. + * @param object aOwner + * The network monitor owner. This object needs to hold: + * - onNetworkEvent(aRequestInfo). This method is invoked once for every + * new network request and it is given one arguments: the initial network + * request information. onNetworkEvent() must return an object which + * holds several add*() methods which are used to add further network + * request/response information. + * - saveRequestAndResponseBodies property which tells if you want to log + * request and response bodies. + */ +function NetworkMonitor(aWindow, aOwner) +{ + this.window = aWindow; + this.owner = aOwner; + this.openRequests = {}; + this.openResponses = {}; + this._httpResponseExaminer = this._httpResponseExaminer.bind(this); +} + +NetworkMonitor.prototype = { + httpTransactionCodes: { + 0x5001: "REQUEST_HEADER", + 0x5002: "REQUEST_BODY_SENT", + 0x5003: "RESPONSE_START", + 0x5004: "RESPONSE_HEADER", + 0x5005: "RESPONSE_COMPLETE", + 0x5006: "TRANSACTION_CLOSE", + + 0x804b0003: "STATUS_RESOLVING", + 0x804b000b: "STATUS_RESOLVED", + 0x804b0007: "STATUS_CONNECTING_TO", + 0x804b0004: "STATUS_CONNECTED_TO", + 0x804b0005: "STATUS_SENDING_TO", + 0x804b000a: "STATUS_WAITING_FOR", + 0x804b0006: "STATUS_RECEIVING_FROM" + }, + + // Network response bodies are piped through a buffer of the given size (in + // bytes). + responsePipeSegmentSize: null, + + owner: null, + + /** + * Whether to save the bodies of network requests and responses. Disabled by + * default to save memory. + */ + get saveRequestAndResponseBodies() + this.owner && this.owner.saveRequestAndResponseBodies, + + /** + * Object that holds the HTTP activity objects for ongoing requests. + */ + openRequests: null, + + /** + * Object that holds response headers coming from this._httpResponseExaminer. + */ + openResponses: null, + + /** + * The network monitor initializer. + */ + init: function NM_init() + { + this.responsePipeSegmentSize = Services.prefs + .getIntPref("network.buffer.cache.size"); + + gActivityDistributor.addObserver(this); + + Services.obs.addObserver(this._httpResponseExaminer, + "http-on-examine-response", false); + }, + + /** + * Observe notifications for the http-on-examine-response topic, coming from + * the nsIObserverService. + * + * @private + * @param nsIHttpChannel aSubject + * @param string aTopic + * @returns void + */ + _httpResponseExaminer: function NM__httpResponseExaminer(aSubject, aTopic) + { + // The httpResponseExaminer is used to retrieve the uncached response + // headers. The data retrieved is stored in openResponses. The + // NetworkResponseListener is responsible with updating the httpActivity + // object with the data from the new object in openResponses. + + if (!this.owner || aTopic != "http-on-examine-response" || + !(aSubject instanceof Ci.nsIHttpChannel)) { + return; + } + + let channel = aSubject.QueryInterface(Ci.nsIHttpChannel); + // Try to get the source window of the request. + let win = NetworkHelper.getWindowForRequest(channel); + if (!win || win.top !== this.window) { + return; + } + + let response = { + id: gSequenceId(), + channel: channel, + headers: [], + cookies: [], + }; + + let setCookieHeader = null; + + channel.visitResponseHeaders({ + visitHeader: function NM__visitHeader(aName, aValue) { + let lowerName = aName.toLowerCase(); + if (lowerName == "set-cookie") { + setCookieHeader = aValue; + } + response.headers.push({ name: aName, value: aValue }); + } + }); + + if (!response.headers.length) { + return; // No need to continue. + } + + if (setCookieHeader) { + response.cookies = NetworkHelper.parseSetCookieHeader(setCookieHeader); + } + + // Determine the HTTP version. + let httpVersionMaj = {}; + let httpVersionMin = {}; + + channel.QueryInterface(Ci.nsIHttpChannelInternal); + channel.getResponseVersion(httpVersionMaj, httpVersionMin); + + response.status = channel.responseStatus; + response.statusText = channel.responseStatusText; + response.httpVersion = "HTTP/" + httpVersionMaj.value + "." + + httpVersionMin.value; + + this.openResponses[response.id] = response; + }, + + /** + * Begin observing HTTP traffic that originates inside the current tab. + * + * @see https://developer.mozilla.org/en/XPCOM_Interface_Reference/nsIHttpActivityObserver + * + * @param nsIHttpChannel aChannel + * @param number aActivityType + * @param number aActivitySubtype + * @param number aTimestamp + * @param number aExtraSizeData + * @param string aExtraStringData + */ + observeActivity: + function NM_observeActivity(aChannel, aActivityType, aActivitySubtype, + aTimestamp, aExtraSizeData, aExtraStringData) + { + if (!this.owner || + aActivityType != gActivityDistributor.ACTIVITY_TYPE_HTTP_TRANSACTION && + aActivityType != gActivityDistributor.ACTIVITY_TYPE_SOCKET_TRANSPORT) { + return; + } + + if (!(aChannel instanceof Ci.nsIHttpChannel)) { + return; + } + + aChannel = aChannel.QueryInterface(Ci.nsIHttpChannel); + + if (aActivitySubtype == + gActivityDistributor.ACTIVITY_SUBTYPE_REQUEST_HEADER) { + this._onRequestHeader(aChannel, aTimestamp, aExtraStringData); + return; + } + + // Iterate over all currently ongoing requests. If aChannel can't + // be found within them, then exit this function. + let httpActivity = null; + for each (let item in this.openRequests) { + if (item.channel === aChannel) { + httpActivity = item; + break; + } + } + + if (!httpActivity) { + return; + } + + let transCodes = this.httpTransactionCodes; + + // Store the time information for this activity subtype. + if (aActivitySubtype in transCodes) { + let stage = transCodes[aActivitySubtype]; + if (stage in httpActivity.timings) { + httpActivity.timings[stage].last = aTimestamp; + } + else { + httpActivity.timings[stage] = { + first: aTimestamp, + last: aTimestamp, + }; + } + } + + switch (aActivitySubtype) { + case gActivityDistributor.ACTIVITY_SUBTYPE_REQUEST_BODY_SENT: + this._onRequestBodySent(httpActivity); + break; + case gActivityDistributor.ACTIVITY_SUBTYPE_RESPONSE_HEADER: + this._onResponseHeader(httpActivity, aExtraStringData); + break; + case gActivityDistributor.ACTIVITY_SUBTYPE_TRANSACTION_CLOSE: + this._onTransactionClose(httpActivity); + break; + default: + break; + } + }, + + /** + * Handler for ACTIVITY_SUBTYPE_REQUEST_HEADER. When a request starts the + * headers are sent to the server. This method creates the |httpActivity| + * object where we store the request and response information that is + * collected through its lifetime. + * + * @private + * @param nsIHttpChannel aChannel + * @param number aTimestamp + * @param string aExtraStringData + * @return void + */ + _onRequestHeader: + function NM__onRequestHeader(aChannel, aTimestamp, aExtraStringData) + { + // Try to get the source window of the request. + let win = NetworkHelper.getWindowForRequest(aChannel); + if (!win || win.top !== this.window) { + return; + } + + let httpActivity = this.createActivityObject(aChannel); + httpActivity.charset = win.document.characterSet; // see NM__onRequestBodySent() + + httpActivity.timings.REQUEST_HEADER = { + first: aTimestamp, + last: aTimestamp + }; + + let httpVersionMaj = {}; + let httpVersionMin = {}; + let event = {}; + event.startedDateTime = new Date(Math.round(aTimestamp / 1000)).toISOString(); + event.headersSize = aExtraStringData.length; + event.method = aChannel.requestMethod; + event.url = aChannel.URI.spec; + + // Determine the HTTP version. + aChannel.QueryInterface(Ci.nsIHttpChannelInternal); + aChannel.getRequestVersion(httpVersionMaj, httpVersionMin); + + event.httpVersion = "HTTP/" + httpVersionMaj.value + "." + + httpVersionMin.value; + + event.discardRequestBody = !this.saveRequestAndResponseBodies; + event.discardResponseBody = !this.saveRequestAndResponseBodies; + + let headers = []; + let cookies = []; + let cookieHeader = null; + + // Copy the request header data. + aChannel.visitRequestHeaders({ + visitHeader: function NM__visitHeader(aName, aValue) + { + if (aName == "Cookie") { + cookieHeader = aValue; + } + headers.push({ name: aName, value: aValue }); + } + }); + + if (cookieHeader) { + cookies = NetworkHelper.parseCookieHeader(cookieHeader); + } + + httpActivity.owner = this.owner.onNetworkEvent(event); + + this._setupResponseListener(httpActivity); + + this.openRequests[httpActivity.id] = httpActivity; + + httpActivity.owner.addRequestHeaders(headers); + httpActivity.owner.addRequestCookies(cookies); + }, + + /** + * Create the empty HTTP activity object. This object is used for storing all + * the request and response information. + * + * This is a HAR-like object. Conformance to the spec is not guaranteed at + * this point. + * + * TODO: Bug 708717 - Add support for network log export to HAR + * + * @see http://www.softwareishard.com/blog/har-12-spec + * @param nsIHttpChannel aChannel + * The HTTP channel for which the HTTP activity object is created. + * @return object + * The new HTTP activity object. + */ + createActivityObject: function NM_createActivityObject(aChannel) + { + return { + id: gSequenceId(), + channel: aChannel, + charset: null, // see NM__onRequestHeader() + url: aChannel.URI.spec, + discardRequestBody: !this.saveRequestAndResponseBodies, + discardResponseBody: !this.saveRequestAndResponseBodies, + timings: {}, // internal timing information, see NM_observeActivity() + responseStatus: null, // see NM__onResponseHeader() + owner: null, // the activity owner which is notified when changes happen + }; + }, + + /** + * Setup the network response listener for the given HTTP activity. The + * NetworkResponseListener is responsible for storing the response body. + * + * @private + * @param object aHttpActivity + * The HTTP activity object we are tracking. + */ + _setupResponseListener: function NM__setupResponseListener(aHttpActivity) + { + let channel = aHttpActivity.channel; + channel.QueryInterface(Ci.nsITraceableChannel); + + // The response will be written into the outputStream of this pipe. + // This allows us to buffer the data we are receiving and read it + // asynchronously. + // Both ends of the pipe must be blocking. + let sink = Cc["@mozilla.org/pipe;1"].createInstance(Ci.nsIPipe); + + // The streams need to be blocking because this is required by the + // stream tee. + sink.init(false, false, this.responsePipeSegmentSize, PR_UINT32_MAX, null); + + // Add listener for the response body. + let newListener = new NetworkResponseListener(this, aHttpActivity); + + // Remember the input stream, so it isn't released by GC. + newListener.inputStream = sink.inputStream; + newListener.sink = sink; + + let tee = Cc["@mozilla.org/network/stream-listener-tee;1"]. + createInstance(Ci.nsIStreamListenerTee); + + let originalListener = channel.setNewListener(tee); + + tee.init(originalListener, sink.outputStream, newListener); + }, + + /** + * Handler for ACTIVITY_SUBTYPE_REQUEST_BODY_SENT. The request body is logged + * here. + * + * @private + * @param object aHttpActivity + * The HTTP activity object we are working with. + */ + _onRequestBodySent: function NM__onRequestBodySent(aHttpActivity) + { + if (aHttpActivity.discardRequestBody) { + return; + } + + let sentBody = NetworkHelper. + readPostTextFromRequest(aHttpActivity.channel, + aHttpActivity.charset); + + if (!sentBody && aHttpActivity.url == this.window.location.href) { + // If the request URL is the same as the current page URL, then + // we can try to get the posted text from the page directly. + // This check is necessary as otherwise the + // NetworkHelper.readPostTextFromPageViaWebNav() + // function is called for image requests as well but these + // are not web pages and as such don't store the posted text + // in the cache of the webpage. + let webNav = this.window.QueryInterface(Ci.nsIInterfaceRequestor). + getInterface(Ci.nsIWebNavigation); + sentBody = NetworkHelper. + readPostTextFromPageViaWebNav(webNav, aHttpActivity.charset); + } + + if (sentBody) { + aHttpActivity.owner.addRequestPostData({ text: sentBody }); + } + }, + + /** + * Handler for ACTIVITY_SUBTYPE_RESPONSE_HEADER. This method stores + * information about the response headers. + * + * @private + * @param object aHttpActivity + * The HTTP activity object we are working with. + * @param string aExtraStringData + * The uncached response headers. + */ + _onResponseHeader: + function NM__onResponseHeader(aHttpActivity, aExtraStringData) + { + // aExtraStringData contains the uncached response headers. The first line + // contains the response status (e.g. HTTP/1.1 200 OK). + // + // Note: The response header is not saved here. Calling the + // channel.visitResponseHeaders() methood at this point sometimes causes an + // NS_ERROR_NOT_AVAILABLE exception. + // + // We could parse aExtraStringData to get the headers and their values, but + // that is not trivial to do in an accurate manner. Hence, we save the + // response headers in this._httpResponseExaminer(). + + let headers = aExtraStringData.split(/\r\n|\n|\r/); + let statusLine = headers.shift(); + let statusLineArray = statusLine.split(" "); + + let response = {}; + response.httpVersion = statusLineArray.shift(); + response.status = statusLineArray.shift(); + response.statusText = statusLineArray.join(" "); + response.headersSize = aExtraStringData.length; + + aHttpActivity.responseStatus = response.status; + + // Discard the response body for known response statuses. + switch (parseInt(response.status)) { + case HTTP_MOVED_PERMANENTLY: + case HTTP_FOUND: + case HTTP_SEE_OTHER: + case HTTP_TEMPORARY_REDIRECT: + aHttpActivity.discardResponseBody = true; + break; + } + + response.discardResponseBody = aHttpActivity.discardResponseBody; + + aHttpActivity.owner.addResponseStart(response); + }, + + /** + * Handler for ACTIVITY_SUBTYPE_TRANSACTION_CLOSE. This method updates the HAR + * timing information on the HTTP activity object and clears the request + * from the list of known open requests. + * + * @private + * @param object aHttpActivity + * The HTTP activity object we work with. + */ + _onTransactionClose: function NM__onTransactionClose(aHttpActivity) + { + let result = this._setupHarTimings(aHttpActivity); + aHttpActivity.owner.addEventTimings(result.total, result.timings); + delete this.openRequests[aHttpActivity.id]; + }, + + /** + * Update the HTTP activity object to include timing information as in the HAR + * spec. The HTTP activity object holds the raw timing information in + * |timings| - these are timings stored for each activity notification. The + * HAR timing information is constructed based on these lower level data. + * + * @param object aHttpActivity + * The HTTP activity object we are working with. + * @return object + * This object holds two properties: + * - total - the total time for all of the request and response. + * - timings - the HAR timings object. + */ + _setupHarTimings: function NM__setupHarTimings(aHttpActivity) + { + let timings = aHttpActivity.timings; + let harTimings = {}; + + // Not clear how we can determine "blocked" time. + harTimings.blocked = -1; + + // DNS timing information is available only in when the DNS record is not + // cached. + harTimings.dns = timings.STATUS_RESOLVING && timings.STATUS_RESOLVED ? + timings.STATUS_RESOLVED.last - + timings.STATUS_RESOLVING.first : -1; + + if (timings.STATUS_CONNECTING_TO && timings.STATUS_CONNECTED_TO) { + harTimings.connect = timings.STATUS_CONNECTED_TO.last - + timings.STATUS_CONNECTING_TO.first; + } + else if (timings.STATUS_SENDING_TO) { + harTimings.connect = timings.STATUS_SENDING_TO.first - + timings.REQUEST_HEADER.first; + } + else { + harTimings.connect = -1; + } + + if ((timings.STATUS_WAITING_FOR || timings.STATUS_RECEIVING_FROM) && + (timings.STATUS_CONNECTED_TO || timings.STATUS_SENDING_TO)) { + harTimings.send = (timings.STATUS_WAITING_FOR || + timings.STATUS_RECEIVING_FROM).first - + (timings.STATUS_CONNECTED_TO || + timings.STATUS_SENDING_TO).last; + } + else { + harTimings.send = -1; + } + + if (timings.RESPONSE_START) { + harTimings.wait = timings.RESPONSE_START.first - + (timings.REQUEST_BODY_SENT || + timings.STATUS_SENDING_TO).last; + } + else { + harTimings.wait = -1; + } + + if (timings.RESPONSE_START && timings.RESPONSE_COMPLETE) { + harTimings.receive = timings.RESPONSE_COMPLETE.last - + timings.RESPONSE_START.first; + } + else { + harTimings.receive = -1; + } + + let totalTime = 0; + for (let timing in harTimings) { + let time = Math.max(Math.round(harTimings[timing] / 1000), -1); + harTimings[timing] = time; + if (time > -1) { + totalTime += time; + } + } + + return { + total: totalTime, + timings: harTimings, + }; + }, + + /** + * Suspend Web Console activity. This is called when all Web Consoles are + * closed. + */ + destroy: function NM_destroy() + { + Services.obs.removeObserver(this._httpResponseExaminer, + "http-on-examine-response"); + + gActivityDistributor.removeObserver(this); + + this.openRequests = {}; + this.openResponses = {}; + this.owner = null; + this.window = null; + }, +}; + +_global.NetworkMonitor = NetworkMonitor; +_global.NetworkResponseListener = NetworkResponseListener; +})(this, WebConsoleUtils); + + +/** + * A WebProgressListener that listens for location changes. + * + * This progress listener is used to track file loads and other kinds of + * location changes. + * + * @constructor + * @param object aBrowser + * The xul:browser for which we need to track location changes. + * @param object aOwner + * The listener owner which needs to implement two methods: + * - onFileActivity(aFileURI) + * - onLocationChange(aState, aTabURI, aPageTitle) + */ +function ConsoleProgressListener(aBrowser, aOwner) +{ + this.browser = aBrowser; + this.owner = aOwner; +} + +ConsoleProgressListener.prototype = { + /** + * Constant used for startMonitor()/stopMonitor() that tells you want to + * monitor file loads. + */ + MONITOR_FILE_ACTIVITY: 1, + + /** + * Constant used for startMonitor()/stopMonitor() that tells you want to + * monitor page location changes. + */ + MONITOR_LOCATION_CHANGE: 2, + + /** + * Tells if you want to monitor file activity. + * @private + * @type boolean + */ + _fileActivity: false, + + /** + * Tells if you want to monitor location changes. + * @private + * @type boolean + */ + _locationChange: false, + + /** + * Tells if the console progress listener is initialized or not. + * @private + * @type boolean + */ + _initialized: false, + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener, + Ci.nsISupportsWeakReference]), + + /** + * Initialize the ConsoleProgressListener. + * @private + */ + _init: function CPL__init() + { + if (this._initialized) { + return; + } + + this._initialized = true; + this.browser.addProgressListener(this, Ci.nsIWebProgress.NOTIFY_STATE_ALL); + }, + + /** + * Start a monitor/tracker related to the current nsIWebProgressListener + * instance. + * + * @param number aMonitor + * Tells what you want to track. Available constants: + * - this.MONITOR_FILE_ACTIVITY + * Track file loads. + * - this.MONITOR_LOCATION_CHANGE + * Track location changes for the top window. + */ + startMonitor: function CPL_startMonitor(aMonitor) + { + switch (aMonitor) { + case this.MONITOR_FILE_ACTIVITY: + this._fileActivity = true; + break; + case this.MONITOR_LOCATION_CHANGE: + this._locationChange = true; + break; + default: + throw new Error("ConsoleProgressListener: unknown monitor type " + + aMonitor + "!"); + } + this._init(); + }, + + /** + * Stop a monitor. + * + * @param number aMonitor + * Tells what you want to stop tracking. See this.startMonitor() for + * the list of constants. + */ + stopMonitor: function CPL_stopMonitor(aMonitor) + { + switch (aMonitor) { + case this.MONITOR_FILE_ACTIVITY: + this._fileActivity = false; + break; + case this.MONITOR_LOCATION_CHANGE: + this._locationChange = false; + break; + default: + throw new Error("ConsoleProgressListener: unknown monitor type " + + aMonitor + "!"); + } + + if (!this._fileActivity && !this._locationChange) { + this.destroy(); + } + }, + + onStateChange: + function CPL_onStateChange(aProgress, aRequest, aState, aStatus) + { + if (!this.owner) { + return; + } + + if (this._fileActivity) { + this._checkFileActivity(aProgress, aRequest, aState, aStatus); + } + + if (this._locationChange) { + this._checkLocationChange(aProgress, aRequest, aState, aStatus); + } + }, + + /** + * Check if there is any file load, given the arguments of + * nsIWebProgressListener.onStateChange. If the state change tells that a file + * URI has been loaded, then the remote Web Console instance is notified. + * @private + */ + _checkFileActivity: + function CPL__checkFileActivity(aProgress, aRequest, aState, aStatus) + { + if (!(aState & Ci.nsIWebProgressListener.STATE_START)) { + return; + } + + let uri = null; + if (aRequest instanceof Ci.imgIRequest) { + let imgIRequest = aRequest.QueryInterface(Ci.imgIRequest); + uri = imgIRequest.URI; + } + else if (aRequest instanceof Ci.nsIChannel) { + let nsIChannel = aRequest.QueryInterface(Ci.nsIChannel); + uri = nsIChannel.URI; + } + + if (!uri || !uri.schemeIs("file") && !uri.schemeIs("ftp")) { + return; + } + + this.owner.onFileActivity(uri.spec); + }, + + /** + * Check if the current window.top location is changing, given the arguments + * of nsIWebProgressListener.onStateChange. If that is the case, the remote + * Web Console instance is notified. + * @private + */ + _checkLocationChange: + function CPL__checkLocationChange(aProgress, aRequest, aState, aStatus) + { + let isStart = aState & Ci.nsIWebProgressListener.STATE_START; + let isStop = aState & Ci.nsIWebProgressListener.STATE_STOP; + let isNetwork = aState & Ci.nsIWebProgressListener.STATE_IS_NETWORK; + let isWindow = aState & Ci.nsIWebProgressListener.STATE_IS_WINDOW; + + // Skip non-interesting states. + if (!isNetwork || !isWindow || + aProgress.DOMWindow != this.browser.contentWindow) { + return; + } + + if (isStart && aRequest instanceof Ci.nsIChannel) { + this.owner.onLocationChange("start", aRequest.URI.spec, ""); + } + else if (isStop) { + let window = this.browser.contentWindow; + this.owner.onLocationChange("stop", window.location.href, + window.document.title); + } + }, + + onLocationChange: function() {}, + onStatusChange: function() {}, + onProgressChange: function() {}, + onSecurityChange: function() {}, + + /** + * Destroy the ConsoleProgressListener. + */ + destroy: function CPL_destroy() + { + if (!this._initialized) { + return; + } + + this._initialized = false; + this._fileActivity = false; + this._locationChange = false; + + if (this.browser.removeProgressListener) { + this.browser.removeProgressListener(this); + } + + this.browser = null; + this.owner = null; + }, +}; + +function gSequenceId() +{ + return gSequenceId.n++; +} +gSequenceId.n = 0; diff --git a/toolkit/devtools/webconsole/dbg-webconsole-actors.js b/toolkit/devtools/webconsole/dbg-webconsole-actors.js index dec4f6754d4a..82237d8ac3be 100644 --- a/toolkit/devtools/webconsole/dbg-webconsole-actors.js +++ b/toolkit/devtools/webconsole/dbg-webconsole-actors.js @@ -24,12 +24,18 @@ XPCOMUtils.defineLazyModuleGetter(this, "PageErrorListener", XPCOMUtils.defineLazyModuleGetter(this, "ConsoleAPIListener", "resource://gre/modules/devtools/WebConsoleUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "ConsoleProgressListener", + "resource://gre/modules/devtools/WebConsoleUtils.jsm"); + XPCOMUtils.defineLazyModuleGetter(this, "JSTermHelpers", "resource://gre/modules/devtools/WebConsoleUtils.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "JSPropertyProvider", "resource://gre/modules/devtools/WebConsoleUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "NetworkMonitor", + "resource://gre/modules/devtools/WebConsoleUtils.jsm"); + XPCOMUtils.defineLazyModuleGetter(this, "ConsoleAPIStorage", "resource://gre/modules/ConsoleAPIStorage.jsm"); @@ -51,6 +57,11 @@ function WebConsoleActor(aConnection, aTabActor) this._objectActorsPool = new ActorPool(this.conn); this.conn.addActorPool(this._objectActorsPool); + + this._networkEventActorsPool = new ActorPool(this.conn); + this.conn.addActorPool(this._networkEventActorsPool); + + this._prefs = {}; } WebConsoleActor.prototype = @@ -67,10 +78,26 @@ WebConsoleActor.prototype = * @private * @type object * @see ActorPool + * @see WebConsoleObjectActor * @see this.objectGrip() */ _objectActorsPool: null, + /** + * Actor pool for all of the network event actors. + * @private + * @type object + * @see NetworkEventActor + */ + _networkEventActorsPool: null, + + /** + * Web Console-related preferences. + * @private + * @type object + */ + _prefs: null, + /** * Tells the current page location associated to the sandbox. When the page * location is changed, we recreate the sandbox. @@ -108,6 +135,23 @@ WebConsoleActor.prototype = */ consoleAPIListener: null, + /** + * The NetworkMonitor instance. + */ + networkMonitor: null, + + /** + * The ConsoleProgressListener instance. + */ + consoleProgressListener: null, + + /** + * Getter for the NetworkMonitor.saveRequestAndResponseBodies preference. + * @type boolean + */ + get saveRequestAndResponseBodies() + this._prefs["NetworkMonitor.saveRequestAndResponseBodies"], + actorPrefix: "console", grip: function WCA_grip() @@ -146,8 +190,18 @@ WebConsoleActor.prototype = this.consoleAPIListener.destroy(); this.consoleAPIListener = null; } + if (this.networkMonitor) { + this.networkMonitor.destroy(); + this.networkMonitor = null; + } + if (this.consoleProgressListener) { + this.consoleProgressListener.destroy(); + this.consoleProgressListener = null; + } this.conn.removeActorPool(this._objectActorsPool); + this.conn.removeActorPool(this._networkEventActorsPool); this._objectActorsPool = null; + this._networkEventActorsPool = null; this._sandboxLocation = this.sandbox = null; this.conn = this._browser = null; }, @@ -205,6 +259,21 @@ WebConsoleActor.prototype = this._objectActorsPool.removeActor(aActor.actorID); }, + /** + * Release a network event actor. + * + * @param object aActor + * The NetworkEventActor instance you want to release. + */ + releaseNetworkEvent: function WCA_releaseNetworkEvent(aActor) + { + this._networkEventActorsPool.removeActor(aActor.actorID); + }, + + ////////////////// + // Request handlers for known packet types. + ////////////////// + /** * Handler for the "startListeners" request. * @@ -236,6 +305,30 @@ WebConsoleActor.prototype = } startedListeners.push(listener); break; + case "NetworkActivity": + if (!this.networkMonitor) { + this.networkMonitor = + new NetworkMonitor(this.window, this); + this.networkMonitor.init(); + } + startedListeners.push(listener); + break; + case "FileActivity": + if (!this.consoleProgressListener) { + this.consoleProgressListener = + new ConsoleProgressListener(this._browser, this); + } + this.consoleProgressListener.startMonitor(this.consoleProgressListener. + MONITOR_FILE_ACTIVITY); + startedListeners.push(listener); + break; + case "LocationChange": + if (!this.consoleProgressListener) { + this.consoleProgressListener = + new ConsoleProgressListener(this._browser, this); + } + this.consoleProgressListener.startMonitor(this.consoleProgressListener. + MONITOR_LOCATION_CHANGE); } } return { @@ -259,7 +352,9 @@ WebConsoleActor.prototype = // If no specific listeners are requested to be detached, we stop all // listeners. - let toDetach = aRequest.listeners || ["PageError", "ConsoleAPI"]; + let toDetach = aRequest.listeners || + ["PageError", "ConsoleAPI", "NetworkActivity", + "FileActivity", "LocationChange"]; while (toDetach.length > 0) { let listener = toDetach.shift(); @@ -278,6 +373,27 @@ WebConsoleActor.prototype = } stoppedListeners.push(listener); break; + case "NetworkActivity": + if (this.networkMonitor) { + this.networkMonitor.destroy(); + this.networkMonitor = null; + } + stoppedListeners.push(listener); + break; + case "FileActivity": + if (this.consoleProgressListener) { + this.consoleProgressListener.stopMonitor(this.consoleProgressListener. + MONITOR_FILE_ACTIVITY); + } + stoppedListeners.push(listener); + break; + case "LocationChange": + if (this.consoleProgressListener) { + this.consoleProgressListener.stopMonitor(this.consoleProgressListener. + MONITOR_LOCATION_CHANGE); + } + stoppedListeners.push(listener); + break; } } @@ -409,6 +525,24 @@ WebConsoleActor.prototype = return {}; }, + /** + * The "setPreferences" request handler. + * + * @param object aRequest + * The request message - which preferences need to be updated. + */ + onSetPreferences: function WCA_onSetPreferences(aRequest) + { + for (let key in aRequest.preferences) { + this._prefs[key] = aRequest.preferences[key]; + } + return { updated: Object.keys(aRequest.preferences) }; + }, + + ////////////////// + // End of request handlers. + ////////////////// + /** * Create the JavaScript sandbox where user input is evaluated. * @private @@ -474,6 +608,10 @@ WebConsoleActor.prototype = return result; }, + ////////////////// + // Event handlers for various listeners. + ////////////////// + /** * Handler for page errors received from the PageErrorListener. This method * sends the nsIScriptError to the remote Web Console client. @@ -521,6 +659,7 @@ WebConsoleActor.prototype = * Handler for window.console API calls received from the ConsoleAPIListener. * This method sends the object to the remote Web Console client. * + * @see ConsoleAPIListener * @param object aMessage * The console API call we need to send to the remote client. */ @@ -534,6 +673,85 @@ WebConsoleActor.prototype = this.conn.send(packet); }, + /** + * Handler for network events. This method is invoked when a new network event + * is about to be recorded. + * + * @see NetworkEventActor + * @see NetworkMonitor from WebConsoleUtils.jsm + * + * @param object aEvent + * The initial network request event information. + * @return object + * A new NetworkEventActor is returned. This is used for tracking the + * network request and response. + */ + onNetworkEvent: function WCA_onNetworkEvent(aEvent) + { + let actor = new NetworkEventActor(aEvent, this); + this._networkEventActorsPool.addActor(actor); + + let packet = { + from: this.actorID, + type: "networkEvent", + eventActor: actor.grip(), + }; + + this.conn.send(packet); + + return actor; + }, + + /** + * Handler for file activity. This method sends the file request information + * to the remote Web Console client. + * + * @see ConsoleProgressListener + * @param string aFileURI + * The requested file URI. + */ + onFileActivity: function WCA_onFileActivity(aFileURI) + { + let packet = { + from: this.actorID, + type: "fileActivity", + uri: aFileURI, + }; + this.conn.send(packet); + }, + + /** + * Handler for location changes. This method sends the new browser location + * to the remote Web Console client. + * + * @see ConsoleProgressListener + * @param string aState + * Tells the location change state: + * - "start" means a load has begun. + * - "stop" means load completed. + * @param string aURI + * The new browser URI. + * @param string aTitle + * The new page title URI. + */ + onLocationChange: function WCA_onLocationChange(aState, aURI, aTitle) + { + // TODO: Bug 792062 - Make the tabNavigated notification reusable by the Web Console + let packet = { + from: this.actorID, + type: "locationChange", + uri: aURI, + title: aTitle, + state: aState, + nativeConsoleAPI: this.hasNativeConsoleAPI(), + }; + this.conn.send(packet); + }, + + ////////////////// + // End of event handlers for various listeners. + ////////////////// + /** * Prepare a message from the console API to be sent to the remote Web Console * instance. @@ -604,6 +822,7 @@ WebConsoleActor.prototype.requestTypes = evaluateJS: WebConsoleActor.prototype.onEvaluateJS, autocomplete: WebConsoleActor.prototype.onAutocomplete, clearMessagesCache: WebConsoleActor.prototype.onClearMessagesCache, + setPreferences: WebConsoleActor.prototype.onSetPreferences, }; /** @@ -678,3 +897,375 @@ WebConsoleObjectActor.prototype.requestTypes = "release": WebConsoleObjectActor.prototype.onRelease, }; + +/** + * Creates an actor for a network event. + * + * @constructor + * @param object aNetworkEvent + * The network event you want to use the actor for. + * @param object aWebConsoleActor + * The parent WebConsoleActor instance for this object. + */ +function NetworkEventActor(aNetworkEvent, aWebConsoleActor) +{ + this.parent = aWebConsoleActor; + this.conn = this.parent.conn; + + this._startedDateTime = aNetworkEvent.startedDateTime; + + this._request = { + method: aNetworkEvent.method, + url: aNetworkEvent.url, + httpVersion: aNetworkEvent.httpVersion, + headers: [], + cookies: [], + headersSize: aNetworkEvent.headersSize, + postData: {}, + }; + + this._response = { + headers: [], + cookies: [], + content: {}, + }; + + this._timings = {}; + + this._discardRequestBody = aNetworkEvent.discardRequestBody; + this._discardResponseBody = aNetworkEvent.discardResponseBody; +} + +NetworkEventActor.prototype = +{ + _request: null, + _response: null, + _timings: null, + + actorPrefix: "netEvent", + + /** + * Returns a grip for this actor for returning in a protocol message. + */ + grip: function NEA_grip() + { + return { + actor: this.actorID, + startedDateTime: this._startedDateTime, + url: this._request.url, + method: this._request.method, + }; + }, + + /** + * Releases this actor from the pool. + */ + release: function NEA_release() + { + this.parent.releaseNetworkEvent(this); + }, + + /** + * Handle a protocol request to release a grip. + */ + onRelease: function NEA_onRelease() + { + this.release(); + return {}; + }, + + /** + * The "getRequestHeaders" packet type handler. + * + * @return object + * The response packet - network request headers. + */ + onGetRequestHeaders: function NEA_onGetRequestHeaders() + { + return { + from: this.actorID, + headers: this._request.headers, + headersSize: this._request.headersSize, + }; + }, + + /** + * The "getRequestCookies" packet type handler. + * + * @return object + * The response packet - network request cookies. + */ + onGetRequestCookies: function NEA_onGetRequestCookies() + { + return { + from: this.actorID, + cookies: this._request.cookies, + }; + }, + + /** + * The "getRequestPostData" packet type handler. + * + * @return object + * The response packet - network POST data. + */ + onGetRequestPostData: function NEA_onGetRequestPostData() + { + return { + from: this.actorID, + postData: this._request.postData, + postDataDiscarded: this._discardRequestBody, + }; + }, + + /** + * The "getResponseHeaders" packet type handler. + * + * @return object + * The response packet - network response headers. + */ + onGetResponseHeaders: function NEA_onGetResponseHeaders() + { + return { + from: this.actorID, + headers: this._response.headers, + headersSize: this._response.headersSize, + }; + }, + + /** + * The "getResponseCookies" packet type handler. + * + * @return object + * The response packet - network response cookies. + */ + onGetResponseCookies: function NEA_onGetResponseCookies() + { + return { + from: this.actorID, + cookies: this._response.cookies, + }; + }, + + /** + * The "getResponseContent" packet type handler. + * + * @return object + * The response packet - network response content. + */ + onGetResponseContent: function NEA_onGetResponseContent() + { + return { + from: this.actorID, + content: this._response.content, + contentDiscarded: this._discardResponseBody, + }; + }, + + /** + * The "getEventTimings" packet type handler. + * + * @return object + * The response packet - network event timings. + */ + onGetEventTimings: function NEA_onGetEventTimings() + { + return { + from: this.actorID, + timings: this._timings, + totalTime: this._totalTime, + }; + }, + + /****************************************************************** + * Listeners for new network event data coming from NetworkMonitor. + ******************************************************************/ + + /** + * Add network request headers. + * + * @param array aHeaders + * The request headers array. + */ + addRequestHeaders: function NEA_addRequestHeaders(aHeaders) + { + this._request.headers = aHeaders; + + let packet = { + from: this.actorID, + type: "networkEventUpdate", + updateType: "requestHeaders", + headers: aHeaders.length, + headersSize: this._request.headersSize, + }; + + this.conn.send(packet); + }, + + /** + * Add network request cookies. + * + * @param array aCookies + * The request cookies array. + */ + addRequestCookies: function NEA_addRequestCookies(aCookies) + { + this._request.cookies = aCookies; + + let packet = { + from: this.actorID, + type: "networkEventUpdate", + updateType: "requestCookies", + cookies: aCookies.length, + }; + + this.conn.send(packet); + }, + + /** + * Add network request POST data. + * + * @param object aPostData + * The request POST data. + */ + addRequestPostData: function NEA_addRequestPostData(aPostData) + { + this._request.postData = aPostData; + + let packet = { + from: this.actorID, + type: "networkEventUpdate", + updateType: "requestPostData", + dataSize: aPostData.text.length, + discardRequestBody: this._discardRequestBody, + }; + + this.conn.send(packet); + }, + + /** + * Add the initial network response information. + * + * @param object aInfo + * The response information. + */ + addResponseStart: function NEA_addResponseStart(aInfo) + { + this._response.httpVersion = aInfo.httpVersion; + this._response.status = aInfo.status; + this._response.statusText = aInfo.statusText; + this._response.headersSize = aInfo.headersSize; + this._discardResponseBody = aInfo.discardResponseBody; + + let packet = { + from: this.actorID, + type: "networkEventUpdate", + updateType: "responseStart", + response: aInfo, + }; + + this.conn.send(packet); + }, + + /** + * Add network response headers. + * + * @param array aHeaders + * The response headers array. + */ + addResponseHeaders: function NEA_addResponseHeaders(aHeaders) + { + this._response.headers = aHeaders; + + let packet = { + from: this.actorID, + type: "networkEventUpdate", + updateType: "responseHeaders", + headers: aHeaders.length, + headersSize: this._response.headersSize, + }; + + this.conn.send(packet); + }, + + /** + * Add network response cookies. + * + * @param array aCookies + * The response cookies array. + */ + addResponseCookies: function NEA_addResponseCookies(aCookies) + { + this._response.cookies = aCookies; + + let packet = { + from: this.actorID, + type: "networkEventUpdate", + updateType: "responseCookies", + cookies: aCookies.length, + }; + + this.conn.send(packet); + }, + + /** + * Add network response content. + * + * @param object aContent + * The response content. + * @param boolean aDiscardedResponseBody + * Tells if the response content was recorded or not. + */ + addResponseContent: + function NEA_addResponseContent(aContent, aDiscardedResponseBody) + { + this._response.content = aContent; + + let packet = { + from: this.actorID, + type: "networkEventUpdate", + updateType: "responseContent", + mimeType: aContent.mimeType, + contentSize: aContent.text.length, + discardResponseBody: aDiscardedResponseBody, + }; + + this.conn.send(packet); + }, + + /** + * Add network event timing information. + * + * @param number aTotal + * The total time of the network event. + * @param object aTimings + * Timing details about the network event. + */ + addEventTimings: function NEA_addEventTimings(aTotal, aTimings) + { + this._totalTime = aTotal; + this._timings = aTimings; + + let packet = { + from: this.actorID, + type: "networkEventUpdate", + updateType: "eventTimings", + totalTime: aTotal, + }; + + this.conn.send(packet); + }, +}; + +NetworkEventActor.prototype.requestTypes = +{ + "release": NetworkEventActor.prototype.onRelease, + "getRequestHeaders": NetworkEventActor.prototype.onGetRequestHeaders, + "getRequestCookies": NetworkEventActor.prototype.onGetRequestCookies, + "getRequestPostData": NetworkEventActor.prototype.onGetRequestPostData, + "getResponseHeaders": NetworkEventActor.prototype.onGetResponseHeaders, + "getResponseCookies": NetworkEventActor.prototype.onGetResponseCookies, + "getResponseContent": NetworkEventActor.prototype.onGetResponseContent, + "getEventTimings": NetworkEventActor.prototype.onGetEventTimings, +}; + From 2ebc30fe3e409015dafa105ce43a1c51f36f4277 Mon Sep 17 00:00:00 2001 From: Mihai Sucan Date: Fri, 5 Oct 2012 19:15:51 +0300 Subject: [PATCH 15/18] Bug 768096 - Web Console remote debugging protocol support - Part 4: cleanups; r=robcee --- browser/devtools/jar.mn | 1 - browser/devtools/shared/DeveloperToolbar.jsm | 98 +- ...browser_toolbar_webconsole_errors_count.js | 7 +- .../devtools/webconsole/HUDService-content.js | 2541 ----------------- browser/devtools/webconsole/HUDService.jsm | 125 +- .../browser_console_log_inspectable_object.js | 2 +- .../browser_webconsole_bug_595223_file_uri.js | 4 +- ...e_bug_651501_document_body_autocomplete.js | 3 +- ...wser_webconsole_bug_737873_mixedcontent.js | 7 +- .../test/test-bug-737873-mixedcontent.html | 1 + browser/devtools/webconsole/webconsole.js | 89 +- .../devtools/webconsole/WebConsoleUtils.jsm | 204 +- 12 files changed, 120 insertions(+), 2962 deletions(-) delete mode 100644 browser/devtools/webconsole/HUDService-content.js diff --git a/browser/devtools/jar.mn b/browser/devtools/jar.mn index 1a509ef2927c..35ce86e07099 100644 --- a/browser/devtools/jar.mn +++ b/browser/devtools/jar.mn @@ -6,7 +6,6 @@ browser.jar: content/browser/devtools/markup-view.xhtml (markupview/markup-view.xhtml) content/browser/devtools/markup-view.css (markupview/markup-view.css) content/browser/NetworkPanel.xhtml (webconsole/NetworkPanel.xhtml) - content/browser/devtools/HUDService-content.js (webconsole/HUDService-content.js) content/browser/devtools/webconsole.js (webconsole/webconsole.js) * content/browser/devtools/webconsole.xul (webconsole/webconsole.xul) * content/browser/scratchpad.xul (scratchpad/scratchpad.xul) diff --git a/browser/devtools/shared/DeveloperToolbar.jsm b/browser/devtools/shared/DeveloperToolbar.jsm index 5007ca5e1be7..c70f1c751b0a 100644 --- a/browser/devtools/shared/DeveloperToolbar.jsm +++ b/browser/devtools/shared/DeveloperToolbar.jsm @@ -9,9 +9,6 @@ const EXPORTED_SYMBOLS = [ "DeveloperToolbar" ]; const NS_XHTML = "http://www.w3.org/1999/xhtml"; const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; -const WEBCONSOLE_CONTENT_SCRIPT_URL = - "chrome://browser/content/devtools/HUDService-content.js"; - Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); Components.utils.import("resource://gre/modules/Services.jsm"); Components.utils.import("resource:///modules/devtools/Commands.jsm"); @@ -27,6 +24,9 @@ XPCOMUtils.defineLazyModuleGetter(this, "gcli", XPCOMUtils.defineLazyModuleGetter(this, "CmdCommands", "resource:///modules/devtools/CmdCmd.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "PageErrorListener", + "resource://gre/modules/devtools/WebConsoleUtils.jsm"); + /** * Due to a number of panel bugs we need a way to check if we are running on * Linux. See the comments for TooltipPanel and OutputPanel for further details. @@ -57,7 +57,8 @@ function DeveloperToolbar(aChromeWindow, aToolbarElement) this._lastState = NOTIFICATIONS.HIDE; this._pendingShowCallback = undefined; this._pendingHide = false; - this._errorsCount = {}; + this._errorsCount = Object.create(null); + this._errorListeners = Object.create(null); this._webConsoleButton = this._doc .getElementById("developer-toolbar-webconsole"); @@ -89,9 +90,6 @@ const NOTIFICATIONS = { */ DeveloperToolbar.prototype.NOTIFICATIONS = NOTIFICATIONS; -DeveloperToolbar.prototype._contentMessageListeners = - ["WebConsole:CachedMessages", "WebConsole:PageError"]; - /** * Is the toolbar open? */ @@ -286,21 +284,18 @@ DeveloperToolbar.prototype._initErrorsCount = function DT__initErrorsCount(aTab) return; } - let messageManager = aTab.linkedBrowser.messageManager; - messageManager.loadFrameScript(WEBCONSOLE_CONTENT_SCRIPT_URL, true); + let window = aTab.linkedBrowser.contentWindow; + let listener = new PageErrorListener(window, { + onPageError: this._onPageError.bind(this, tabId), + }); + listener.init(); + this._errorListeners[tabId] = listener; this._errorsCount[tabId] = 0; - this._contentMessageListeners.forEach(function(aName) { - messageManager.addMessageListener(aName, this); - }, this); + let messages = listener.getCachedMessages(); + messages.forEach(this._onPageError.bind(this, tabId)); - let message = { - features: ["PageError"], - cachedMessages: ["PageError"], - }; - - this.sendMessageToTab(aTab, "WebConsole:Init", message); this._updateErrorsCount(); }; @@ -320,14 +315,10 @@ DeveloperToolbar.prototype._stopErrorsCount = function DT__stopErrorsCount(aTab) return; } - this.sendMessageToTab(aTab, "WebConsole:Destroy", {}); - - let messageManager = aTab.linkedBrowser.messageManager; - this._contentMessageListeners.forEach(function(aName) { - messageManager.removeMessageListener(aName, this); - }, this); - + this._errorListeners[tabId].destroy(); + delete this._errorListeners[tabId]; delete this._errorsCount[tabId]; + this._updateErrorsCount(); }; @@ -435,61 +426,13 @@ DeveloperToolbar.prototype.handleEvent = function DT_handleEvent(aEvent) }; /** - * The handler of messages received from the nsIMessageManager. - * - * @param object aMessage the message received from the content process. - */ -DeveloperToolbar.prototype.receiveMessage = function DT_receiveMessage(aMessage) -{ - if (!aMessage.json || !(aMessage.json.hudId in this._errorsCount)) { - return; - } - - let tabId = aMessage.json.hudId; - let errors = this._errorsCount[tabId]; - - switch (aMessage.name) { - case "WebConsole:PageError": - this._onPageError(tabId, aMessage.json.pageError); - break; - case "WebConsole:CachedMessages": - aMessage.json.messages.forEach(this._onPageError.bind(this, tabId)); - break; - } - - if (errors != this._errorsCount[tabId]) { - this._updateErrorsCount(tabId); - } -}; - -/** - * Send a message to the content process using the nsIMessageManager of the - * given tab. - * - * @param nsIDOMNode aTab the tab you want to send a message to. - * @param string aName the name of the message you want to send. - * @param object aMessage the message to send. - */ -DeveloperToolbar.prototype.sendMessageToTab = -function DT_sendMessageToTab(aTab, aName, aMessage) -{ - let tabId = aTab.linkedPanel; - aMessage.hudId = tabId; - if (!("id" in aMessage)) { - aMessage.id = "DevToolbar-" + this.sequenceId; - } - - aTab.linkedBrowser.messageManager.sendAsyncMessage(aName, aMessage); -}; - -/** - * Process a "WebConsole:PageError" message received from the given tab. This - * method counts the JavaScript exceptions received. + * Count a page error received for the currently selected tab. This + * method counts the JavaScript exceptions received and CSS errors/warnings. * * @private * @param string aTabId the ID of the tab from where the page error comes. - * @param object aPageError the page error object received from the content - * process. + * @param object aPageError the page error object received from the + * PageErrorListener. */ DeveloperToolbar.prototype._onPageError = function DT__onPageError(aTabId, aPageError) @@ -502,6 +445,7 @@ function DT__onPageError(aTabId, aPageError) } this._errorsCount[aTabId]++; + this._updateErrorsCount(aTabId); }; /** diff --git a/browser/devtools/shared/test/browser_toolbar_webconsole_errors_count.js b/browser/devtools/shared/test/browser_toolbar_webconsole_errors_count.js index 9e1d3a43a4b7..9dfbdc12c00b 100644 --- a/browser/devtools/shared/test/browser_toolbar_webconsole_errors_count.js +++ b/browser/devtools/shared/test/browser_toolbar_webconsole_errors_count.js @@ -47,8 +47,11 @@ function test() { function addErrors() { expectUncaughtException(); - let button = content.document.querySelector("button"); - EventUtils.synthesizeMouse(button, 2, 2, {}, content); + + waitForFocus(function() { + let button = content.document.querySelector("button"); + EventUtils.synthesizeMouse(button, 2, 2, {}, content); + }, content); waitForValue({ name: "button shows one more error after click in page", diff --git a/browser/devtools/webconsole/HUDService-content.js b/browser/devtools/webconsole/HUDService-content.js deleted file mode 100644 index d4aa54ff2dd4..000000000000 --- a/browser/devtools/webconsole/HUDService-content.js +++ /dev/null @@ -1,2541 +0,0 @@ -/* -*- Mode: js2; js2-basic-offset: 2; indent-tabs-mode: nil; -*- */ -/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ -/* 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/. */ - -"use strict"; - -// This code is appended to the browser content script. -(function _HUDServiceContent() { -let Cc = Components.classes; -let Ci = Components.interfaces; -let Cu = Components.utils; -const STRINGS_URI = "chrome://browser/locale/devtools/webconsole.properties"; - -let tempScope = {}; -Cu.import("resource://gre/modules/XPCOMUtils.jsm", tempScope); -Cu.import("resource://gre/modules/Services.jsm", tempScope); -Cu.import("resource://gre/modules/ConsoleAPIStorage.jsm", tempScope); -Cu.import("resource://gre/modules/devtools/WebConsoleUtils.jsm", tempScope); -Cu.import("resource://gre/modules/devtools/NetworkHelper.jsm", tempScope); -Cu.import("resource://gre/modules/NetUtil.jsm", tempScope); - -let XPCOMUtils = tempScope.XPCOMUtils; -let Services = tempScope.Services; -let gConsoleStorage = tempScope.ConsoleAPIStorage; -let WebConsoleUtils = tempScope.WebConsoleUtils; -let l10n = new WebConsoleUtils.l10n(STRINGS_URI); -let JSPropertyProvider = tempScope.JSPropertyProvider; -let NetworkHelper = tempScope.NetworkHelper; -let NetUtil = tempScope.NetUtil; -tempScope = null; - -let activityDistributor = Cc["@mozilla.org/network/http-activity-distributor;1"].getService(Ci.nsIHttpActivityDistributor); - -let _alive = true; // Track if this content script should still be alive. - -/** - * The Web Console content instance manager. - */ -let Manager = { - get window() content, - hudId: null, - _sequence: 0, - _messageListeners: ["WebConsole:Init", "WebConsole:EnableFeature", - "WebConsole:DisableFeature", "WebConsole:SetPreferences", - "WebConsole:GetPreferences", "WebConsole:Destroy"], - _messageHandlers: null, - _enabledFeatures: null, - _prefs: { }, - - /** - * Getter for a unique ID for the current Web Console content instance. - */ - get sequenceId() "HUDContent-" + (++this._sequence), - - /** - * Initialize the Web Console manager. - */ - init: function Manager_init() - { - this._enabledFeatures = []; - this._messageHandlers = {}; - - this._messageListeners.forEach(function(aName) { - addMessageListener(aName, this); - }, this); - - // Need to track the owner XUL window to listen to the unload and TabClose - // events, to avoid memory leaks. - let xulWindow = this._xulWindow(); - xulWindow.addEventListener("unload", this._onXULWindowClose, false); - - let tabContainer = xulWindow.gBrowser.tabContainer; - tabContainer.addEventListener("TabClose", this._onTabClose, false); - - // Need to track private browsing change and quit application notifications, - // again to avoid memory leaks. The Web Console main process cannot notify - // this content script when the XUL window close, tab close, private - // browsing change and quit application events happen, so we must call - // Manager.destroy() on our own. - Services.obs.addObserver(this, "private-browsing-change-granted", false); - Services.obs.addObserver(this, "quit-application-granted", false); - }, - - /** - * The message handler. This method forwards all the remote messages to the - * appropriate code. - */ - receiveMessage: function Manager_receiveMessage(aMessage) - { - if (!_alive || !aMessage.json) { - return; - } - - if (aMessage.name == "WebConsole:Init" && !this.hudId) { - this._onInit(aMessage.json); - return; - } - if (aMessage.json.hudId != this.hudId) { - return; - } - - switch (aMessage.name) { - case "WebConsole:EnableFeature": - this.enableFeature(aMessage.json.feature, aMessage.json); - break; - case "WebConsole:DisableFeature": - this.disableFeature(aMessage.json.feature); - break; - case "WebConsole:GetPreferences": - this.handleGetPreferences(aMessage.json); - break; - case "WebConsole:SetPreferences": - this.handleSetPreferences(aMessage.json); - break; - case "WebConsole:Destroy": - this.destroy(); - break; - default: { - let handler = this._messageHandlers[aMessage.name]; - handler && handler(aMessage.json); - break; - } - } - }, - - /** - * Observe notifications from the nsIObserverService. - * - * @param mixed aSubject - * @param string aTopic - * @param mixed aData - */ - observe: function Manager_observe(aSubject, aTopic, aData) - { - if (_alive && (aTopic == "quit-application-granted" || - (aTopic == "private-browsing-change-granted" && - (aData == "enter" || aData == "exit")))) { - this.destroy(); - } - }, - - /** - * The manager initialization code. This method is called when the Web Console - * remote process initializes the content process (this code!). - * - * @param object aMessage - * The object received from the remote process. The WebConsole:Init - * message properties: - * - hudId - (required) the remote Web Console instance ID. - * - features - (optional) array of features you want to enable from - * the start. For each feature you enable you can pass feature-specific - * options in a property on the JSON object you send with the same name - * as the feature. See this.enableFeature() for the list of available - * features. - * - preferences - (optional) an object of preferences you want to set. - * Use keys for preference names and values for preference values. - * - cachedMessages - (optional) an array of cached messages you want - * to receive. See this._sendCachedMessages() for the list of available - * message types. - * - * Example message: - * { - * hudId: "foo1", - * features: ["JSTerm", "ConsoleAPI"], - * ConsoleAPI: { ... }, // ConsoleAPI-specific options - * cachedMessages: ["ConsoleAPI"], - * preferences: {"foo.bar": true}, - * } - */ - _onInit: function Manager_onInit(aMessage) - { - this.hudId = aMessage.hudId; - - if (aMessage.preferences) { - this.handleSetPreferences({ preferences: aMessage.preferences }); - } - - if (aMessage.features) { - aMessage.features.forEach(function(aFeature) { - this.enableFeature(aFeature, aMessage[aFeature]); - }, this); - } - - if (aMessage.cachedMessages) { - this._sendCachedMessages(aMessage.cachedMessages); - } - - this.sendMessage("WebConsole:Initialized", {}); - }, - - /** - * Add a remote message handler. This is used by other components of the Web - * Console content script. - * - * @param string aName - * Message name to listen for. - * @param function aCallback - * Function to execute when the message is received. This function is - * given the JSON object that came from the remote Web Console - * instance. - * Only one callback per message name is allowed! - */ - addMessageHandler: function Manager_addMessageHandler(aName, aCallback) - { - if (aName in this._messageHandlers) { - Cu.reportError("Web Console content script: addMessageHandler() called for an existing message handler: " + aName); - return; - } - - this._messageHandlers[aName] = aCallback; - addMessageListener(aName, this); - }, - - /** - * Remove the message handler for the given name. - * - * @param string aName - * Message name for the handler you want removed. - */ - removeMessageHandler: function Manager_removeMessageHandler(aName) - { - if (!(aName in this._messageHandlers)) { - return; - } - - delete this._messageHandlers[aName]; - removeMessageListener(aName, this); - }, - - /** - * Send a message to the remote Web Console instance. - * - * @param string aName - * The name of the message you want to send. - * @param object aMessage - * The message object you want to send. - */ - sendMessage: function Manager_sendMessage(aName, aMessage) - { - aMessage.hudId = this.hudId; - if (!("id" in aMessage)) { - aMessage.id = this.sequenceId; - } - - sendAsyncMessage(aName, aMessage); - }, - - /** - * Enable a feature in the Web Console content script. A feature is generally - * a set of observers/listeners that are added in the content process. This - * content script exposes the data via the message manager for the features - * you enable. - * - * Supported features: - * - JSTerm - a JavaScript "terminal" which allows code execution. - * - ConsoleAPI - support for routing the window.console API to the remote - * process. - * - PageError - route all the nsIScriptErrors from the nsIConsoleService - * to the remote process. - * - NetworkMonitor - log all the network activity and send HAR-like - * messages to the remote Web Console process. - * - LocationChange - log page location changes. See - * ConsoleProgressListener. - * - * @param string aFeature - * One of the supported features. - * @param object [aMessage] - * Optional JSON message object coming from the remote Web Console - * instance. This can be used for feature-specific options. - */ - enableFeature: function Manager_enableFeature(aFeature, aMessage) - { - if (this._enabledFeatures.indexOf(aFeature) != -1) { - return; - } - - switch (aFeature) { - case "JSTerm": - JSTerm.init(aMessage); - break; - case "ConsoleAPI": - ConsoleAPIObserver.init(aMessage); - break; - case "PageError": - ConsoleListener.init(aMessage); - break; - case "NetworkMonitor": - NetworkMonitor.init(aMessage); - break; - case "LocationChange": - ConsoleProgressListener.startMonitor(ConsoleProgressListener - .MONITOR_LOCATION_CHANGE); - ConsoleProgressListener.sendLocation(this.window.location.href, - this.window.document.title); - break; - default: - Cu.reportError("Web Console content: unknown feature " + aFeature); - break; - } - - this._enabledFeatures.push(aFeature); - }, - - /** - * Disable a Web Console content script feature. - * - * @see this.enableFeature - * @param string aFeature - * One of the supported features - see this.enableFeature() for the - * list of supported features. - */ - disableFeature: function Manager_disableFeature(aFeature) - { - let index = this._enabledFeatures.indexOf(aFeature); - if (index == -1) { - return; - } - this._enabledFeatures.splice(index, 1); - - switch (aFeature) { - case "JSTerm": - JSTerm.destroy(); - break; - case "ConsoleAPI": - ConsoleAPIObserver.destroy(); - break; - case "PageError": - ConsoleListener.destroy(); - break; - case "NetworkMonitor": - NetworkMonitor.destroy(); - break; - case "LocationChange": - ConsoleProgressListener.stopMonitor(ConsoleProgressListener - .MONITOR_LOCATION_CHANGE); - break; - default: - Cu.reportError("Web Console content: unknown feature " + aFeature); - break; - } - }, - - /** - * Handle the "WebConsole:GetPreferences" messages from the remote Web Console - * instance. - * - * @param object aMessage - * The JSON object of the remote message. This object holds one - * property: preferences. The |preferences| value must be an array of - * preference names you want to retrieve the values for. - * A "WebConsole:Preferences" message is sent back to the remote Web - * Console instance. The message holds a |preferences| object which has - * key names for preference names and values for each preference value. - */ - handleGetPreferences: function Manager_handleGetPreferences(aMessage) - { - let prefs = {}; - aMessage.preferences.forEach(function(aName) { - prefs[aName] = this.getPreference(aName); - }, this); - - this.sendMessage("WebConsole:Preferences", {preferences: prefs}); - }, - - /** - * Handle the "WebConsole:SetPreferences" messages from the remote Web Console - * instance. - * - * @param object aMessage - * The JSON object of the remote message. This object holds one - * property: preferences. The |preferences| value must be an object of - * preference names as keys and preference values as object values, for - * each preference you want to change. - */ - handleSetPreferences: function Manager_handleSetPreferences(aMessage) - { - for (let key in aMessage.preferences) { - this.setPreference(key, aMessage.preferences[key]); - } - }, - - /** - * Retrieve a preference. - * - * @param string aName - * Preference name. - * @return mixed|null - * Preference value. Null is returned if the preference does not - * exist. - */ - getPreference: function Manager_getPreference(aName) - { - return aName in this._prefs ? this._prefs[aName] : null; - }, - - /** - * Set a preference to a new value. - * - * @param string aName - * Preference name. - * @param mixed aValue - * Preference value. - */ - setPreference: function Manager_setPreference(aName, aValue) - { - this._prefs[aName] = aValue; - }, - - /** - * Send the cached messages to the remote Web Console instance. - * - * @private - * @param array aMessageTypes - * An array that lists which kinds of messages you want. Supported - * message types: "ConsoleAPI" and "PageError". - */ - _sendCachedMessages: function Manager__sendCachedMessages(aMessageTypes) - { - let messages = []; - - while (aMessageTypes.length > 0) { - switch (aMessageTypes.shift()) { - case "ConsoleAPI": - messages.push.apply(messages, ConsoleAPIObserver.getCachedMessages()); - break; - case "PageError": - messages.push.apply(messages, ConsoleListener.getCachedMessages()); - break; - } - } - - messages.sort(function(a, b) { return a.timeStamp - b.timeStamp; }); - - this.sendMessage("WebConsole:CachedMessages", {messages: messages}); - }, - - /** - * The XUL window "unload" event handler which destroys this content script - * instance. - * @private - */ - _onXULWindowClose: function Manager__onXULWindowClose() - { - if (_alive) { - Manager.destroy(); - } - }, - - /** - * The "TabClose" event handler which destroys this content script - * instance, if needed. - * @private - */ - _onTabClose: function Manager__onTabClose(aEvent) - { - let tab = aEvent.target; - if (_alive && tab.linkedBrowser.contentWindow === Manager.window) { - Manager.destroy(); - } - }, - - /** - * Find the XUL window that owns the content script. - * @private - * @return Window - * The XUL window that owns the content script. - */ - _xulWindow: function Manager__xulWindow() - { - return this.window.QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIWebNavigation).QueryInterface(Ci.nsIDocShell) - .chromeEventHandler.ownerDocument.defaultView; - }, - - /** - * Destroy the Web Console content script instance. - */ - destroy: function Manager_destroy() - { - Services.obs.removeObserver(this, "private-browsing-change-granted"); - Services.obs.removeObserver(this, "quit-application-granted"); - - _alive = false; - let xulWindow = this._xulWindow(); - xulWindow.removeEventListener("unload", this._onXULWindowClose, false); - let tabContainer = xulWindow.gBrowser.tabContainer; - tabContainer.removeEventListener("TabClose", this._onTabClose, false); - - this._messageListeners.forEach(function(aName) { - removeMessageListener(aName, this); - }, this); - - this._enabledFeatures.slice().forEach(this.disableFeature, this); - - this.hudId = null; - this._messageHandlers = null; - - Manager = ConsoleAPIObserver = JSTerm = ConsoleListener = NetworkMonitor = - NetworkResponseListener = ConsoleProgressListener = null; - - XPCOMUtils = gConsoleStorage = WebConsoleUtils = l10n = JSPropertyProvider = - null; - }, -}; - -/////////////////////////////////////////////////////////////////////////////// -// JavaScript Terminal -/////////////////////////////////////////////////////////////////////////////// - -/** - * JSTerm helper functions. - * - * Defines a set of functions ("helper functions") that are available from the - * Web Console but not from the web page. - * - * A list of helper functions used by Firebug can be found here: - * http://getfirebug.com/wiki/index.php/Command_Line_API - */ -function JSTermHelper(aJSTerm) -{ - /** - * Find a node by ID. - * - * @param string aId - * The ID of the element you want. - * @return nsIDOMNode or null - * The result of calling document.querySelector(aSelector). - */ - aJSTerm.sandbox.$ = function JSTH_$(aSelector) - { - return aJSTerm.window.document.querySelector(aSelector); - }; - - /** - * Find the nodes matching a CSS selector. - * - * @param string aSelector - * A string that is passed to window.document.querySelectorAll. - * @return nsIDOMNodeList - * Returns the result of document.querySelectorAll(aSelector). - */ - aJSTerm.sandbox.$$ = function JSTH_$$(aSelector) - { - return aJSTerm.window.document.querySelectorAll(aSelector); - }; - - /** - * Runs an xPath query and returns all matched nodes. - * - * @param string aXPath - * xPath search query to execute. - * @param [optional] nsIDOMNode aContext - * Context to run the xPath query on. Uses window.document if not set. - * @returns array of nsIDOMNode - */ - aJSTerm.sandbox.$x = function JSTH_$x(aXPath, aContext) - { - let nodes = []; - let doc = aJSTerm.window.document; - let aContext = aContext || doc; - - try { - let results = doc.evaluate(aXPath, aContext, null, - Ci.nsIDOMXPathResult.ANY_TYPE, null); - let node; - while (node = results.iterateNext()) { - nodes.push(node); - } - } - catch (ex) { - aJSTerm.console.error(ex.message); - } - - return nodes; - }; - - /** - * Returns the currently selected object in the highlighter. - * - * Warning: this implementation crosses the process boundaries! This is not - * usable within a remote browser. To implement this feature correctly we need - * support for remote inspection capabilities within the Inspector as well. - * - * @return nsIDOMElement|null - * The DOM element currently selected in the highlighter. - */ - Object.defineProperty(aJSTerm.sandbox, "$0", { - get: function() { - try { - return Manager._xulWindow().InspectorUI.selection; - } - catch (ex) { - aJSTerm.console.error(ex.message); - } - }, - enumerable: true, - configurable: false - }); - - /** - * Clears the output of the JSTerm. - */ - aJSTerm.sandbox.clear = function JSTH_clear() - { - aJSTerm.helperEvaluated = true; - Manager.sendMessage("JSTerm:ClearOutput", {}); - }; - - /** - * Returns the result of Object.keys(aObject). - * - * @param object aObject - * Object to return the property names from. - * @returns array of string - */ - aJSTerm.sandbox.keys = function JSTH_keys(aObject) - { - return Object.keys(WebConsoleUtils.unwrap(aObject)); - }; - - /** - * Returns the values of all properties on aObject. - * - * @param object aObject - * Object to display the values from. - * @returns array of string - */ - aJSTerm.sandbox.values = function JSTH_values(aObject) - { - let arrValues = []; - let obj = WebConsoleUtils.unwrap(aObject); - - try { - for (let prop in obj) { - arrValues.push(obj[prop]); - } - } - catch (ex) { - aJSTerm.console.error(ex.message); - } - return arrValues; - }; - - /** - * Opens a help window in MDN. - */ - aJSTerm.sandbox.help = function JSTH_help() - { - aJSTerm.helperEvaluated = true; - aJSTerm.window.open( - "https://developer.mozilla.org/docs/Tools/Web_Console/Helpers", - "help", ""); - }; - - /** - * Inspects the passed aObject. This is done by opening the PropertyPanel. - * - * @param object aObject - * Object to inspect. - */ - aJSTerm.sandbox.inspect = function JSTH_inspect(aObject) - { - if (!WebConsoleUtils.isObjectInspectable(aObject)) { - return aObject; - } - - aJSTerm.helperEvaluated = true; - - let message = { - input: aJSTerm._evalInput, - objectCacheId: Manager.sequenceId, - }; - - message.resultObject = - aJSTerm.prepareObjectForRemote(WebConsoleUtils.unwrap(aObject), - message.objectCacheId); - - Manager.sendMessage("JSTerm:InspectObject", message); - }; - - /** - * Prints aObject to the output. - * - * @param object aObject - * Object to print to the output. - * @return string - */ - aJSTerm.sandbox.pprint = function JSTH_pprint(aObject) - { - aJSTerm.helperEvaluated = true; - if (aObject === null || aObject === undefined || aObject === true || - aObject === false) { - aJSTerm.console.error(l10n.getStr("helperFuncUnsupportedTypeError")); - return; - } - else if (typeof aObject == "function") { - aJSTerm.helperRawOutput = true; - return aObject + "\n"; - } - - aJSTerm.helperRawOutput = true; - - let output = []; - let pairs = WebConsoleUtils.namesAndValuesOf(WebConsoleUtils.unwrap(aObject)); - pairs.forEach(function(aPair) { - output.push(aPair.name + ": " + aPair.value); - }); - - return " " + output.join("\n "); - }; - - /** - * Print a string to the output, as-is. - * - * @param string aString - * A string you want to output. - * @returns void - */ - aJSTerm.sandbox.print = function JSTH_print(aString) - { - aJSTerm.helperEvaluated = true; - aJSTerm.helperRawOutput = true; - return String(aString); - }; -} - -/** - * The JavaScript terminal is meant to allow remote code execution for the Web - * Console. - */ -let JSTerm = { - get window() Manager.window, - get console() this.window.console, - - /** - * The Cu.Sandbox() object where code is evaluated. - */ - sandbox: null, - - _sandboxLocation: null, - _messageHandlers: {}, - - /** - * Evaluation result objects are cached in this object. The chrome process can - * request any object based on its ID. - */ - _objectCache: null, - - /** - * Initialize the JavaScript terminal feature. - * - * @param object aMessage - * Options for JSTerm sent from the remote Web Console instance. This - * object holds the following properties: - * - * - notifyNonNativeConsoleAPI - boolean that tells if you want to be - * notified if the window.console API object in the page is not the - * native one (if the page overrides it). - * A "JSTerm:NonNativeConsoleAPI" message will be sent if this is the - * case. - */ - init: function JST_init(aMessage) - { - this._objectCache = {}; - this._messageHandlers = { - "JSTerm:EvalRequest": this.handleEvalRequest, - "JSTerm:GetEvalObject": this.handleGetEvalObject, - "JSTerm:Autocomplete": this.handleAutocomplete, - "JSTerm:ClearObjectCache": this.handleClearObjectCache, - }; - - for (let name in this._messageHandlers) { - let handler = this._messageHandlers[name].bind(this); - Manager.addMessageHandler(name, handler); - } - - if (aMessage && aMessage.notifyNonNativeConsoleAPI) { - let consoleObject = WebConsoleUtils.unwrap(this.window).console; - if (!("__mozillaConsole__" in consoleObject)) { - Manager.sendMessage("JSTerm:NonNativeConsoleAPI", {}); - } - } - }, - - /** - * Handler for the "JSTerm:EvalRequest" remote message. This method evaluates - * user input in the JavaScript sandbox and sends the result back to the - * remote process. The "JSTerm:EvalResult" message includes the following - * data: - * - id - the same ID as the EvalRequest (for tracking purposes). - * - input - the JS string that was evaluated. - * - resultString - the evaluation result converted to a string formatted - * for display. - * - timestamp - timestamp when evaluation occurred (Date.now(), - * milliseconds since the UNIX epoch). - * - inspectable - boolean that tells if the evaluation result object can be - * inspected or not. - * - error - the evaluation exception object (if any). - * - errorMessage - the exception object converted to a string (if any error - * occurred). - * - helperResult - boolean that tells if a JSTerm helper was evaluated. - * - helperRawOutput - boolean that tells if the helper evaluation result - * should be displayed as raw output. - * - * If the result object is inspectable then two additional properties are - * included: - * - childrenCacheId - tells where child objects are cached. This is the - * same as aRequest.resultCacheId. - * - resultObject - the result object prepared for the remote process. See - * this.prepareObjectForRemote(). - * - * @param object aRequest - * The code evaluation request object: - * - id - request ID. - * - str - string to evaluate. - * - resultCacheId - where to cache the evaluation child objects. - */ - handleEvalRequest: function JST_handleEvalRequest(aRequest) - { - let id = aRequest.id; - let input = aRequest.str; - let result, error = null; - let timestamp; - - this.helperEvaluated = false; - this.helperRawOutput = false; - this._evalInput = input; - try { - timestamp = Date.now(); - result = this.evalInSandbox(input); - } - catch (ex) { - error = ex; - } - delete this._evalInput; - - let inspectable = !error && WebConsoleUtils.isObjectInspectable(result); - let resultString = undefined; - if (!error) { - resultString = this.helperRawOutput ? result : - WebConsoleUtils.formatResult(result); - } - - let message = { - id: id, - input: input, - resultString: resultString, - timestamp: timestamp, - error: error, - errorMessage: error ? String(error) : null, - inspectable: inspectable, - helperResult: this.helperEvaluated, - helperRawOutput: this.helperRawOutput, - }; - - if (inspectable) { - message.childrenCacheId = aRequest.resultCacheId; - message.resultObject = - this.prepareObjectForRemote(result, message.childrenCacheId); - } - - Manager.sendMessage("JSTerm:EvalResult", message); - }, - - /** - * Handler for the remote "JSTerm:GetEvalObject" message. This allows the - * remote Web Console instance to retrieve an object from the content process. - * The "JSTerm:EvalObject" message is sent back to the remote process: - * - id - the request ID, used to trace back to the initial request. - * - cacheId - the cache ID where the requested object is stored. - * - objectId - the ID of the object being sent. - * - object - the object representation prepared for remote inspection. See - * this.prepareObjectForRemote(). - * - childrenCacheId - the cache ID where any child object of |object| are - * stored. - * - * @param object aRequest - * The message that requests the content object. Properties: cacheId, - * objectId and resultCacheId. - * - * Evaluated objects are stored in "buckets" (cache IDs). Each object - * is assigned an ID (object ID). You can request a specific object - * (objectId) from a specific cache (cacheId) and tell where the result - * should be cached (resultCacheId). The requested object can have - * further references to other objects - those references will be - * cached in the "bucket" of your choice (based on resultCacheId). If - * you do not provide any resultCacheId in the request message, then - * cacheId will be used. - */ - handleGetEvalObject: function JST_handleGetEvalObject(aRequest) - { - if (aRequest.cacheId in this._objectCache && - aRequest.objectId in this._objectCache[aRequest.cacheId]) { - let object = this._objectCache[aRequest.cacheId][aRequest.objectId]; - let resultCacheId = aRequest.resultCacheId || aRequest.cacheId; - let message = { - id: aRequest.id, - cacheId: aRequest.cacheId, - objectId: aRequest.objectId, - object: this.prepareObjectForRemote(object, resultCacheId), - childrenCacheId: resultCacheId, - }; - Manager.sendMessage("JSTerm:EvalObject", message); - } - else { - Cu.reportError("JSTerm:GetEvalObject request " + aRequest.id + - ": stale object."); - } - }, - - /** - * Handler for the remote "JSTerm:ClearObjectCache" message. This allows the - * remote Web Console instance to clear the cache of objects that it no longer - * uses. - * - * @param object aRequest - * An object that holds one property: the cacheId you want cleared. - */ - handleClearObjectCache: function JST_handleClearObjectCache(aRequest) - { - if (aRequest.cacheId in this._objectCache) { - delete this._objectCache[aRequest.cacheId]; - } - }, - - /** - * Prepare an object to be sent to the remote Web Console instance. - * - * @param object aObject - * The object you want to send to the remote Web Console instance. - * @param number aCacheId - * Cache ID where you want object references to be stored into. The - * given object may include references to other objects - those - * references will be stored in the given cache ID so the remote - * process can later retrieve them as well. - * @return array - * An array that holds one element for each enumerable property and - * method in aObject. Each element describes the property. For details - * see WebConsoleUtils.namesAndValuesOf(). - */ - prepareObjectForRemote: function JST_prepareObjectForRemote(aObject, aCacheId) - { - // Cache the properties that have inspectable values. - let propCache = this._objectCache[aCacheId] || {}; - let result = WebConsoleUtils.namesAndValuesOf(aObject, propCache); - if (!(aCacheId in this._objectCache) && Object.keys(propCache).length > 0) { - this._objectCache[aCacheId] = propCache; - } - - return result; - }, - - /** - * Handler for the "JSTerm:Autocomplete" remote message. This handler provides - * completion results for user input. The "JSterm:AutocompleteProperties" - * message is sent to the remote process: - * - id - the same as request ID. - * - input - the user input (same as in the request message). - * - matches - an array of matched properties (strings). - * - matchProp - the part that was used from the user input for finding the - * matches. For details see the JSPropertyProvider description and - * implementation. - * - * - * @param object aRequest - * The remote request object which holds two properties: an |id| and - * the user |input|. - */ - handleAutocomplete: function JST_handleAutocomplete(aRequest) - { - let result = JSPropertyProvider(this.window, aRequest.input) || {}; - let message = { - id: aRequest.id, - input: aRequest.input, - matches: result.matches || [], - matchProp: result.matchProp, - }; - Manager.sendMessage("JSTerm:AutocompleteProperties", message); - }, - - /** - * Create the JavaScript sandbox where user input is evaluated. - * @private - */ - _createSandbox: function JST__createSandbox() - { - this._sandboxLocation = this.window.location; - this.sandbox = new Cu.Sandbox(this.window, { - sandboxPrototype: this.window, - wantXrays: false, - }); - - this.sandbox.console = this.console; - - JSTermHelper(this); - }, - - /** - * Evaluates a string in the sandbox. - * - * @param string aString - * String to evaluate in the sandbox. - * @return mixed - * The result of the evaluation. - */ - evalInSandbox: function JST_evalInSandbox(aString) - { - // If the user changed to a different location, we need to update the - // sandbox. - if (this._sandboxLocation !== this.window.location) { - this._createSandbox(); - } - - // The help function needs to be easy to guess, so we make the () optional - if (aString.trim() == "help" || aString.trim() == "?") { - aString = "help()"; - } - - let window = WebConsoleUtils.unwrap(this.sandbox.window); - let $ = null, $$ = null; - - // We prefer to execute the page-provided implementations for the $() and - // $$() functions. - if (typeof window.$ == "function") { - $ = this.sandbox.$; - delete this.sandbox.$; - } - if (typeof window.$$ == "function") { - $$ = this.sandbox.$$; - delete this.sandbox.$$; - } - - let result = Cu.evalInSandbox(aString, this.sandbox, "1.8", - "Web Console", 1); - - if ($) { - this.sandbox.$ = $; - } - if ($$) { - this.sandbox.$$ = $$; - } - - return result; - }, - - /** - * Destroy the JSTerm instance. - */ - destroy: function JST_destroy() - { - for (let name in this._messageHandlers) { - Manager.removeMessageHandler(name); - } - - delete this.sandbox; - delete this._sandboxLocation; - delete this._messageHandlers; - delete this._objectCache; - }, -}; - -/////////////////////////////////////////////////////////////////////////////// -// The window.console API observer -/////////////////////////////////////////////////////////////////////////////// - -/** - * The window.console API observer. This allows the window.console API messages - * to be sent to the remote Web Console instance. - */ -let ConsoleAPIObserver = { - QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]), - - /** - * Initialize the window.console API observer. - */ - init: function CAO_init() - { - // Note that the observer is process-wide. We will filter the messages as - // needed, see CAO_observe(). - Services.obs.addObserver(this, "console-api-log-event", false); - - Manager.addMessageHandler("ConsoleAPI:ClearCache", - this.handleClearCache.bind(this)); - }, - - /** - * The console API message observer. When messages are received from the - * observer service we forward them to the remote Web Console instance. - * - * @param object aMessage - * The message object receives from the observer service. - * @param string aTopic - * The message topic received from the observer service. - */ - observe: function CAO_observe(aMessage, aTopic) - { - if (!_alive || !aMessage || aTopic != "console-api-log-event") { - return; - } - - let apiMessage = aMessage.wrappedJSObject; - - let msgWindow = - WebConsoleUtils.getWindowByOuterId(apiMessage.ID, Manager.window); - if (!msgWindow || msgWindow.top != Manager.window) { - // Not the same window! - return; - } - - let messageToChrome = {}; - this._prepareApiMessageForRemote(apiMessage, messageToChrome); - Manager.sendMessage("WebConsole:ConsoleAPI", messageToChrome); - }, - - /** - * Prepare a message from the console APi to be sent to the remote Web Console - * instance. - * - * @param object aOriginalMessage - * The original message received from console-api-log-event. - * @param object aRemoteMessage - * The object you want to send to the remote Web Console. This object - * is updated to hold information from the original message. New - * properties added: - * - timeStamp - * Message timestamp (same as the aOriginalMessage.timeStamp property). - * - apiMessage - * An object that copies almost all the properties from - * aOriginalMessage. Arguments might be skipped if it holds references - * to objects that cannot be sent as they are to the remote Web Console - * instance. - * - argumentsToString - * Optional: the aOriginalMessage.arguments object stringified. - * - * The apiMessage.arguments property is set to hold data appropriate - * to the message level. A similar approach is used for - * argumentsToString. - */ - _prepareApiMessageForRemote: - function CAO__prepareApiMessageForRemote(aOriginalMessage, aRemoteMessage) - { - aRemoteMessage.apiMessage = - WebConsoleUtils.cloneObject(aOriginalMessage, true, - function(aKey, aValue, aObject) { - // We need to skip the arguments property from the original object. - if (aKey == "wrappedJSObject" || aObject === aOriginalMessage && - aKey == "arguments") { - return false; - } - return true; - }); - - aRemoteMessage.timeStamp = aOriginalMessage.timeStamp; - - switch (aOriginalMessage.level) { - case "trace": - case "time": - case "timeEnd": - case "group": - case "groupCollapsed": - aRemoteMessage.apiMessage.arguments = - WebConsoleUtils.cloneObject(aOriginalMessage.arguments, true); - break; - - case "groupEnd": - aRemoteMessage.argumentsToString = - Array.map(aOriginalMessage.arguments || [], - this._formatObject.bind(this)); - break; - - case "log": - case "info": - case "warn": - case "error": - case "debug": - case "dir": { - aRemoteMessage.objectsCacheId = Manager.sequenceId; - aRemoteMessage.argumentsToString = []; - let mapFunction = function(aItem) { - let formattedObject = this._formatObject(aItem); - aRemoteMessage.argumentsToString.push(formattedObject); - if (WebConsoleUtils.isObjectInspectable(aItem)) { - return JSTerm.prepareObjectForRemote(aItem, - aRemoteMessage.objectsCacheId); - } - return formattedObject; - }.bind(this); - - aRemoteMessage.apiMessage.arguments = - Array.map(aOriginalMessage.arguments || [], mapFunction); - break; - } - default: - Cu.reportError("Unknown Console API log level: " + - aOriginalMessage.level); - break; - } - }, - - /** - * Format an object's value to be displayed in the Web Console. - * - * @private - * @param object aObject - * The object you want to display. - * @return string - * The string you can display for the given object. - */ - _formatObject: function CAO__formatObject(aObject) - { - return typeof aObject == "string" ? - aObject : WebConsoleUtils.formatResult(aObject); - }, - - /** - * Get the cached messages for the current inner window. - * - * @see this._prepareApiMessageForRemote() - * @return array - * The array of cached messages. Each element is a Console API - * prepared to be sent to the remote Web Console instance. - */ - getCachedMessages: function CAO_getCachedMessages() - { - let innerWindowId = WebConsoleUtils.getInnerWindowId(Manager.window); - let messages = gConsoleStorage.getEvents(innerWindowId); - - let result = messages.map(function(aMessage) { - let remoteMessage = { _type: "ConsoleAPI" }; - this._prepareApiMessageForRemote(aMessage.wrappedJSObject, remoteMessage); - return remoteMessage; - }, this); - - return result; - }, - - /** - * Handler for the "ConsoleAPI:ClearCache" message. - */ - handleClearCache: function CAO_handleClearCache() - { - let windowId = WebConsoleUtils.getInnerWindowId(Manager.window); - gConsoleStorage.clearEvents(windowId); - }, - - /** - * Destroy the ConsoleAPIObserver listeners. - */ - destroy: function CAO_destroy() - { - Manager.removeMessageHandler("ConsoleAPI:ClearCache"); - Services.obs.removeObserver(this, "console-api-log-event"); - }, -}; - -/////////////////////////////////////////////////////////////////////////////// -// The page errors listener -/////////////////////////////////////////////////////////////////////////////// - -/** - * The nsIConsoleService listener. This is used to send all the page errors - * (JavaScript, CSS and more) to the remote Web Console instance. - */ -let ConsoleListener = { - QueryInterface: XPCOMUtils.generateQI([Ci.nsIConsoleListener]), - - /** - * Initialize the nsIConsoleService listener. - */ - init: function CL_init() - { - Services.console.registerListener(this); - }, - - /** - * The nsIConsoleService observer. This method takes all the script error - * messages belonging to the current window and sends them to the remote Web - * Console instance. - * - * @param nsIScriptError aScriptError - * The script error object coming from the nsIConsoleService. - */ - observe: function CL_observe(aScriptError) - { - if (!_alive || !(aScriptError instanceof Ci.nsIScriptError) || - !aScriptError.outerWindowID) { - return; - } - - if (!this.isCategoryAllowed(aScriptError.category)) { - return; - } - - let errorWindow = - WebConsoleUtils.getWindowByOuterId(aScriptError.outerWindowID, - Manager.window); - if (!errorWindow || errorWindow.top != Manager.window) { - return; - } - - Manager.sendMessage("WebConsole:PageError", { pageError: aScriptError }); - }, - - - /** - * Check if the given script error category is allowed to be tracked or not. - * We ignore chrome-originating errors as we only care about content. - * - * @param string aCategory - * The nsIScriptError category you want to check. - * @return boolean - * True if the category is allowed to be logged, false otherwise. - */ - isCategoryAllowed: function CL_isCategoryAllowed(aCategory) - { - switch (aCategory) { - case "XPConnect JavaScript": - case "component javascript": - case "chrome javascript": - case "chrome registration": - case "XBL": - case "XBL Prototype Handler": - case "XBL Content Sink": - case "xbl javascript": - return false; - } - - return true; - }, - - /** - * Get the cached page errors for the current inner window. - * - * @return array - * The array of cached messages. Each element is an nsIScriptError - * with an added _type property so the remote Web Console instance can - * tell the difference between various types of cached messages. - */ - getCachedMessages: function CL_getCachedMessages() - { - let innerWindowId = WebConsoleUtils.getInnerWindowId(Manager.window); - let result = []; - let errors = {}; - Services.console.getMessageArray(errors, {}); - - (errors.value || []).forEach(function(aError) { - if (!(aError instanceof Ci.nsIScriptError) || - aError.innerWindowID != innerWindowId || - !this.isCategoryAllowed(aError.category)) { - return; - } - - let remoteMessage = WebConsoleUtils.cloneObject(aError); - remoteMessage._type = "PageError"; - result.push(remoteMessage); - }, this); - - return result; - }, - - /** - * Remove the nsIConsoleService listener. - */ - destroy: function CL_destroy() - { - Services.console.unregisterListener(this); - }, -}; - -/////////////////////////////////////////////////////////////////////////////// -// Network logging -/////////////////////////////////////////////////////////////////////////////// - -// The maximum uint32 value. -const PR_UINT32_MAX = 4294967295; - -// HTTP status codes. -const HTTP_MOVED_PERMANENTLY = 301; -const HTTP_FOUND = 302; -const HTTP_SEE_OTHER = 303; -const HTTP_TEMPORARY_REDIRECT = 307; - -// The maximum number of bytes a NetworkResponseListener can hold. -const RESPONSE_BODY_LIMIT = 1048576; // 1 MB - -/** - * The network response listener implements the nsIStreamListener and - * nsIRequestObserver interfaces. This is used within the NetworkMonitor feature - * to get the response body of the request. - * - * The code is mostly based on code listings from: - * - * http://www.softwareishard.com/blog/firebug/ - * nsitraceablechannel-intercept-http-traffic/ - * - * @constructor - * @param object aHttpActivity - * HttpActivity object associated with this request. Once the request is - * complete the aHttpActivity object is updated to include the response - * headers and body. - */ -function NetworkResponseListener(aHttpActivity) { - this.receivedData = ""; - this.httpActivity = aHttpActivity; - this.bodySize = 0; -} - -NetworkResponseListener.prototype = { - QueryInterface: - XPCOMUtils.generateQI([Ci.nsIStreamListener, Ci.nsIInputStreamCallback, - Ci.nsIRequestObserver, Ci.nsISupports]), - - /** - * This NetworkResponseListener tracks the NetworkMonitor.openResponses object - * to find the associated uncached headers. - * @private - */ - _foundOpenResponse: false, - - /** - * The response will be written into the outputStream of this nsIPipe. - * Both ends of the pipe must be blocking. - */ - sink: null, - - /** - * The HttpActivity object associated with this response. - */ - httpActivity: null, - - /** - * Stores the received data as a string. - */ - receivedData: null, - - /** - * The network response body size. - */ - bodySize: null, - - /** - * The nsIRequest we are started for. - */ - request: null, - - /** - * Set the async listener for the given nsIAsyncInputStream. This allows us to - * wait asynchronously for any data coming from the stream. - * - * @param nsIAsyncInputStream aStream - * The input stream from where we are waiting for data to come in. - * @param nsIInputStreamCallback aListener - * The input stream callback you want. This is an object that must have - * the onInputStreamReady() method. If the argument is null, then the - * current callback is removed. - * @return void - */ - setAsyncListener: function NRL_setAsyncListener(aStream, aListener) - { - // Asynchronously wait for the stream to be readable or closed. - aStream.asyncWait(aListener, 0, 0, Services.tm.mainThread); - }, - - /** - * Stores the received data, if request/response body logging is enabled. It - * also does limit the number of stored bytes, based on the - * RESPONSE_BODY_LIMIT constant. - * - * Learn more about nsIStreamListener at: - * https://developer.mozilla.org/en/XPCOM_Interface_Reference/nsIStreamListener - * - * @param nsIRequest aRequest - * @param nsISupports aContext - * @param nsIInputStream aInputStream - * @param unsigned long aOffset - * @param unsigned long aCount - */ - onDataAvailable: - function NRL_onDataAvailable(aRequest, aContext, aInputStream, aOffset, aCount) - { - this._findOpenResponse(); - let data = NetUtil.readInputStreamToString(aInputStream, aCount); - - this.bodySize += aCount; - - if (!this.httpActivity.meta.discardResponseBody && - this.receivedData.length < RESPONSE_BODY_LIMIT) { - this.receivedData += NetworkHelper. - convertToUnicode(data, aRequest.contentCharset); - } - }, - - /** - * See documentation at - * https://developer.mozilla.org/En/NsIRequestObserver - * - * @param nsIRequest aRequest - * @param nsISupports aContext - */ - onStartRequest: function NRL_onStartRequest(aRequest) - { - this.request = aRequest; - this._findOpenResponse(); - // Asynchronously wait for the data coming from the request. - this.setAsyncListener(this.sink.inputStream, this); - }, - - /** - * Handle the onStopRequest by closing the sink output stream. - * - * For more documentation about nsIRequestObserver go to: - * https://developer.mozilla.org/En/NsIRequestObserver - */ - onStopRequest: function NRL_onStopRequest() - { - this._findOpenResponse(); - this.sink.outputStream.close(); - }, - - /** - * Find the open response object associated to the current request. The - * NetworkMonitor.httpResponseExaminer() method saves the response headers in - * NetworkMonitor.openResponses. This method takes the data from the open - * response object and puts it into the HTTP activity object, then sends it to - * the remote Web Console instance. - * - * @private - */ - _findOpenResponse: function NRL__findOpenResponse() - { - if (!_alive || this._foundOpenResponse) { - return; - } - - let openResponse = null; - - for each (let item in NetworkMonitor.openResponses) { - if (item.channel === this.httpActivity.channel) { - openResponse = item; - break; - } - } - - if (!openResponse) { - return; - } - this._foundOpenResponse = true; - - let logResponse = this.httpActivity.log.entries[0].response; - logResponse.headers = openResponse.headers; - logResponse.httpVersion = openResponse.httpVersion; - logResponse.status = openResponse.status; - logResponse.statusText = openResponse.statusText; - if (openResponse.cookies) { - logResponse.cookies = openResponse.cookies; - } - - delete NetworkMonitor.openResponses[openResponse.id]; - - this.httpActivity.meta.stages.push("http-on-examine-response"); - NetworkMonitor.sendActivity(this.httpActivity); - }, - - /** - * Clean up the response listener once the response input stream is closed. - * This is called from onStopRequest() or from onInputStreamReady() when the - * stream is closed. - * @return void - */ - onStreamClose: function NRL_onStreamClose() - { - if (!this.httpActivity) { - return; - } - // Remove our listener from the request input stream. - this.setAsyncListener(this.sink.inputStream, null); - - this._findOpenResponse(); - - let meta = this.httpActivity.meta; - let entry = this.httpActivity.log.entries[0]; - let request = entry.request; - let response = entry.response; - - meta.stages.push("REQUEST_STOP"); - - if (!meta.discardResponseBody && this.receivedData.length) { - this._onComplete(this.receivedData); - } - else if (!meta.discardResponseBody && response.status == 304) { - // Response is cached, so we load it from cache. - let charset = this.request.contentCharset || this.httpActivity.charset; - NetworkHelper.loadFromCache(request.url, charset, - this._onComplete.bind(this)); - } - else { - this._onComplete(); - } - }, - - /** - * Handler for when the response completes. This function cleans up the - * response listener. - * - * @param string [aData] - * Optional, the received data coming from the response listener or - * from the cache. - */ - _onComplete: function NRL__onComplete(aData) - { - let response = this.httpActivity.log.entries[0].response; - - try { - response.bodySize = response.status != 304 ? this.request.contentLength : 0; - } - catch (ex) { - response.bodySize = -1; - } - - try { - response.content = { mimeType: this.request.contentType }; - } - catch (ex) { - response.content = { mimeType: "" }; - } - - if (response.content.mimeType && this.request.contentCharset) { - response.content.mimeType += "; charset=" + this.request.contentCharset; - } - - response.content.size = this.bodySize || (aData || "").length; - - if (aData) { - response.content.text = aData; - } - - this.receivedData = ""; - - if (_alive) { - NetworkMonitor.sendActivity(this.httpActivity); - } - - this.httpActivity.channel = null; - this.httpActivity = null; - this.sink = null; - this.inputStream = null; - this.request = null; - }, - - /** - * The nsIInputStreamCallback for when the request input stream is ready - - * either it has more data or it is closed. - * - * @param nsIAsyncInputStream aStream - * The sink input stream from which data is coming. - * @returns void - */ - onInputStreamReady: function NRL_onInputStreamReady(aStream) - { - if (!(aStream instanceof Ci.nsIAsyncInputStream) || !this.httpActivity) { - return; - } - - let available = -1; - try { - // This may throw if the stream is closed normally or due to an error. - available = aStream.available(); - } - catch (ex) { } - - if (available != -1) { - if (available != 0) { - // Note that passing 0 as the offset here is wrong, but the - // onDataAvailable() method does not use the offset, so it does not - // matter. - this.onDataAvailable(this.request, null, aStream, 0, available); - } - this.setAsyncListener(aStream, this); - } - else { - this.onStreamClose(); - } - }, -}; - -/** - * The network monitor uses the nsIHttpActivityDistributor to monitor network - * requests. The nsIObserverService is also used for monitoring - * http-on-examine-response notifications. All network request information is - * routed to the remote Web Console. - */ -let NetworkMonitor = { - httpTransactionCodes: { - 0x5001: "REQUEST_HEADER", - 0x5002: "REQUEST_BODY_SENT", - 0x5003: "RESPONSE_START", - 0x5004: "RESPONSE_HEADER", - 0x5005: "RESPONSE_COMPLETE", - 0x5006: "TRANSACTION_CLOSE", - - 0x804b0003: "STATUS_RESOLVING", - 0x804b000b: "STATUS_RESOLVED", - 0x804b0007: "STATUS_CONNECTING_TO", - 0x804b0004: "STATUS_CONNECTED_TO", - 0x804b0005: "STATUS_SENDING_TO", - 0x804b000a: "STATUS_WAITING_FOR", - 0x804b0006: "STATUS_RECEIVING_FROM" - }, - - harCreator: { - name: Services.appinfo.name + " - Web Console", - version: Services.appinfo.version, - }, - - // Network response bodies are piped through a buffer of the given size (in - // bytes). - responsePipeSegmentSize: null, - - /** - * Whether to save the bodies of network requests and responses. Disabled by - * default to save memory. - */ - get saveRequestAndResponseBodies() { - return Manager.getPreference("NetworkMonitor.saveRequestAndResponseBodies"); - }, - - openRequests: null, - openResponses: null, - progressListener: null, - - /** - * The network monitor initializer. - * - * @param object aMessage - * Initialization object sent by the remote Web Console instance. This - * object can hold one property: monitorFileActivity - a boolean that - * tells if monitoring of file:// requests should be enabled as well or - * not. - */ - init: function NM_init(aMessage) - { - this.responsePipeSegmentSize = Services.prefs - .getIntPref("network.buffer.cache.size"); - - this.openRequests = {}; - this.openResponses = {}; - - activityDistributor.addObserver(this); - - Services.obs.addObserver(this.httpResponseExaminer, - "http-on-examine-response", false); - - // Monitor file:// activity as well. - if (aMessage && aMessage.monitorFileActivity) { - ConsoleProgressListener.startMonitor(ConsoleProgressListener - .MONITOR_FILE_ACTIVITY); - } - }, - - /** - * Observe notifications for the http-on-examine-response topic, coming from - * the nsIObserverService. - * - * @param nsIHttpChannel aSubject - * @param string aTopic - * @returns void - */ - httpResponseExaminer: function NM_httpResponseExaminer(aSubject, aTopic) - { - // The httpResponseExaminer is used to retrieve the uncached response - // headers. The data retrieved is stored in openResponses. The - // NetworkResponseListener is responsible with updating the httpActivity - // object with the data from the new object in openResponses. - - if (!_alive || aTopic != "http-on-examine-response" || - !(aSubject instanceof Ci.nsIHttpChannel)) { - return; - } - - let channel = aSubject.QueryInterface(Ci.nsIHttpChannel); - // Try to get the source window of the request. - let win = NetworkHelper.getWindowForRequest(channel); - if (!win || win.top !== Manager.window) { - return; - } - - let response = { - id: Manager.sequenceId, - channel: channel, - headers: [], - cookies: [], - }; - - let setCookieHeader = null; - - channel.visitResponseHeaders({ - visitHeader: function NM__visitHeader(aName, aValue) { - let lowerName = aName.toLowerCase(); - if (lowerName == "set-cookie") { - setCookieHeader = aValue; - } - response.headers.push({ name: aName, value: aValue }); - } - }); - - if (!response.headers.length) { - return; // No need to continue. - } - - if (setCookieHeader) { - response.cookies = NetworkHelper.parseSetCookieHeader(setCookieHeader); - } - - // Determine the HTTP version. - let httpVersionMaj = {}; - let httpVersionMin = {}; - - channel.QueryInterface(Ci.nsIHttpChannelInternal); - channel.getResponseVersion(httpVersionMaj, httpVersionMin); - - response.status = channel.responseStatus; - response.statusText = channel.responseStatusText; - response.httpVersion = "HTTP/" + httpVersionMaj.value + "." + - httpVersionMin.value; - - NetworkMonitor.openResponses[response.id] = response; - }, - - /** - * Begin observing HTTP traffic that originates inside the current tab. - * - * @see https://developer.mozilla.org/en/XPCOM_Interface_Reference/nsIHttpActivityObserver - * - * @param nsIHttpChannel aChannel - * @param number aActivityType - * @param number aActivitySubtype - * @param number aTimestamp - * @param number aExtraSizeData - * @param string aExtraStringData - */ - observeActivity: - function NM_observeActivity(aChannel, aActivityType, aActivitySubtype, - aTimestamp, aExtraSizeData, aExtraStringData) - { - if (!_alive || - aActivityType != activityDistributor.ACTIVITY_TYPE_HTTP_TRANSACTION && - aActivityType != activityDistributor.ACTIVITY_TYPE_SOCKET_TRANSPORT) { - return; - } - - if (!(aChannel instanceof Ci.nsIHttpChannel)) { - return; - } - - aChannel = aChannel.QueryInterface(Ci.nsIHttpChannel); - - if (aActivitySubtype == - activityDistributor.ACTIVITY_SUBTYPE_REQUEST_HEADER) { - this._onRequestHeader(aChannel, aTimestamp, aExtraStringData); - return; - } - - // Iterate over all currently ongoing requests. If aChannel can't - // be found within them, then exit this function. - let httpActivity = null; - for each (let item in this.openRequests) { - if (item.channel === aChannel) { - httpActivity = item; - break; - } - } - - if (!httpActivity) { - return; - } - - let transCodes = this.httpTransactionCodes; - - // Store the time information for this activity subtype. - if (aActivitySubtype in transCodes) { - let stage = transCodes[aActivitySubtype]; - if (stage in httpActivity.timings) { - httpActivity.timings[stage].last = aTimestamp; - } - else { - httpActivity.meta.stages.push(stage); - httpActivity.timings[stage] = { - first: aTimestamp, - last: aTimestamp, - }; - } - } - - switch (aActivitySubtype) { - case activityDistributor.ACTIVITY_SUBTYPE_REQUEST_BODY_SENT: - this._onRequestBodySent(httpActivity); - break; - case activityDistributor.ACTIVITY_SUBTYPE_RESPONSE_HEADER: - this._onResponseHeader(httpActivity, aExtraStringData); - break; - case activityDistributor.ACTIVITY_SUBTYPE_TRANSACTION_CLOSE: - this._onTransactionClose(httpActivity); - break; - default: - break; - } - }, - - /** - * Handler for ACTIVITY_SUBTYPE_REQUEST_HEADER. When a request starts the - * headers are sent to the server. This method creates the |httpActivity| - * object where we store the request and response information that is - * collected through its lifetime. - * - * @private - * @param nsIHttpChannel aChannel - * @param number aTimestamp - * @param string aExtraStringData - * @return void - */ - _onRequestHeader: - function NM__onRequestHeader(aChannel, aTimestamp, aExtraStringData) - { - // Try to get the source window of the request. - let win = NetworkHelper.getWindowForRequest(aChannel); - if (!win || win.top !== Manager.window) { - return; - } - - let httpActivity = this.createActivityObject(aChannel); - httpActivity.charset = win.document.characterSet; // see NM__onRequestBodySent() - httpActivity.meta.stages.push("REQUEST_HEADER"); // activity stage (aActivitySubtype) - - httpActivity.timings.REQUEST_HEADER = { - first: aTimestamp, - last: aTimestamp - }; - - let entry = httpActivity.log.entries[0]; - entry.startedDateTime = new Date(Math.round(aTimestamp / 1000)).toISOString(); - - let request = httpActivity.log.entries[0].request; - - let cookieHeader = null; - - // Copy the request header data. - aChannel.visitRequestHeaders({ - visitHeader: function NM__visitHeader(aName, aValue) - { - if (aName == "Cookie") { - cookieHeader = aValue; - } - request.headers.push({ name: aName, value: aValue }); - } - }); - - if (cookieHeader) { - request.cookies = NetworkHelper.parseCookieHeader(cookieHeader); - } - - // Determine the HTTP version. - let httpVersionMaj = {}; - let httpVersionMin = {}; - - aChannel.QueryInterface(Ci.nsIHttpChannelInternal); - aChannel.getRequestVersion(httpVersionMaj, httpVersionMin); - - request.httpVersion = "HTTP/" + httpVersionMaj.value + "." + - httpVersionMin.value; - - request.headersSize = aExtraStringData.length; - - this._setupResponseListener(httpActivity); - - this.openRequests[httpActivity.id] = httpActivity; - - this.sendActivity(httpActivity); - }, - - /** - * Create the empty HTTP activity object. This object is used for storing all - * the request and response information. - * - * This is a HAR-like object. Conformance to the spec is not guaranteed at - * this point. - * - * TODO: Bug 708717 - Add support for network log export to HAR - * - * @see http://www.softwareishard.com/blog/har-12-spec - * @param nsIHttpChannel aChannel - * The HTTP channel for which the HTTP activity object is created. - * @return object - * The new HTTP activity object. - */ - createActivityObject: function NM_createActivityObject(aChannel) - { - return { - hudId: Manager.hudId, - id: Manager.sequenceId, - channel: aChannel, - charset: null, // see NM__onRequestHeader() - meta: { // holds metadata about the activity object - stages: [], // activity stages (aActivitySubtype) - discardRequestBody: !this.saveRequestAndResponseBodies, - discardResponseBody: !this.saveRequestAndResponseBodies, - }, - timings: {}, // internal timing information, see NM_observeActivity() - log: { // HAR-like object - version: "1.2", - creator: this.harCreator, - // missing |browser| and |pages| - entries: [{ // we only track one entry at a time - connection: Manager.sequenceId, // connection ID - startedDateTime: 0, // see NM__onRequestHeader() - time: 0, // see NM__setupHarTimings() - // missing |serverIPAddress| and |cache| - request: { - method: aChannel.requestMethod, - url: aChannel.URI.spec, - httpVersion: "", // see NM__onRequestHeader() - headers: [], // see NM__onRequestHeader() - cookies: [], // see NM__onRequestHeader() - queryString: [], // never set - headersSize: -1, // see NM__onRequestHeader() - bodySize: -1, // see NM__onRequestBodySent() - postData: null, // see NM__onRequestBodySent() - }, - response: { - status: 0, // see NM__onResponseHeader() - statusText: "", // see NM__onResponseHeader() - httpVersion: "", // see NM__onResponseHeader() - headers: [], // see NM_httpResponseExaminer() - cookies: [], // see NM_httpResponseExaminer() - content: null, // see NRL_onStreamClose() - redirectURL: "", // never set - headersSize: -1, // see NM__onResponseHeader() - bodySize: -1, // see NRL_onStreamClose() - }, - timings: {}, // see NM__setupHarTimings() - }], - }, - }; - }, - - /** - * Setup the network response listener for the given HTTP activity. The - * NetworkResponseListener is responsible for storing the response body. - * - * @private - * @param object aHttpActivity - * The HTTP activity object we are tracking. - */ - _setupResponseListener: function NM__setupResponseListener(aHttpActivity) - { - let channel = aHttpActivity.channel; - channel.QueryInterface(Ci.nsITraceableChannel); - - // The response will be written into the outputStream of this pipe. - // This allows us to buffer the data we are receiving and read it - // asynchronously. - // Both ends of the pipe must be blocking. - let sink = Cc["@mozilla.org/pipe;1"].createInstance(Ci.nsIPipe); - - // The streams need to be blocking because this is required by the - // stream tee. - sink.init(false, false, this.responsePipeSegmentSize, PR_UINT32_MAX, null); - - // Add listener for the response body. - let newListener = new NetworkResponseListener(aHttpActivity); - - // Remember the input stream, so it isn't released by GC. - newListener.inputStream = sink.inputStream; - newListener.sink = sink; - - let tee = Cc["@mozilla.org/network/stream-listener-tee;1"]. - createInstance(Ci.nsIStreamListenerTee); - - let originalListener = channel.setNewListener(tee); - - tee.init(originalListener, sink.outputStream, newListener); - }, - - /** - * Send an HTTP activity object to the remote Web Console instance. - * A WebConsole:NetworkActivity message is sent. The message holds two - * properties: - * - meta - the |aHttpActivity.meta| object. - * - log - the |aHttpActivity.log| object. - * - * @param object aHttpActivity - * The HTTP activity object you want to send. - */ - sendActivity: function NM_sendActivity(aHttpActivity) - { - Manager.sendMessage("WebConsole:NetworkActivity", { - meta: aHttpActivity.meta, - log: aHttpActivity.log, - }); - }, - - /** - * Handler for ACTIVITY_SUBTYPE_REQUEST_BODY_SENT. The request body is logged - * here. - * - * @private - * @param object aHttpActivity - * The HTTP activity object we are working with. - */ - _onRequestBodySent: function NM__onRequestBodySent(aHttpActivity) - { - if (aHttpActivity.meta.discardRequestBody) { - return; - } - - let request = aHttpActivity.log.entries[0].request; - - let sentBody = NetworkHelper. - readPostTextFromRequest(aHttpActivity.channel, - aHttpActivity.charset); - - if (!sentBody && request.url == Manager.window.location.href) { - // If the request URL is the same as the current page URL, then - // we can try to get the posted text from the page directly. - // This check is necessary as otherwise the - // NetworkHelper.readPostTextFromPage() - // function is called for image requests as well but these - // are not web pages and as such don't store the posted text - // in the cache of the webpage. - sentBody = NetworkHelper.readPostTextFromPage(docShell, - aHttpActivity.charset); - } - if (!sentBody) { - return; - } - - request.postData = { - mimeType: "", // never set - params: [], // never set - text: sentBody, - }; - - request.bodySize = sentBody.length; - - this.sendActivity(aHttpActivity); - }, - - /** - * Handler for ACTIVITY_SUBTYPE_RESPONSE_HEADER. This method stores - * information about the response headers. - * - * @private - * @param object aHttpActivity - * The HTTP activity object we are working with. - * @param string aExtraStringData - * The uncached response headers. - */ - _onResponseHeader: - function NM__onResponseHeader(aHttpActivity, aExtraStringData) - { - // aExtraStringData contains the uncached response headers. The first line - // contains the response status (e.g. HTTP/1.1 200 OK). - // - // Note: The response header is not saved here. Calling the - // channel.visitResponseHeaders() methood at this point sometimes causes an - // NS_ERROR_NOT_AVAILABLE exception. - // - // We could parse aExtraStringData to get the headers and their values, but - // that is not trivial to do in an accurate manner. Hence, we save the - // response headers in this.httpResponseExaminer(). - - let response = aHttpActivity.log.entries[0].response; - - let headers = aExtraStringData.split(/\r\n|\n|\r/); - let statusLine = headers.shift(); - - let statusLineArray = statusLine.split(" "); - response.httpVersion = statusLineArray.shift(); - response.status = statusLineArray.shift(); - response.statusText = statusLineArray.join(" "); - response.headersSize = aExtraStringData.length; - - // Discard the response body for known response statuses. - switch (parseInt(response.status)) { - case HTTP_MOVED_PERMANENTLY: - case HTTP_FOUND: - case HTTP_SEE_OTHER: - case HTTP_TEMPORARY_REDIRECT: - aHttpActivity.meta.discardResponseBody = true; - break; - } - - this.sendActivity(aHttpActivity); - }, - - /** - * Handler for ACTIVITY_SUBTYPE_TRANSACTION_CLOSE. This method updates the HAR - * timing information on the HTTP activity object and clears the request - * from the list of known open requests. - * - * @private - * @param object aHttpActivity - * The HTTP activity object we work with. - */ - _onTransactionClose: function NM__onTransactionClose(aHttpActivity) - { - this._setupHarTimings(aHttpActivity); - this.sendActivity(aHttpActivity); - delete this.openRequests[aHttpActivity.id]; - }, - - /** - * Update the HTTP activity object to include timing information as in the HAR - * spec. The HTTP activity object holds the raw timing information in - * |timings| - these are timings stored for each activity notification. The - * HAR timing information is constructed based on these lower level data. - * - * @param object aHttpActivity - * The HTTP activity object we are working with. - */ - _setupHarTimings: function NM__setupHarTimings(aHttpActivity) - { - let timings = aHttpActivity.timings; - let entry = aHttpActivity.log.entries[0]; - let harTimings = entry.timings; - - // Not clear how we can determine "blocked" time. - harTimings.blocked = -1; - - // DNS timing information is available only in when the DNS record is not - // cached. - harTimings.dns = timings.STATUS_RESOLVING && timings.STATUS_RESOLVED ? - timings.STATUS_RESOLVED.last - - timings.STATUS_RESOLVING.first : -1; - - if (timings.STATUS_CONNECTING_TO && timings.STATUS_CONNECTED_TO) { - harTimings.connect = timings.STATUS_CONNECTED_TO.last - - timings.STATUS_CONNECTING_TO.first; - } - else if (timings.STATUS_SENDING_TO) { - harTimings.connect = timings.STATUS_SENDING_TO.first - - timings.REQUEST_HEADER.first; - } - else { - harTimings.connect = -1; - } - - if ((timings.STATUS_WAITING_FOR || timings.STATUS_RECEIVING_FROM) && - (timings.STATUS_CONNECTED_TO || timings.STATUS_SENDING_TO)) { - harTimings.send = (timings.STATUS_WAITING_FOR || - timings.STATUS_RECEIVING_FROM).first - - (timings.STATUS_CONNECTED_TO || - timings.STATUS_SENDING_TO).last; - } - else { - harTimings.send = -1; - } - - if (timings.RESPONSE_START) { - harTimings.wait = timings.RESPONSE_START.first - - (timings.REQUEST_BODY_SENT || - timings.STATUS_SENDING_TO).last; - } - else { - harTimings.wait = -1; - } - - if (timings.RESPONSE_START && timings.RESPONSE_COMPLETE) { - harTimings.receive = timings.RESPONSE_COMPLETE.last - - timings.RESPONSE_START.first; - } - else { - harTimings.receive = -1; - } - - entry.time = 0; - for (let timing in harTimings) { - let time = Math.max(Math.round(harTimings[timing] / 1000), -1); - harTimings[timing] = time; - if (time > -1) { - entry.time += time; - } - } - }, - - /** - * Suspend Web Console activity. This is called when all Web Consoles are - * closed. - */ - destroy: function NM_destroy() - { - Services.obs.removeObserver(this.httpResponseExaminer, - "http-on-examine-response"); - - activityDistributor.removeObserver(this); - - ConsoleProgressListener.stopMonitor(ConsoleProgressListener - .MONITOR_FILE_ACTIVITY); - - delete this.openRequests; - delete this.openResponses; - }, -}; - -/** - * A WebProgressListener that listens for location changes. - * - * This progress listener is used to track file loads and other kinds of - * location changes. - * - * When a file:// URI is loaded a "WebConsole:FileActivity" message is sent to - * the remote Web Console instance. The message JSON holds only one property: - * uri (the file URI). - * - * When the current page location changes a "WebConsole:LocationChange" message - * is sent. See ConsoleProgressListener.sendLocation() for details. - */ -let ConsoleProgressListener = { - /** - * Constant used for startMonitor()/stopMonitor() that tells you want to - * monitor file loads. - */ - MONITOR_FILE_ACTIVITY: 1, - - /** - * Constant used for startMonitor()/stopMonitor() that tells you want to - * monitor page location changes. - */ - MONITOR_LOCATION_CHANGE: 2, - - /** - * Tells if you want to monitor file activity. - * @private - * @type boolean - */ - _fileActivity: false, - - /** - * Tells if you want to monitor location changes. - * @private - * @type boolean - */ - _locationChange: false, - - /** - * Tells if the console progress listener is initialized or not. - * @private - * @type boolean - */ - _initialized: false, - - QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener, - Ci.nsISupportsWeakReference]), - - /** - * Initialize the ConsoleProgressListener. - * @private - */ - _init: function CPL__init() - { - if (this._initialized) { - return; - } - - this._initialized = true; - let webProgress = docShell.QueryInterface(Ci.nsIWebProgress); - webProgress.addProgressListener(this, Ci.nsIWebProgress.NOTIFY_STATE_ALL); - }, - - /** - * Start a monitor/tracker related to the current nsIWebProgressListener - * instance. - * - * @param number aMonitor - * Tells what you want to track. Available constants: - * - this.MONITOR_FILE_ACTIVITY - * Track file loads. - * - this.MONITOR_LOCATION_CHANGE - * Track location changes for the top window. - */ - startMonitor: function CPL_startMonitor(aMonitor) - { - switch (aMonitor) { - case this.MONITOR_FILE_ACTIVITY: - this._fileActivity = true; - break; - case this.MONITOR_LOCATION_CHANGE: - this._locationChange = true; - break; - default: - throw new Error("HUDService-content: unknown monitor type " + - aMonitor + " for the ConsoleProgressListener!"); - } - this._init(); - }, - - /** - * Stop a monitor. - * - * @param number aMonitor - * Tells what you want to stop tracking. See this.startMonitor() for - * the list of constants. - */ - stopMonitor: function CPL_stopMonitor(aMonitor) - { - switch (aMonitor) { - case this.MONITOR_FILE_ACTIVITY: - this._fileActivity = false; - break; - case this.MONITOR_LOCATION_CHANGE: - this._locationChange = false; - break; - default: - throw new Error("HUDService-content: unknown monitor type " + - aMonitor + " for the ConsoleProgressListener!"); - } - - if (!this._fileActivity && !this._locationChange) { - this.destroy(); - } - }, - - onStateChange: - function CPL_onStateChange(aProgress, aRequest, aState, aStatus) - { - if (!_alive) { - return; - } - - if (this._fileActivity) { - this._checkFileActivity(aProgress, aRequest, aState, aStatus); - } - - if (this._locationChange) { - this._checkLocationChange(aProgress, aRequest, aState, aStatus); - } - }, - - /** - * Check if there is any file load, given the arguments of - * nsIWebProgressListener.onStateChange. If the state change tells that a file - * URI has been loaded, then the remote Web Console instance is notified. - * @private - */ - _checkFileActivity: - function CPL__checkFileActivity(aProgress, aRequest, aState, aStatus) - { - if (!(aState & Ci.nsIWebProgressListener.STATE_START)) { - return; - } - - let uri = null; - if (aRequest instanceof Ci.imgIRequest) { - let imgIRequest = aRequest.QueryInterface(Ci.imgIRequest); - uri = imgIRequest.URI; - } - else if (aRequest instanceof Ci.nsIChannel) { - let nsIChannel = aRequest.QueryInterface(Ci.nsIChannel); - uri = nsIChannel.URI; - } - - if (!uri || !uri.schemeIs("file") && !uri.schemeIs("ftp")) { - return; - } - - Manager.sendMessage("WebConsole:FileActivity", {uri: uri.spec}); - }, - - /** - * Check if the current window.top location is changing, given the arguments - * of nsIWebProgressListener.onStateChange. If that is the case, the remote - * Web Console instance is notified. - * @private - */ - _checkLocationChange: - function CPL__checkLocationChange(aProgress, aRequest, aState, aStatus) - { - let isStart = aState & Ci.nsIWebProgressListener.STATE_START; - let isStop = aState & Ci.nsIWebProgressListener.STATE_STOP; - let isNetwork = aState & Ci.nsIWebProgressListener.STATE_IS_NETWORK; - let isWindow = aState & Ci.nsIWebProgressListener.STATE_IS_WINDOW; - - // Skip non-interesting states. - if (!isNetwork || !isWindow || - aProgress.DOMWindow != Manager.window) { - return; - } - - if (isStart && aRequest instanceof Ci.nsIChannel) { - this.sendLocation(aRequest.URI.spec, ""); - } - else if (isStop) { - this.sendLocation(Manager.window.location.href, - Manager.window.document.title); - } - }, - - onLocationChange: function() {}, - onStatusChange: function() {}, - onProgressChange: function() {}, - onSecurityChange: function() {}, - - /** - * Send the location of the current top window to the remote Web Console. - * A "WebConsole:LocationChange" message is sent. The JSON object holds two - * properties: location and title. - * - * @param string aLocation - * Current page address. - * @param string aTitle - * Current page title. - */ - sendLocation: function CPL_sendLocation(aLocation, aTitle) - { - let message = { - "location": aLocation, - "title": aTitle, - }; - Manager.sendMessage("WebConsole:LocationChange", message); - }, - - /** - * Destroy the ConsoleProgressListener. - */ - destroy: function CPL_destroy() - { - if (!this._initialized) { - return; - } - - this._initialized = false; - this._fileActivity = false; - this._locationChange = false; - let webProgress = docShell.QueryInterface(Ci.nsIWebProgress); - webProgress.removeProgressListener(this); - }, -}; - -Manager.init(); -})(); diff --git a/browser/devtools/webconsole/HUDService.jsm b/browser/devtools/webconsole/HUDService.jsm index 02fd3452c531..19d5e9c14fb4 100644 --- a/browser/devtools/webconsole/HUDService.jsm +++ b/browser/devtools/webconsole/HUDService.jsm @@ -57,9 +57,6 @@ const MINIMUM_PAGE_HEIGHT = 50; // The default console height, as a ratio from the content window inner height. const DEFAULT_CONSOLE_HEIGHT = 0.33; -// This script is inserted into the content process. -const CONTENT_SCRIPT_URL = "chrome://browser/content/devtools/HUDService-content.js"; - // points to the file to load in the Web Console iframe. const UI_IFRAME_URL = "chrome://browser/content/devtools/webconsole.xul"; @@ -500,9 +497,11 @@ HUD_SERVICE.prototype = function WebConsole(aTab) { this.tab = aTab; + this.chromeDocument = this.tab.ownerDocument; + this.chromeWindow = this.chromeDocument.defaultView; + this.hudId = "hud_" + this.tab.linkedPanel; this._onIframeLoad = this._onIframeLoad.bind(this); - this._asyncRequests = {}; - this._init(); + this._initUI(); } WebConsole.prototype = { @@ -512,6 +511,9 @@ WebConsole.prototype = { */ tab: null, + chromeWindow: null, + chromeDocument: null, + /** * Getter for HUDService.lastFinishedRequestCallback. * @@ -520,15 +522,6 @@ WebConsole.prototype = { */ get lastFinishedRequestCallback() HUDService.lastFinishedRequestCallback, - /** - * Track callback functions registered for specific async requests sent to - * the content process. - * - * @private - * @type object - */ - _asyncRequests: null, - /** * The xul:panel that holds the Web Console when it is positioned as a window. * @type nsIDOMElement @@ -555,22 +548,6 @@ WebConsole.prototype = { get gViewSourceUtils() this.chromeWindow.gViewSourceUtils, - /** - * Initialize the Web Console instance. - * @private - */ - _init: function WC__init() - { - this.chromeDocument = this.tab.ownerDocument; - this.chromeWindow = this.chromeDocument.defaultView; - this.messageManager = this.tab.linkedBrowser.messageManager; - this.hudId = "hud_" + this.tab.linkedPanel; - this.notificationBox = this.chromeDocument - .getElementById(this.tab.linkedPanel); - - this._initUI(); - }, - /** * Initialize the Web Console UI. This method sets up the iframe. * @private @@ -783,7 +760,7 @@ WebConsole.prototype = { // get the node position index let nodeIdx = this.positions[aPosition]; - let nBox = this.notificationBox; + let nBox = this.chromeDocument.getElementById(this.tab.linkedPanel); let node = nBox.childNodes[nodeIdx]; // check to see if console is already positioned in aPosition @@ -887,92 +864,6 @@ WebConsole.prototype = { this.chromeWindow.DeveloperToolbar.resetErrorsCount(this.tab); }, - /** - * Setup the message manager used to communicate with the Web Console content - * script. This method loads the content script, adds the message listeners - * and initializes the connection to the content script. - * - * @private - */ - _setupMessageManager: function WC__setupMessageManager() - { - this.messageManager.loadFrameScript(CONTENT_SCRIPT_URL, true); - - this._messageListeners.forEach(function(aName) { - this.messageManager.addMessageListener(aName, this.ui); - }, this); - - let message = { - features: ["NetworkMonitor", "LocationChange"], - NetworkMonitor: { monitorFileActivity: true }, - preferences: { - "NetworkMonitor.saveRequestAndResponseBodies": - this.ui.saveRequestAndResponseBodies, - }, - }; - - this.sendMessageToContent("WebConsole:Init", message); - }, - - /** - * Handler for messages that have an associated callback function. The - * this.sendMessageToContent() allows one to provide a function to be invoked - * when the content script replies to the message previously sent. This is the - * method that invokes the callback. - * - * @see this.sendMessageToContent - * @private - * @param object aResponse - * Message object received from the content script. - */ - _receiveMessageWithCallback: - function WC__receiveMessageWithCallback(aResponse) - { - if (aResponse.id in this._asyncRequests) { - let request = this._asyncRequests[aResponse.id]; - request.callback(aResponse, request.message); - delete this._asyncRequests[aResponse.id]; - } - else { - Cu.reportError("receiveMessageWithCallback response for stale request " + - "ID " + aResponse.id); - } - }, - - /** - * Send a message to the content script. - * - * @param string aName - * The name of the message you want to send. - * - * @param object aMessage - * The message object you want to send. This object needs to have no - * cyclic references and it needs to be JSON-stringifiable. - * - * @param function [aCallback] - * Optional function you want to have called when the content script - * replies to your message. Your callback receives two arguments: - * (1) the response object from the content script and (2) the message - * you sent to the content script (which is aMessage here). - */ - sendMessageToContent: - function WC_sendMessageToContent(aName, aMessage, aCallback) - { - aMessage.hudId = this.hudId; - if (!("id" in aMessage)) { - aMessage.id = "HUDChrome-" + HUDService.sequenceId(); - } - - if (aCallback) { - this._asyncRequests[aMessage.id] = { - name: aName, - message: aMessage, - callback: aCallback, - }; - } - this.messageManager.sendAsyncMessage(aName, aMessage); - }, - /** * Handler for page location changes. If the Web Console is * opened in a panel the panel title is updated. diff --git a/browser/devtools/webconsole/test/browser_console_log_inspectable_object.js b/browser/devtools/webconsole/test/browser_console_log_inspectable_object.js index 2369a98d107b..ad471b528f0c 100644 --- a/browser/devtools/webconsole/test/browser_console_log_inspectable_object.js +++ b/browser/devtools/webconsole/test/browser_console_log_inspectable_object.js @@ -8,7 +8,7 @@ function test() { waitForExplicitFinish(); - addTab("data:text/html,test for bug 676722 - inspectable objects for window.console"); + addTab("data:text/html;charset=utf8,test for bug 676722 - inspectable objects for window.console"); gBrowser.selectedBrowser.addEventListener("load", function onLoad() { gBrowser.selectedBrowser.removeEventListener("load", onLoad, true); diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_595223_file_uri.js b/browser/devtools/webconsole/test/browser_webconsole_bug_595223_file_uri.js index f0f07f383cfd..f78b35baf8bf 100644 --- a/browser/devtools/webconsole/test/browser_webconsole_bug_595223_file_uri.js +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_595223_file_uri.js @@ -37,14 +37,14 @@ function test() { let uri = Services.io.newFileURI(dir); - addTab(uri.spec); + addTab("data:text/html;charset=utf8,

test file URI"); browser.addEventListener("load", function tabLoad() { browser.removeEventListener("load", tabLoad, true); openConsole(null, function(aHud) { hud = aHud; hud.jsterm.clearOutput(); browser.addEventListener("load", tabReload, true); - content.location.reload(); + content.location = uri.spec; }); }, true); } diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_651501_document_body_autocomplete.js b/browser/devtools/webconsole/test/browser_webconsole_bug_651501_document_body_autocomplete.js index 76a99fa9325f..d25a63a51622 100644 --- a/browser/devtools/webconsole/test/browser_webconsole_bug_651501_document_body_autocomplete.js +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_651501_document_body_autocomplete.js @@ -38,7 +38,8 @@ function consoleOpened(aHud) { // __defineGetter__ __defineSetter__ __lookupGetter__ __lookupSetter__ // constructor hasOwnProperty isPrototypeOf propertyIsEnumerable // toLocaleString toSource toString unwatch valueOf watch. - let props = WCU.namesAndValuesOf(content.wrappedJSObject.document.body); + let props = WCU.inspectObject(content.wrappedJSObject.document.body, + function() { }); is(popup.itemCount, 14 + props.length, "popup.itemCount is correct"); popup._panel.addEventListener("popuphidden", autocompletePopupHidden, false); diff --git a/browser/devtools/webconsole/test/browser_webconsole_bug_737873_mixedcontent.js b/browser/devtools/webconsole/test/browser_webconsole_bug_737873_mixedcontent.js index ba362e65978a..98912d4da5cf 100644 --- a/browser/devtools/webconsole/test/browser_webconsole_bug_737873_mixedcontent.js +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_737873_mixedcontent.js @@ -13,7 +13,7 @@ const TEST_HTTPS_URI = "https://example.com/browser/browser/devtools/webconsole/test/test-bug-737873-mixedcontent.html"; function test() { - addTab("data:text/html,Web Console basic network logging test"); + addTab("data:text/html;charset=utf8,Web Console mixed content test"); browser.addEventListener("load", onLoad, true); } @@ -68,8 +68,9 @@ function testClickOpenNewTab(warningNode) { let oldOpenUILinkIn = window.openUILinkIn; window.openUILinkIn = function(aLink) { - if (aLink == "https://developer.mozilla.org/en/Security/MixedContent"); - linkOpened = true; + if (aLink == "https://developer.mozilla.org/en/Security/MixedContent") { + linkOpened = true; + } } EventUtils.synthesizeMouse(warningNode, 2, 2, {}, diff --git a/browser/devtools/webconsole/test/test-bug-737873-mixedcontent.html b/browser/devtools/webconsole/test/test-bug-737873-mixedcontent.html index 03e2f3140791..db83274f0968 100644 --- a/browser/devtools/webconsole/test/test-bug-737873-mixedcontent.html +++ b/browser/devtools/webconsole/test/test-bug-737873-mixedcontent.html @@ -1,5 +1,6 @@ + Mixed Content test - http on https + + + +

Web Console HTTP Logging Testpage

+

This page is used to test the HTTP logging.

+ +
+
+
+
+ + diff --git a/toolkit/devtools/webconsole/test/test_basics.html b/toolkit/devtools/webconsole/test/test_basics.html new file mode 100644 index 000000000000..26e77c07b84b --- /dev/null +++ b/toolkit/devtools/webconsole/test/test_basics.html @@ -0,0 +1,79 @@ + + + + + Basic Web Console Actor tests + + + + + +

Basic Web Console Actor tests

+ + + + diff --git a/toolkit/devtools/webconsole/test/test_cached_messages.html b/toolkit/devtools/webconsole/test/test_cached_messages.html new file mode 100644 index 000000000000..71fc02d22b1b --- /dev/null +++ b/toolkit/devtools/webconsole/test/test_cached_messages.html @@ -0,0 +1,179 @@ + + + + + Test for cached messages + + + + + +

Test for cached messages

+ + + + + + diff --git a/toolkit/devtools/webconsole/test/test_consoleapi.html b/toolkit/devtools/webconsole/test/test_consoleapi.html new file mode 100644 index 000000000000..d29984dc9200 --- /dev/null +++ b/toolkit/devtools/webconsole/test/test_consoleapi.html @@ -0,0 +1,148 @@ + + + + + Test for the Console API + + + + + +

Test for the Console API

+ + + + diff --git a/toolkit/devtools/webconsole/test/test_jsterm.html b/toolkit/devtools/webconsole/test/test_jsterm.html new file mode 100644 index 000000000000..98211ca56e79 --- /dev/null +++ b/toolkit/devtools/webconsole/test/test_jsterm.html @@ -0,0 +1,145 @@ + + + + + Test for JavaScript terminal functionality + + + + + +

Test for JavaScript terminal functionality

+ + + + diff --git a/toolkit/devtools/webconsole/test/test_network_get.html b/toolkit/devtools/webconsole/test/test_network_get.html new file mode 100644 index 000000000000..0a60ec797978 --- /dev/null +++ b/toolkit/devtools/webconsole/test/test_network_get.html @@ -0,0 +1,242 @@ + + + + + Test for the network actor (GET request) + + + + + +

Test for the network actor (GET request)

+ + + + + + diff --git a/toolkit/devtools/webconsole/test/test_network_post.html b/toolkit/devtools/webconsole/test/test_network_post.html new file mode 100644 index 000000000000..fa24f058bca6 --- /dev/null +++ b/toolkit/devtools/webconsole/test/test_network_post.html @@ -0,0 +1,266 @@ + + + + + Test for the network actor (POST request) + + + + + +

Test for the network actor (POST request)

+ + + + + + diff --git a/toolkit/devtools/webconsole/test/test_object_actor.html b/toolkit/devtools/webconsole/test/test_object_actor.html new file mode 100644 index 000000000000..4d9ea6c4ba3f --- /dev/null +++ b/toolkit/devtools/webconsole/test/test_object_actor.html @@ -0,0 +1,171 @@ + + + + + Test for the object actor + + + + + +

Test for the object actor

+ + + + diff --git a/toolkit/devtools/webconsole/test/test_page_errors.html b/toolkit/devtools/webconsole/test/test_page_errors.html new file mode 100644 index 000000000000..5e57434a38af --- /dev/null +++ b/toolkit/devtools/webconsole/test/test_page_errors.html @@ -0,0 +1,97 @@ + + + + + Test for page errors + + + + + +

Test for page errors

+ + + + From eaffb912590569b96d3088106d98db844f820e3e Mon Sep 17 00:00:00 2001 From: Panos Astithas Date: Mon, 8 Oct 2012 12:15:56 +0300 Subject: [PATCH 18/18] Disable browser_responsiveui.js and browser_responsiveuiaddcustompreset.js on OS X for intermittent failures (bug 798772, bug 798775); r=jwalker --- browser/devtools/responsivedesign/test/Makefile.in | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/browser/devtools/responsivedesign/test/Makefile.in b/browser/devtools/responsivedesign/test/Makefile.in index 5cb0ec53f25c..5157752bdf85 100644 --- a/browser/devtools/responsivedesign/test/Makefile.in +++ b/browser/devtools/responsivedesign/test/Makefile.in @@ -45,15 +45,23 @@ include $(DEPTH)/config/autoconf.mk include $(topsrcdir)/config/rules.mk _BROWSER_FILES = \ - browser_responsiveui.js \ browser_responsiveruleview.js \ browser_responsive_cmd.js \ browser_responsivecomputedview.js \ - browser_responsiveuiaddcustompreset.js \ head.js \ helpers.js \ $(NULL) +# Disabled on Mac for frequent intermittent failures +ifneq ($(OS_ARCH), Darwin) +_BROWSER_FILES += \ + browser_responsiveui.js \ + browser_responsiveuiaddcustompreset.js \ + $(NULL) +else +$(warning browser_responsiveui.js is disabled on OS X for intermittent failures. Bug 798772) \ +$(warning browser_responsiveuiaddcustompreset.js is disabled on OS X for intermittent failures. Bugs 798775, 798777) +endif libs:: $(_BROWSER_FILES) $(INSTALL) $(foreach f,$^,"$f") $(DEPTH)/_tests/testing/mochitest/browser/$(relativesrcdir)