webconsole-utils.js 30 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063
  1. /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
  2. /* This Source Code Form is subject to the terms of the Mozilla Public
  3. * License, v. 2.0. If a copy of the MPL was not distributed with this
  4. * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
  5. "use strict";
  6. const {Cc, Ci, Cu, components} = require("chrome");
  7. const {isWindowIncluded} = require("devtools/shared/layout/utils");
  8. const Services = require("Services");
  9. const {XPCOMUtils} = require("resource://gre/modules/XPCOMUtils.jsm");
  10. // TODO: Bug 842672 - browser/ imports modules from toolkit/.
  11. // Note that these are only used in WebConsoleCommands, see $0 and pprint().
  12. loader.lazyImporter(this, "VariablesView", "resource://devtools/client/shared/widgets/VariablesView.jsm");
  13. XPCOMUtils.defineLazyServiceGetter(this,
  14. "swm",
  15. "@mozilla.org/serviceworkers/manager;1",
  16. "nsIServiceWorkerManager");
  17. const CONSOLE_WORKER_IDS = exports.CONSOLE_WORKER_IDS = [
  18. "SharedWorker",
  19. "ServiceWorker",
  20. "Worker"
  21. ];
  22. var WebConsoleUtils = {
  23. /**
  24. * Given a message, return one of CONSOLE_WORKER_IDS if it matches
  25. * one of those.
  26. *
  27. * @return string
  28. */
  29. getWorkerType: function (message) {
  30. let id = message ? message.innerID : null;
  31. return CONSOLE_WORKER_IDS[CONSOLE_WORKER_IDS.indexOf(id)] || null;
  32. },
  33. /**
  34. * Clone an object.
  35. *
  36. * @param object object
  37. * The object you want cloned.
  38. * @param boolean recursive
  39. * Tells if you want to dig deeper into the object, to clone
  40. * recursively.
  41. * @param function [filter]
  42. * Optional, filter function, called for every property. Three
  43. * arguments are passed: key, value and object. Return true if the
  44. * property should be added to the cloned object. Return false to skip
  45. * the property.
  46. * @return object
  47. * The cloned object.
  48. */
  49. cloneObject: function (object, recursive, filter) {
  50. if (typeof object != "object") {
  51. return object;
  52. }
  53. let temp;
  54. if (Array.isArray(object)) {
  55. temp = [];
  56. Array.forEach(object, function (value, index) {
  57. if (!filter || filter(index, value, object)) {
  58. temp.push(recursive ? WebConsoleUtils.cloneObject(value) : value);
  59. }
  60. });
  61. } else {
  62. temp = {};
  63. for (let key in object) {
  64. let value = object[key];
  65. if (object.hasOwnProperty(key) &&
  66. (!filter || filter(key, value, object))) {
  67. temp[key] = recursive ? WebConsoleUtils.cloneObject(value) : value;
  68. }
  69. }
  70. }
  71. return temp;
  72. },
  73. /**
  74. * Gets the ID of the inner window of this DOM window.
  75. *
  76. * @param nsIDOMWindow window
  77. * @return integer
  78. * Inner ID for the given window.
  79. */
  80. getInnerWindowId: function (window) {
  81. return window.QueryInterface(Ci.nsIInterfaceRequestor)
  82. .getInterface(Ci.nsIDOMWindowUtils).currentInnerWindowID;
  83. },
  84. /**
  85. * Recursively gather a list of inner window ids given a
  86. * top level window.
  87. *
  88. * @param nsIDOMWindow window
  89. * @return Array
  90. * list of inner window ids.
  91. */
  92. getInnerWindowIDsForFrames: function (window) {
  93. let innerWindowID = this.getInnerWindowId(window);
  94. let ids = [innerWindowID];
  95. if (window.frames) {
  96. for (let i = 0; i < window.frames.length; i++) {
  97. let frame = window.frames[i];
  98. ids = ids.concat(this.getInnerWindowIDsForFrames(frame));
  99. }
  100. }
  101. return ids;
  102. },
  103. /**
  104. * Get the property descriptor for the given object.
  105. *
  106. * @param object object
  107. * The object that contains the property.
  108. * @param string prop
  109. * The property you want to get the descriptor for.
  110. * @return object
  111. * Property descriptor.
  112. */
  113. getPropertyDescriptor: function (object, prop) {
  114. let desc = null;
  115. while (object) {
  116. try {
  117. if ((desc = Object.getOwnPropertyDescriptor(object, prop))) {
  118. break;
  119. }
  120. } catch (ex) {
  121. // Native getters throw here. See bug 520882.
  122. // null throws TypeError.
  123. if (ex.name != "NS_ERROR_XPC_BAD_CONVERT_JS" &&
  124. ex.name != "NS_ERROR_XPC_BAD_OP_ON_WN_PROTO" &&
  125. ex.name != "TypeError") {
  126. throw ex;
  127. }
  128. }
  129. try {
  130. object = Object.getPrototypeOf(object);
  131. } catch (ex) {
  132. if (ex.name == "TypeError") {
  133. return desc;
  134. }
  135. throw ex;
  136. }
  137. }
  138. return desc;
  139. },
  140. /**
  141. * Create a grip for the given value. If the value is an object,
  142. * an object wrapper will be created.
  143. *
  144. * @param mixed value
  145. * The value you want to create a grip for, before sending it to the
  146. * client.
  147. * @param function objectWrapper
  148. * If the value is an object then the objectWrapper function is
  149. * invoked to give us an object grip. See this.getObjectGrip().
  150. * @return mixed
  151. * The value grip.
  152. */
  153. createValueGrip: function (value, objectWrapper) {
  154. switch (typeof value) {
  155. case "boolean":
  156. return value;
  157. case "string":
  158. return objectWrapper(value);
  159. case "number":
  160. if (value === Infinity) {
  161. return { type: "Infinity" };
  162. } else if (value === -Infinity) {
  163. return { type: "-Infinity" };
  164. } else if (Number.isNaN(value)) {
  165. return { type: "NaN" };
  166. } else if (!value && 1 / value === -Infinity) {
  167. return { type: "-0" };
  168. }
  169. return value;
  170. case "undefined":
  171. return { type: "undefined" };
  172. case "object":
  173. if (value === null) {
  174. return { type: "null" };
  175. }
  176. // Fall through.
  177. case "function":
  178. return objectWrapper(value);
  179. default:
  180. console.error("Failed to provide a grip for value of " + typeof value
  181. + ": " + value);
  182. return null;
  183. }
  184. },
  185. };
  186. exports.Utils = WebConsoleUtils;
  187. // The page errors listener
  188. /**
  189. * The nsIConsoleService listener. This is used to send all of the console
  190. * messages (JavaScript, CSS and more) to the remote Web Console instance.
  191. *
  192. * @constructor
  193. * @param nsIDOMWindow [window]
  194. * Optional - the window object for which we are created. This is used
  195. * for filtering out messages that belong to other windows.
  196. * @param object listener
  197. * The listener object must have one method:
  198. * - onConsoleServiceMessage(). This method is invoked with one argument,
  199. * the nsIConsoleMessage, whenever a relevant message is received.
  200. */
  201. function ConsoleServiceListener(window, listener) {
  202. this.window = window;
  203. this.listener = listener;
  204. }
  205. exports.ConsoleServiceListener = ConsoleServiceListener;
  206. ConsoleServiceListener.prototype =
  207. {
  208. QueryInterface: XPCOMUtils.generateQI([Ci.nsIConsoleListener]),
  209. /**
  210. * The content window for which we listen to page errors.
  211. * @type nsIDOMWindow
  212. */
  213. window: null,
  214. /**
  215. * The listener object which is notified of messages from the console service.
  216. * @type object
  217. */
  218. listener: null,
  219. /**
  220. * Initialize the nsIConsoleService listener.
  221. */
  222. init: function () {
  223. Services.console.registerListener(this);
  224. },
  225. /**
  226. * The nsIConsoleService observer. This method takes all the script error
  227. * messages belonging to the current window and sends them to the remote Web
  228. * Console instance.
  229. *
  230. * @param nsIConsoleMessage message
  231. * The message object coming from the nsIConsoleService.
  232. */
  233. observe: function (message) {
  234. if (!this.listener) {
  235. return;
  236. }
  237. if (this.window) {
  238. if (!(message instanceof Ci.nsIScriptError) ||
  239. !message.outerWindowID ||
  240. !this.isCategoryAllowed(message.category)) {
  241. return;
  242. }
  243. let errorWindow = Services.wm.getOuterWindowWithId(message.outerWindowID);
  244. if (!errorWindow || !isWindowIncluded(this.window, errorWindow)) {
  245. return;
  246. }
  247. }
  248. this.listener.onConsoleServiceMessage(message);
  249. },
  250. /**
  251. * Check if the given message category is allowed to be tracked or not.
  252. * We ignore chrome-originating errors as we only care about content.
  253. *
  254. * @param string category
  255. * The message category you want to check.
  256. * @return boolean
  257. * True if the category is allowed to be logged, false otherwise.
  258. */
  259. isCategoryAllowed: function (category) {
  260. if (!category) {
  261. return false;
  262. }
  263. switch (category) {
  264. case "XPConnect JavaScript":
  265. case "component javascript":
  266. case "chrome javascript":
  267. case "chrome registration":
  268. case "XBL":
  269. case "XBL Prototype Handler":
  270. case "XBL Content Sink":
  271. case "xbl javascript":
  272. return false;
  273. }
  274. return true;
  275. },
  276. /**
  277. * Get the cached page errors for the current inner window and its (i)frames.
  278. *
  279. * @param boolean [includePrivate=false]
  280. * Tells if you want to also retrieve messages coming from private
  281. * windows. Defaults to false.
  282. * @return array
  283. * The array of cached messages. Each element is an nsIScriptError or
  284. * an nsIConsoleMessage
  285. */
  286. getCachedMessages: function (includePrivate = false) {
  287. let errors = Services.console.getMessageArray() || [];
  288. // if !this.window, we're in a browser console. Still need to filter
  289. // private messages.
  290. if (!this.window) {
  291. return errors.filter((error) => {
  292. if (error instanceof Ci.nsIScriptError) {
  293. if (!includePrivate && error.isFromPrivateWindow) {
  294. return false;
  295. }
  296. }
  297. return true;
  298. });
  299. }
  300. let ids = WebConsoleUtils.getInnerWindowIDsForFrames(this.window);
  301. return errors.filter((error) => {
  302. if (error instanceof Ci.nsIScriptError) {
  303. if (!includePrivate && error.isFromPrivateWindow) {
  304. return false;
  305. }
  306. if (ids &&
  307. (ids.indexOf(error.innerWindowID) == -1 ||
  308. !this.isCategoryAllowed(error.category))) {
  309. return false;
  310. }
  311. } else if (ids && ids[0]) {
  312. // If this is not an nsIScriptError and we need to do window-based
  313. // filtering we skip this message.
  314. return false;
  315. }
  316. return true;
  317. });
  318. },
  319. /**
  320. * Remove the nsIConsoleService listener.
  321. */
  322. destroy: function () {
  323. Services.console.unregisterListener(this);
  324. this.listener = this.window = null;
  325. },
  326. };
  327. // The window.console API observer
  328. /**
  329. * The window.console API observer. This allows the window.console API messages
  330. * to be sent to the remote Web Console instance.
  331. *
  332. * @constructor
  333. * @param nsIDOMWindow window
  334. * Optional - the window object for which we are created. This is used
  335. * for filtering out messages that belong to other windows.
  336. * @param object owner
  337. * The owner object must have the following methods:
  338. * - onConsoleAPICall(). This method is invoked with one argument, the
  339. * Console API message that comes from the observer service, whenever
  340. * a relevant console API call is received.
  341. * @param object filteringOptions
  342. * Optional - The filteringOptions that this listener should listen to:
  343. * - addonId: filter console messages based on the addonId.
  344. */
  345. function ConsoleAPIListener(window, owner, {addonId} = {}) {
  346. this.window = window;
  347. this.owner = owner;
  348. this.addonId = addonId;
  349. }
  350. exports.ConsoleAPIListener = ConsoleAPIListener;
  351. ConsoleAPIListener.prototype =
  352. {
  353. QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]),
  354. /**
  355. * The content window for which we listen to window.console API calls.
  356. * @type nsIDOMWindow
  357. */
  358. window: null,
  359. /**
  360. * The owner object which is notified of window.console API calls. It must
  361. * have a onConsoleAPICall method which is invoked with one argument: the
  362. * console API call object that comes from the observer service.
  363. *
  364. * @type object
  365. * @see WebConsoleActor
  366. */
  367. owner: null,
  368. /**
  369. * The addonId that we listen for. If not null then only messages from this
  370. * console will be returned.
  371. */
  372. addonId: null,
  373. /**
  374. * Initialize the window.console API observer.
  375. */
  376. init: function () {
  377. // Note that the observer is process-wide. We will filter the messages as
  378. // needed, see CAL_observe().
  379. Services.obs.addObserver(this, "console-api-log-event", false);
  380. },
  381. /**
  382. * The console API message observer. When messages are received from the
  383. * observer service we forward them to the remote Web Console instance.
  384. *
  385. * @param object message
  386. * The message object receives from the observer service.
  387. * @param string topic
  388. * The message topic received from the observer service.
  389. */
  390. observe: function (message, topic) {
  391. if (!this.owner) {
  392. return;
  393. }
  394. // Here, wrappedJSObject is not a security wrapper but a property defined
  395. // by the XPCOM component which allows us to unwrap the XPCOM interface and
  396. // access the underlying JSObject.
  397. let apiMessage = message.wrappedJSObject;
  398. if (!this.isMessageRelevant(apiMessage)) {
  399. return;
  400. }
  401. this.owner.onConsoleAPICall(apiMessage);
  402. },
  403. /**
  404. * Given a message, return true if this window should show it and false
  405. * if it should be ignored.
  406. *
  407. * @param message
  408. * The message from the Storage Service
  409. * @return bool
  410. * Do we care about this message?
  411. */
  412. isMessageRelevant: function (message) {
  413. let workerType = WebConsoleUtils.getWorkerType(message);
  414. if (this.window && workerType === "ServiceWorker") {
  415. // For messages from Service Workers, message.ID is the
  416. // scope, which can be used to determine whether it's controlling
  417. // a window.
  418. let scope = message.ID;
  419. if (!swm.shouldReportToWindow(this.window, scope)) {
  420. return false;
  421. }
  422. }
  423. if (this.window && !workerType) {
  424. let msgWindow = Services.wm.getCurrentInnerWindowWithId(message.innerID);
  425. if (!msgWindow || !isWindowIncluded(this.window, msgWindow)) {
  426. // Not the same window!
  427. return false;
  428. }
  429. }
  430. if (this.addonId) {
  431. // ConsoleAPI.jsm messages contains a consoleID, (and it is currently
  432. // used in Addon SDK add-ons), the standard 'console' object
  433. // (which is used in regular webpages and in WebExtensions pages)
  434. // contains the originAttributes of the source document principal.
  435. // Filtering based on the originAttributes used by
  436. // the Console API object.
  437. if (message.originAttributes &&
  438. message.originAttributes.addonId == this.addonId) {
  439. return true;
  440. }
  441. // Filtering based on the old-style consoleID property used by
  442. // the legacy Console JSM module.
  443. if (message.consoleID && message.consoleID == `addon/${this.addonId}`) {
  444. return true;
  445. }
  446. return false;
  447. }
  448. return true;
  449. },
  450. /**
  451. * Get the cached messages for the current inner window and its (i)frames.
  452. *
  453. * @param boolean [includePrivate=false]
  454. * Tells if you want to also retrieve messages coming from private
  455. * windows. Defaults to false.
  456. * @return array
  457. * The array of cached messages.
  458. */
  459. getCachedMessages: function (includePrivate = false) {
  460. let messages = [];
  461. let ConsoleAPIStorage = Cc["@mozilla.org/consoleAPI-storage;1"]
  462. .getService(Ci.nsIConsoleAPIStorage);
  463. // if !this.window, we're in a browser console. Retrieve all events
  464. // for filtering based on privacy.
  465. if (!this.window) {
  466. messages = ConsoleAPIStorage.getEvents();
  467. } else {
  468. let ids = WebConsoleUtils.getInnerWindowIDsForFrames(this.window);
  469. ids.forEach((id) => {
  470. messages = messages.concat(ConsoleAPIStorage.getEvents(id));
  471. });
  472. }
  473. CONSOLE_WORKER_IDS.forEach((id) => {
  474. messages = messages.concat(ConsoleAPIStorage.getEvents(id));
  475. });
  476. messages = messages.filter(msg => {
  477. return this.isMessageRelevant(msg);
  478. });
  479. if (includePrivate) {
  480. return messages;
  481. }
  482. return messages.filter((m) => !m.private);
  483. },
  484. /**
  485. * Destroy the console API listener.
  486. */
  487. destroy: function () {
  488. Services.obs.removeObserver(this, "console-api-log-event");
  489. this.window = this.owner = null;
  490. },
  491. };
  492. /**
  493. * WebConsole commands manager.
  494. *
  495. * Defines a set of functions /variables ("commands") that are available from
  496. * the Web Console but not from the web page.
  497. *
  498. */
  499. var WebConsoleCommands = {
  500. _registeredCommands: new Map(),
  501. _originalCommands: new Map(),
  502. /**
  503. * @private
  504. * Reserved for built-in commands. To register a command from the code of an
  505. * add-on, see WebConsoleCommands.register instead.
  506. *
  507. * @see WebConsoleCommands.register
  508. */
  509. _registerOriginal: function (name, command) {
  510. this.register(name, command);
  511. this._originalCommands.set(name, this.getCommand(name));
  512. },
  513. /**
  514. * Register a new command.
  515. * @param {string} name The command name (exemple: "$")
  516. * @param {(function|object)} command The command to register.
  517. * It can be a function so the command is a function (like "$()"),
  518. * or it can also be a property descriptor to describe a getter / value (like
  519. * "$0").
  520. *
  521. * The command function or the command getter are passed a owner object as
  522. * their first parameter (see the example below).
  523. *
  524. * Note that setters don't work currently and "enumerable" and "configurable"
  525. * are forced to true.
  526. *
  527. * @example
  528. *
  529. * WebConsoleCommands.register("$", function JSTH_$(owner, selector)
  530. * {
  531. * return owner.window.document.querySelector(selector);
  532. * });
  533. *
  534. * WebConsoleCommands.register("$0", {
  535. * get: function(owner) {
  536. * return owner.makeDebuggeeValue(owner.selectedNode);
  537. * }
  538. * });
  539. */
  540. register: function (name, command) {
  541. this._registeredCommands.set(name, command);
  542. },
  543. /**
  544. * Unregister a command.
  545. *
  546. * If the command being unregister overrode a built-in command,
  547. * the latter is restored.
  548. *
  549. * @param {string} name The name of the command
  550. */
  551. unregister: function (name) {
  552. this._registeredCommands.delete(name);
  553. if (this._originalCommands.has(name)) {
  554. this.register(name, this._originalCommands.get(name));
  555. }
  556. },
  557. /**
  558. * Returns a command by its name.
  559. *
  560. * @param {string} name The name of the command.
  561. *
  562. * @return {(function|object)} The command.
  563. */
  564. getCommand: function (name) {
  565. return this._registeredCommands.get(name);
  566. },
  567. /**
  568. * Returns true if a command is registered with the given name.
  569. *
  570. * @param {string} name The name of the command.
  571. *
  572. * @return {boolean} True if the command is registered.
  573. */
  574. hasCommand: function (name) {
  575. return this._registeredCommands.has(name);
  576. },
  577. };
  578. exports.WebConsoleCommands = WebConsoleCommands;
  579. /*
  580. * Built-in commands.
  581. *
  582. * A list of helper functions used by Firebug can be found here:
  583. * http://getfirebug.com/wiki/index.php/Command_Line_API
  584. */
  585. /**
  586. * Find a node by ID.
  587. *
  588. * @param string id
  589. * The ID of the element you want.
  590. * @return nsIDOMNode or null
  591. * The result of calling document.querySelector(selector).
  592. */
  593. WebConsoleCommands._registerOriginal("$", function (owner, selector) {
  594. return owner.window.document.querySelector(selector);
  595. });
  596. /**
  597. * Find the nodes matching a CSS selector.
  598. *
  599. * @param string selector
  600. * A string that is passed to window.document.querySelectorAll.
  601. * @return nsIDOMNodeList
  602. * Returns the result of document.querySelectorAll(selector).
  603. */
  604. WebConsoleCommands._registerOriginal("$$", function (owner, selector) {
  605. let nodes = owner.window.document.querySelectorAll(selector);
  606. // Calling owner.window.Array.from() doesn't work without accessing the
  607. // wrappedJSObject, so just loop through the results instead.
  608. let result = new owner.window.Array();
  609. for (let i = 0; i < nodes.length; i++) {
  610. result.push(nodes[i]);
  611. }
  612. return result;
  613. });
  614. /**
  615. * Returns the result of the last console input evaluation
  616. *
  617. * @return object|undefined
  618. * Returns last console evaluation or undefined
  619. */
  620. WebConsoleCommands._registerOriginal("$_", {
  621. get: function (owner) {
  622. return owner.consoleActor.getLastConsoleInputEvaluation();
  623. }
  624. });
  625. /**
  626. * Runs an xPath query and returns all matched nodes.
  627. *
  628. * @param string xPath
  629. * xPath search query to execute.
  630. * @param [optional] nsIDOMNode context
  631. * Context to run the xPath query on. Uses window.document if not set.
  632. * @return array of nsIDOMNode
  633. */
  634. WebConsoleCommands._registerOriginal("$x", function (owner, xPath, context) {
  635. let nodes = new owner.window.Array();
  636. // Not waiving Xrays, since we want the original Document.evaluate function,
  637. // instead of anything that's been redefined.
  638. let doc = owner.window.document;
  639. context = context || doc;
  640. let results = doc.evaluate(xPath, context, null,
  641. Ci.nsIDOMXPathResult.ANY_TYPE, null);
  642. let node;
  643. while ((node = results.iterateNext())) {
  644. nodes.push(node);
  645. }
  646. return nodes;
  647. });
  648. /**
  649. * Returns the currently selected object in the highlighter.
  650. *
  651. * @return Object representing the current selection in the
  652. * Inspector, or null if no selection exists.
  653. */
  654. WebConsoleCommands._registerOriginal("$0", {
  655. get: function (owner) {
  656. return owner.makeDebuggeeValue(owner.selectedNode);
  657. }
  658. });
  659. /**
  660. * Clears the output of the WebConsole.
  661. */
  662. WebConsoleCommands._registerOriginal("clear", function (owner) {
  663. owner.helperResult = {
  664. type: "clearOutput",
  665. };
  666. });
  667. /**
  668. * Clears the input history of the WebConsole.
  669. */
  670. WebConsoleCommands._registerOriginal("clearHistory", function (owner) {
  671. owner.helperResult = {
  672. type: "clearHistory",
  673. };
  674. });
  675. /**
  676. * Returns the result of Object.keys(object).
  677. *
  678. * @param object object
  679. * Object to return the property names from.
  680. * @return array of strings
  681. */
  682. WebConsoleCommands._registerOriginal("keys", function (owner, object) {
  683. // Need to waive Xrays so we can iterate functions and accessor properties
  684. return Cu.cloneInto(Object.keys(Cu.waiveXrays(object)), owner.window);
  685. });
  686. /**
  687. * Returns the values of all properties on object.
  688. *
  689. * @param object object
  690. * Object to display the values from.
  691. * @return array of string
  692. */
  693. WebConsoleCommands._registerOriginal("values", function (owner, object) {
  694. let values = [];
  695. // Need to waive Xrays so we can iterate functions and accessor properties
  696. let waived = Cu.waiveXrays(object);
  697. let names = Object.getOwnPropertyNames(waived);
  698. for (let name of names) {
  699. values.push(waived[name]);
  700. }
  701. return Cu.cloneInto(values, owner.window);
  702. });
  703. /**
  704. * Opens a help window in MDN.
  705. */
  706. WebConsoleCommands._registerOriginal("help", function (owner) {
  707. owner.helperResult = { type: "help" };
  708. });
  709. /**
  710. * Change the JS evaluation scope.
  711. *
  712. * @param DOMElement|string|window window
  713. * The window object to use for eval scope. This can be a string that
  714. * is used to perform document.querySelector(), to find the iframe that
  715. * you want to cd() to. A DOMElement can be given as well, the
  716. * .contentWindow property is used. Lastly, you can directly pass
  717. * a window object. If you call cd() with no arguments, the current
  718. * eval scope is cleared back to its default (the top window).
  719. */
  720. WebConsoleCommands._registerOriginal("cd", function (owner, window) {
  721. if (!window) {
  722. owner.consoleActor.evalWindow = null;
  723. owner.helperResult = { type: "cd" };
  724. return;
  725. }
  726. if (typeof window == "string") {
  727. window = owner.window.document.querySelector(window);
  728. }
  729. if (window instanceof Ci.nsIDOMElement && window.contentWindow) {
  730. window = window.contentWindow;
  731. }
  732. if (!(window instanceof Ci.nsIDOMWindow)) {
  733. owner.helperResult = {
  734. type: "error",
  735. message: "cdFunctionInvalidArgument"
  736. };
  737. return;
  738. }
  739. owner.consoleActor.evalWindow = window;
  740. owner.helperResult = { type: "cd" };
  741. });
  742. /**
  743. * Inspects the passed object. This is done by opening the PropertyPanel.
  744. *
  745. * @param object object
  746. * Object to inspect.
  747. */
  748. WebConsoleCommands._registerOriginal("inspect", function (owner, object) {
  749. let dbgObj = owner.makeDebuggeeValue(object);
  750. let grip = owner.createValueGrip(dbgObj);
  751. owner.helperResult = {
  752. type: "inspectObject",
  753. input: owner.evalInput,
  754. object: grip,
  755. };
  756. });
  757. /**
  758. * Prints object to the output.
  759. *
  760. * @param object object
  761. * Object to print to the output.
  762. * @return string
  763. */
  764. WebConsoleCommands._registerOriginal("pprint", function (owner, object) {
  765. if (object === null || object === undefined || object === true ||
  766. object === false) {
  767. owner.helperResult = {
  768. type: "error",
  769. message: "helperFuncUnsupportedTypeError",
  770. };
  771. return null;
  772. }
  773. owner.helperResult = { rawOutput: true };
  774. if (typeof object == "function") {
  775. return object + "\n";
  776. }
  777. let output = [];
  778. let obj = object;
  779. for (let name in obj) {
  780. let desc = WebConsoleUtils.getPropertyDescriptor(obj, name) || {};
  781. if (desc.get || desc.set) {
  782. // TODO: Bug 842672 - toolkit/ imports modules from browser/.
  783. let getGrip = VariablesView.getGrip(desc.get);
  784. let setGrip = VariablesView.getGrip(desc.set);
  785. let getString = VariablesView.getString(getGrip);
  786. let setString = VariablesView.getString(setGrip);
  787. output.push(name + ":", " get: " + getString, " set: " + setString);
  788. } else {
  789. let valueGrip = VariablesView.getGrip(obj[name]);
  790. let valueString = VariablesView.getString(valueGrip);
  791. output.push(name + ": " + valueString);
  792. }
  793. }
  794. return " " + output.join("\n ");
  795. });
  796. /**
  797. * Print the String representation of a value to the output, as-is.
  798. *
  799. * @param any value
  800. * A value you want to output as a string.
  801. * @return void
  802. */
  803. WebConsoleCommands._registerOriginal("print", function (owner, value) {
  804. owner.helperResult = { rawOutput: true };
  805. if (typeof value === "symbol") {
  806. return Symbol.prototype.toString.call(value);
  807. }
  808. // Waiving Xrays here allows us to see a closer representation of the
  809. // underlying object. This may execute arbitrary content code, but that
  810. // code will run with content privileges, and the result will be rendered
  811. // inert by coercing it to a String.
  812. return String(Cu.waiveXrays(value));
  813. });
  814. /**
  815. * Copy the String representation of a value to the clipboard.
  816. *
  817. * @param any value
  818. * A value you want to copy as a string.
  819. * @return void
  820. */
  821. WebConsoleCommands._registerOriginal("copy", function (owner, value) {
  822. let payload;
  823. try {
  824. if (value instanceof Ci.nsIDOMElement) {
  825. payload = value.outerHTML;
  826. } else if (typeof value == "string") {
  827. payload = value;
  828. } else {
  829. payload = JSON.stringify(value, null, " ");
  830. }
  831. } catch (ex) {
  832. payload = "/* " + ex + " */";
  833. }
  834. owner.helperResult = {
  835. type: "copyValueToClipboard",
  836. value: payload,
  837. };
  838. });
  839. /**
  840. * (Internal only) Add the bindings to |owner.sandbox|.
  841. * This is intended to be used by the WebConsole actor only.
  842. *
  843. * @param object owner
  844. * The owning object.
  845. */
  846. function addWebConsoleCommands(owner) {
  847. if (!owner) {
  848. throw new Error("The owner is required");
  849. }
  850. for (let [name, command] of WebConsoleCommands._registeredCommands) {
  851. if (typeof command === "function") {
  852. owner.sandbox[name] = command.bind(undefined, owner);
  853. } else if (typeof command === "object") {
  854. let clone = Object.assign({}, command, {
  855. // We force the enumerability and the configurability (so the
  856. // WebConsoleActor can reconfigure the property).
  857. enumerable: true,
  858. configurable: true
  859. });
  860. if (typeof command.get === "function") {
  861. clone.get = command.get.bind(undefined, owner);
  862. }
  863. if (typeof command.set === "function") {
  864. clone.set = command.set.bind(undefined, owner);
  865. }
  866. Object.defineProperty(owner.sandbox, name, clone);
  867. }
  868. }
  869. }
  870. exports.addWebConsoleCommands = addWebConsoleCommands;
  871. /**
  872. * A ReflowObserver that listens for reflow events from the page.
  873. * Implements nsIReflowObserver.
  874. *
  875. * @constructor
  876. * @param object window
  877. * The window for which we need to track reflow.
  878. * @param object owner
  879. * The listener owner which needs to implement:
  880. * - onReflowActivity(reflowInfo)
  881. */
  882. function ConsoleReflowListener(window, listener) {
  883. this.docshell = window.QueryInterface(Ci.nsIInterfaceRequestor)
  884. .getInterface(Ci.nsIWebNavigation)
  885. .QueryInterface(Ci.nsIDocShell);
  886. this.listener = listener;
  887. this.docshell.addWeakReflowObserver(this);
  888. }
  889. exports.ConsoleReflowListener = ConsoleReflowListener;
  890. ConsoleReflowListener.prototype =
  891. {
  892. QueryInterface: XPCOMUtils.generateQI([Ci.nsIReflowObserver,
  893. Ci.nsISupportsWeakReference]),
  894. docshell: null,
  895. listener: null,
  896. /**
  897. * Forward reflow event to listener.
  898. *
  899. * @param DOMHighResTimeStamp start
  900. * @param DOMHighResTimeStamp end
  901. * @param boolean interruptible
  902. */
  903. sendReflow: function (start, end, interruptible) {
  904. let frame = components.stack.caller.caller;
  905. let filename = frame ? frame.filename : null;
  906. if (filename) {
  907. // Because filename could be of the form "xxx.js -> xxx.js -> xxx.js",
  908. // we only take the last part.
  909. filename = filename.split(" ").pop();
  910. }
  911. this.listener.onReflowActivity({
  912. interruptible: interruptible,
  913. start: start,
  914. end: end,
  915. sourceURL: filename,
  916. sourceLine: frame ? frame.lineNumber : null,
  917. functionName: frame ? frame.name : null
  918. });
  919. },
  920. /**
  921. * On uninterruptible reflow
  922. *
  923. * @param DOMHighResTimeStamp start
  924. * @param DOMHighResTimeStamp end
  925. */
  926. reflow: function (start, end) {
  927. this.sendReflow(start, end, false);
  928. },
  929. /**
  930. * On interruptible reflow
  931. *
  932. * @param DOMHighResTimeStamp start
  933. * @param DOMHighResTimeStamp end
  934. */
  935. reflowInterruptible: function (start, end) {
  936. this.sendReflow(start, end, true);
  937. },
  938. /**
  939. * Unregister listener.
  940. */
  941. destroy: function () {
  942. this.docshell.removeWeakReflowObserver(this);
  943. this.listener = this.docshell = null;
  944. },
  945. };