Wed, 31 Dec 2014 06:09:35 +0100
Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.
1 /* -*- js2-basic-offset: 2; indent-tabs-mode: nil; -*- */
2 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 "use strict";
9 const {Cc, Ci, Cu} = require("chrome");
11 let WebConsoleUtils = require("devtools/toolkit/webconsole/utils").Utils;
12 let Heritage = require("sdk/core/heritage");
14 loader.lazyGetter(this, "Telemetry", () => require("devtools/shared/telemetry"));
15 loader.lazyGetter(this, "WebConsoleFrame", () => require("devtools/webconsole/webconsole").WebConsoleFrame);
16 loader.lazyImporter(this, "promise", "resource://gre/modules/Promise.jsm", "Promise");
17 loader.lazyImporter(this, "gDevTools", "resource:///modules/devtools/gDevTools.jsm");
18 loader.lazyImporter(this, "devtools", "resource://gre/modules/devtools/Loader.jsm");
19 loader.lazyImporter(this, "Services", "resource://gre/modules/Services.jsm");
20 loader.lazyImporter(this, "DebuggerServer", "resource://gre/modules/devtools/dbg-server.jsm");
21 loader.lazyImporter(this, "DebuggerClient", "resource://gre/modules/devtools/dbg-client.jsm");
23 const STRINGS_URI = "chrome://browser/locale/devtools/webconsole.properties";
24 let l10n = new WebConsoleUtils.l10n(STRINGS_URI);
26 const BROWSER_CONSOLE_WINDOW_FEATURES = "chrome,titlebar,toolbar,centerscreen,resizable,dialog=no";
28 // The preference prefix for all of the Browser Console filters.
29 const BROWSER_CONSOLE_FILTER_PREFS_PREFIX = "devtools.browserconsole.filter.";
31 let gHudId = 0;
33 ///////////////////////////////////////////////////////////////////////////
34 //// The HUD service
36 function HUD_SERVICE()
37 {
38 this.consoles = new Map();
39 this.lastFinishedRequest = { callback: null };
40 }
42 HUD_SERVICE.prototype =
43 {
44 _browserConsoleID: null,
45 _browserConsoleDefer: null,
47 /**
48 * Keeps a reference for each Web Console / Browser Console that is created.
49 * @type Map
50 */
51 consoles: null,
53 /**
54 * Assign a function to this property to listen for every request that
55 * completes. Used by unit tests. The callback takes one argument: the HTTP
56 * activity object as received from the remote Web Console.
57 *
58 * @type object
59 * Includes a property named |callback|. Assign the function to the
60 * |callback| property of this object.
61 */
62 lastFinishedRequest: null,
64 /**
65 * Firefox-specific current tab getter
66 *
67 * @returns nsIDOMWindow
68 */
69 currentContext: function HS_currentContext() {
70 return Services.wm.getMostRecentWindow("navigator:browser");
71 },
73 /**
74 * Open a Web Console for the given target.
75 *
76 * @see devtools/framework/target.js for details about targets.
77 *
78 * @param object aTarget
79 * The target that the web console will connect to.
80 * @param nsIDOMWindow aIframeWindow
81 * The window where the web console UI is already loaded.
82 * @param nsIDOMWindow aChromeWindow
83 * The window of the web console owner.
84 * @return object
85 * A promise object for the opening of the new WebConsole instance.
86 */
87 openWebConsole:
88 function HS_openWebConsole(aTarget, aIframeWindow, aChromeWindow)
89 {
90 let hud = new WebConsole(aTarget, aIframeWindow, aChromeWindow);
91 this.consoles.set(hud.hudId, hud);
92 return hud.init();
93 },
95 /**
96 * Open a Browser Console for the given target.
97 *
98 * @see devtools/framework/target.js for details about targets.
99 *
100 * @param object aTarget
101 * The target that the browser console will connect to.
102 * @param nsIDOMWindow aIframeWindow
103 * The window where the browser console UI is already loaded.
104 * @param nsIDOMWindow aChromeWindow
105 * The window of the browser console owner.
106 * @return object
107 * A promise object for the opening of the new BrowserConsole instance.
108 */
109 openBrowserConsole:
110 function HS_openBrowserConsole(aTarget, aIframeWindow, aChromeWindow)
111 {
112 let hud = new BrowserConsole(aTarget, aIframeWindow, aChromeWindow);
113 this._browserConsoleID = hud.hudId;
114 this.consoles.set(hud.hudId, hud);
115 return hud.init();
116 },
118 /**
119 * Returns the Web Console object associated to a content window.
120 *
121 * @param nsIDOMWindow aContentWindow
122 * @returns object
123 */
124 getHudByWindow: function HS_getHudByWindow(aContentWindow)
125 {
126 for (let [hudId, hud] of this.consoles) {
127 let target = hud.target;
128 if (target && target.tab && target.window === aContentWindow) {
129 return hud;
130 }
131 }
132 return null;
133 },
135 /**
136 * Returns the console instance for a given id.
137 *
138 * @param string aId
139 * @returns Object
140 */
141 getHudReferenceById: function HS_getHudReferenceById(aId)
142 {
143 return this.consoles.get(aId);
144 },
146 /**
147 * Find if there is a Web Console open for the current tab and return the
148 * instance.
149 * @return object|null
150 * The WebConsole object or null if the active tab has no open Web
151 * Console.
152 */
153 getOpenWebConsole: function HS_getOpenWebConsole()
154 {
155 let tab = this.currentContext().gBrowser.selectedTab;
156 if (!tab || !devtools.TargetFactory.isKnownTab(tab)) {
157 return null;
158 }
159 let target = devtools.TargetFactory.forTab(tab);
160 let toolbox = gDevTools.getToolbox(target);
161 let panel = toolbox ? toolbox.getPanel("webconsole") : null;
162 return panel ? panel.hud : null;
163 },
165 /**
166 * Toggle the Browser Console.
167 */
168 toggleBrowserConsole: function HS_toggleBrowserConsole()
169 {
170 if (this._browserConsoleID) {
171 let hud = this.getHudReferenceById(this._browserConsoleID);
172 return hud.destroy();
173 }
175 if (this._browserConsoleDefer) {
176 return this._browserConsoleDefer.promise;
177 }
179 this._browserConsoleDefer = promise.defer();
181 function connect()
182 {
183 let deferred = promise.defer();
185 if (!DebuggerServer.initialized) {
186 DebuggerServer.init();
187 DebuggerServer.addBrowserActors();
188 }
190 let client = new DebuggerClient(DebuggerServer.connectPipe());
191 client.connect(() =>
192 client.listTabs((aResponse) => {
193 // Add Global Process debugging...
194 let globals = JSON.parse(JSON.stringify(aResponse));
195 delete globals.tabs;
196 delete globals.selected;
197 // ...only if there are appropriate actors (a 'from' property will
198 // always be there).
199 if (Object.keys(globals).length > 1) {
200 deferred.resolve({ form: globals, client: client, chrome: true });
201 } else {
202 deferred.reject("Global console not found!");
203 }
204 }));
206 return deferred.promise;
207 }
209 let target;
210 function getTarget(aConnection)
211 {
212 let options = {
213 form: aConnection.form,
214 client: aConnection.client,
215 chrome: true,
216 };
218 return devtools.TargetFactory.forRemoteTab(options);
219 }
221 function openWindow(aTarget)
222 {
223 target = aTarget;
225 let deferred = promise.defer();
227 let win = Services.ww.openWindow(null, devtools.Tools.webConsole.url, "_blank",
228 BROWSER_CONSOLE_WINDOW_FEATURES, null);
229 win.addEventListener("DOMContentLoaded", function onLoad() {
230 win.removeEventListener("DOMContentLoaded", onLoad);
232 // Set the correct Browser Console title.
233 let root = win.document.documentElement;
234 root.setAttribute("title", root.getAttribute("browserConsoleTitle"));
236 deferred.resolve(win);
237 });
239 return deferred.promise;
240 }
242 connect().then(getTarget).then(openWindow).then((aWindow) => {
243 this.openBrowserConsole(target, aWindow, aWindow)
244 .then((aBrowserConsole) => {
245 this._browserConsoleDefer.resolve(aBrowserConsole);
246 this._browserConsoleDefer = null;
247 })
248 }, console.error);
250 return this._browserConsoleDefer.promise;
251 },
253 /**
254 * Get the Browser Console instance, if open.
255 *
256 * @return object|null
257 * A BrowserConsole instance or null if the Browser Console is not
258 * open.
259 */
260 getBrowserConsole: function HS_getBrowserConsole()
261 {
262 return this.getHudReferenceById(this._browserConsoleID);
263 },
264 };
267 /**
268 * A WebConsole instance is an interactive console initialized *per target*
269 * that displays console log data as well as provides an interactive terminal to
270 * manipulate the target's document content.
271 *
272 * This object only wraps the iframe that holds the Web Console UI. This is
273 * meant to be an integration point between the Firefox UI and the Web Console
274 * UI and features.
275 *
276 * @constructor
277 * @param object aTarget
278 * The target that the web console will connect to.
279 * @param nsIDOMWindow aIframeWindow
280 * The window where the web console UI is already loaded.
281 * @param nsIDOMWindow aChromeWindow
282 * The window of the web console owner.
283 */
284 function WebConsole(aTarget, aIframeWindow, aChromeWindow)
285 {
286 this.iframeWindow = aIframeWindow;
287 this.chromeWindow = aChromeWindow;
288 this.hudId = "hud_" + ++gHudId;
289 this.target = aTarget;
291 this.browserWindow = this.chromeWindow.top;
293 let element = this.browserWindow.document.documentElement;
294 if (element.getAttribute("windowtype") != "navigator:browser") {
295 this.browserWindow = HUDService.currentContext();
296 }
298 this.ui = new WebConsoleFrame(this);
299 }
301 WebConsole.prototype = {
302 iframeWindow: null,
303 chromeWindow: null,
304 browserWindow: null,
305 hudId: null,
306 target: null,
307 ui: null,
308 _browserConsole: false,
309 _destroyer: null,
311 /**
312 * Getter for a function to to listen for every request that completes. Used
313 * by unit tests. The callback takes one argument: the HTTP activity object as
314 * received from the remote Web Console.
315 *
316 * @type function
317 */
318 get lastFinishedRequestCallback() HUDService.lastFinishedRequest.callback,
320 /**
321 * Getter for the window that can provide various utilities that the web
322 * console makes use of, like opening links, managing popups, etc. In
323 * most cases, this will be |this.browserWindow|, but in some uses (such as
324 * the Browser Toolbox), there is no browser window, so an alternative window
325 * hosts the utilities there.
326 * @type nsIDOMWindow
327 */
328 get chromeUtilsWindow()
329 {
330 if (this.browserWindow) {
331 return this.browserWindow;
332 }
333 return this.chromeWindow.top;
334 },
336 /**
337 * Getter for the xul:popupset that holds any popups we open.
338 * @type nsIDOMElement
339 */
340 get mainPopupSet()
341 {
342 return this.chromeUtilsWindow.document.getElementById("mainPopupSet");
343 },
345 /**
346 * Getter for the output element that holds messages we display.
347 * @type nsIDOMElement
348 */
349 get outputNode()
350 {
351 return this.ui ? this.ui.outputNode : null;
352 },
354 get gViewSourceUtils()
355 {
356 return this.chromeUtilsWindow.gViewSourceUtils;
357 },
359 /**
360 * Initialize the Web Console instance.
361 *
362 * @return object
363 * A promise for the initialization.
364 */
365 init: function WC_init()
366 {
367 return this.ui.init().then(() => this);
368 },
370 /**
371 * Retrieve the Web Console panel title.
372 *
373 * @return string
374 * The Web Console panel title.
375 */
376 getPanelTitle: function WC_getPanelTitle()
377 {
378 let url = this.ui ? this.ui.contentLocation : "";
379 return l10n.getFormatStr("webConsoleWindowTitleAndURL", [url]);
380 },
382 /**
383 * The JSTerm object that manages the console's input.
384 * @see webconsole.js::JSTerm
385 * @type object
386 */
387 get jsterm()
388 {
389 return this.ui ? this.ui.jsterm : null;
390 },
392 /**
393 * The clear output button handler.
394 * @private
395 */
396 _onClearButton: function WC__onClearButton()
397 {
398 if (this.target.isLocalTab) {
399 this.browserWindow.DeveloperToolbar.resetErrorsCount(this.target.tab);
400 }
401 },
403 /**
404 * Alias for the WebConsoleFrame.setFilterState() method.
405 * @see webconsole.js::WebConsoleFrame.setFilterState()
406 */
407 setFilterState: function WC_setFilterState()
408 {
409 this.ui && this.ui.setFilterState.apply(this.ui, arguments);
410 },
412 /**
413 * Open a link in a new tab.
414 *
415 * @param string aLink
416 * The URL you want to open in a new tab.
417 */
418 openLink: function WC_openLink(aLink)
419 {
420 this.chromeUtilsWindow.openUILinkIn(aLink, "tab");
421 },
423 /**
424 * Open a link in Firefox's view source.
425 *
426 * @param string aSourceURL
427 * The URL of the file.
428 * @param integer aSourceLine
429 * The line number which should be highlighted.
430 */
431 viewSource: function WC_viewSource(aSourceURL, aSourceLine)
432 {
433 this.gViewSourceUtils.viewSource(aSourceURL, null,
434 this.iframeWindow.document, aSourceLine);
435 },
437 /**
438 * Tries to open a Stylesheet file related to the web page for the web console
439 * instance in the Style Editor. If the file is not found, it is opened in
440 * source view instead.
441 *
442 * @param string aSourceURL
443 * The URL of the file.
444 * @param integer aSourceLine
445 * The line number which you want to place the caret.
446 * TODO: This function breaks the client-server boundaries.
447 * To be fixed in bug 793259.
448 */
449 viewSourceInStyleEditor:
450 function WC_viewSourceInStyleEditor(aSourceURL, aSourceLine)
451 {
452 let toolbox = gDevTools.getToolbox(this.target);
453 if (!toolbox) {
454 this.viewSource(aSourceURL, aSourceLine);
455 return;
456 }
458 gDevTools.showToolbox(this.target, "styleeditor").then(function(toolbox) {
459 try {
460 toolbox.getCurrentPanel().selectStyleSheet(aSourceURL, aSourceLine);
461 } catch(e) {
462 // Open view source if style editor fails.
463 this.viewSource(aSourceURL, aSourceLine);
464 }
465 });
466 },
468 /**
469 * Tries to open a JavaScript file related to the web page for the web console
470 * instance in the Script Debugger. If the file is not found, it is opened in
471 * source view instead.
472 *
473 * @param string aSourceURL
474 * The URL of the file.
475 * @param integer aSourceLine
476 * The line number which you want to place the caret.
477 */
478 viewSourceInDebugger:
479 function WC_viewSourceInDebugger(aSourceURL, aSourceLine)
480 {
481 let toolbox = gDevTools.getToolbox(this.target);
482 if (!toolbox) {
483 this.viewSource(aSourceURL, aSourceLine);
484 return;
485 }
487 let showSource = ({ DebuggerView }) => {
488 if (DebuggerView.Sources.containsValue(aSourceURL)) {
489 DebuggerView.setEditorLocation(aSourceURL, aSourceLine,
490 { noDebug: true }).then(() => {
491 this.ui.emit("source-in-debugger-opened");
492 });
493 return;
494 }
495 toolbox.selectTool("webconsole");
496 this.viewSource(aSourceURL, aSourceLine);
497 }
499 // If the Debugger was already open, switch to it and try to show the
500 // source immediately. Otherwise, initialize it and wait for the sources
501 // to be added first.
502 let debuggerAlreadyOpen = toolbox.getPanel("jsdebugger");
503 toolbox.selectTool("jsdebugger").then(({ panelWin: dbg }) => {
504 if (debuggerAlreadyOpen) {
505 showSource(dbg);
506 } else {
507 dbg.once(dbg.EVENTS.SOURCES_ADDED, () => showSource(dbg));
508 }
509 });
510 },
513 /**
514 * Tries to open a JavaScript file related to the web page for the web console
515 * instance in the corresponding Scratchpad.
516 *
517 * @param string aSourceURL
518 * The URL of the file which corresponds to a Scratchpad id.
519 */
520 viewSourceInScratchpad: function WC_viewSourceInScratchpad(aSourceURL)
521 {
522 // Check for matching top level Scratchpad window.
523 let wins = Services.wm.getEnumerator("devtools:scratchpad");
525 while (wins.hasMoreElements()) {
526 let win = wins.getNext();
528 if (!win.closed && win.Scratchpad.uniqueName === aSourceURL) {
529 win.focus();
530 return;
531 }
532 }
534 // Check for matching Scratchpad toolbox tab.
535 for (let [, toolbox] of gDevTools) {
536 let scratchpadPanel = toolbox.getPanel("scratchpad");
537 if (scratchpadPanel) {
538 let { scratchpad } = scratchpadPanel;
539 if (scratchpad.uniqueName === aSourceURL) {
540 toolbox.selectTool("scratchpad");
541 toolbox.raise();
542 scratchpad.editor.focus();
543 return;
544 }
545 }
546 }
547 },
549 /**
550 * Retrieve information about the JavaScript debugger's stackframes list. This
551 * is used to allow the Web Console to evaluate code in the selected
552 * stackframe.
553 *
554 * @return object|null
555 * An object which holds:
556 * - frames: the active ThreadClient.cachedFrames array.
557 * - selected: depth/index of the selected stackframe in the debugger
558 * UI.
559 * If the debugger is not open or if it's not paused, then |null| is
560 * returned.
561 */
562 getDebuggerFrames: function WC_getDebuggerFrames()
563 {
564 let toolbox = gDevTools.getToolbox(this.target);
565 if (!toolbox) {
566 return null;
567 }
568 let panel = toolbox.getPanel("jsdebugger");
569 if (!panel) {
570 return null;
571 }
572 let framesController = panel.panelWin.DebuggerController.StackFrames;
573 let thread = framesController.activeThread;
574 if (thread && thread.paused) {
575 return {
576 frames: thread.cachedFrames,
577 selected: framesController.currentFrameDepth,
578 };
579 }
580 return null;
581 },
583 /**
584 * Destroy the object. Call this method to avoid memory leaks when the Web
585 * Console is closed.
586 *
587 * @return object
588 * A promise object that is resolved once the Web Console is closed.
589 */
590 destroy: function WC_destroy()
591 {
592 if (this._destroyer) {
593 return this._destroyer.promise;
594 }
596 HUDService.consoles.delete(this.hudId);
598 this._destroyer = promise.defer();
600 let popupset = this.mainPopupSet;
601 let panels = popupset.querySelectorAll("panel[hudId=" + this.hudId + "]");
602 for (let panel of panels) {
603 panel.hidePopup();
604 }
606 let onDestroy = function WC_onDestroyUI() {
607 try {
608 let tabWindow = this.target.isLocalTab ? this.target.window : null;
609 tabWindow && tabWindow.focus();
610 }
611 catch (ex) {
612 // Tab focus can fail if the tab or target is closed.
613 }
615 let id = WebConsoleUtils.supportsString(this.hudId);
616 Services.obs.notifyObservers(id, "web-console-destroyed", null);
617 this._destroyer.resolve(null);
618 }.bind(this);
620 if (this.ui) {
621 this.ui.destroy().then(onDestroy);
622 }
623 else {
624 onDestroy();
625 }
627 return this._destroyer.promise;
628 },
629 };
632 /**
633 * A BrowserConsole instance is an interactive console initialized *per target*
634 * that displays console log data as well as provides an interactive terminal to
635 * manipulate the target's document content.
636 *
637 * This object only wraps the iframe that holds the Browser Console UI. This is
638 * meant to be an integration point between the Firefox UI and the Browser Console
639 * UI and features.
640 *
641 * @constructor
642 * @param object aTarget
643 * The target that the browser console will connect to.
644 * @param nsIDOMWindow aIframeWindow
645 * The window where the browser console UI is already loaded.
646 * @param nsIDOMWindow aChromeWindow
647 * The window of the browser console owner.
648 */
649 function BrowserConsole()
650 {
651 WebConsole.apply(this, arguments);
652 this._telemetry = new Telemetry();
653 }
655 BrowserConsole.prototype = Heritage.extend(WebConsole.prototype,
656 {
657 _browserConsole: true,
658 _bc_init: null,
659 _bc_destroyer: null,
661 $init: WebConsole.prototype.init,
663 /**
664 * Initialize the Browser Console instance.
665 *
666 * @return object
667 * A promise for the initialization.
668 */
669 init: function BC_init()
670 {
671 if (this._bc_init) {
672 return this._bc_init;
673 }
675 this.ui._filterPrefsPrefix = BROWSER_CONSOLE_FILTER_PREFS_PREFIX;
677 let window = this.iframeWindow;
679 // Make sure that the closing of the Browser Console window destroys this
680 // instance.
681 let onClose = () => {
682 window.removeEventListener("unload", onClose);
683 this.destroy();
684 };
685 window.addEventListener("unload", onClose);
687 // Make sure Ctrl-W closes the Browser Console window.
688 window.document.getElementById("cmd_close").removeAttribute("disabled");
690 this._telemetry.toolOpened("browserconsole");
692 this._bc_init = this.$init();
693 return this._bc_init;
694 },
696 $destroy: WebConsole.prototype.destroy,
698 /**
699 * Destroy the object.
700 *
701 * @return object
702 * A promise object that is resolved once the Browser Console is closed.
703 */
704 destroy: function BC_destroy()
705 {
706 if (this._bc_destroyer) {
707 return this._bc_destroyer.promise;
708 }
710 this._telemetry.toolClosed("browserconsole");
712 this._bc_destroyer = promise.defer();
714 let chromeWindow = this.chromeWindow;
715 this.$destroy().then(() =>
716 this.target.client.close(() => {
717 HUDService._browserConsoleID = null;
718 chromeWindow.close();
719 this._bc_destroyer.resolve(null);
720 }));
722 return this._bc_destroyer.promise;
723 },
724 });
726 const HUDService = new HUD_SERVICE();
728 (() => {
729 let methods = ["openWebConsole", "openBrowserConsole",
730 "toggleBrowserConsole", "getOpenWebConsole",
731 "getBrowserConsole", "getHudByWindow", "getHudReferenceById"];
732 for (let method of methods) {
733 exports[method] = HUDService[method].bind(HUDService);
734 }
736 exports.consoles = HUDService.consoles;
737 exports.lastFinishedRequest = HUDService.lastFinishedRequest;
738 })();