webconsole.js 113 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069307030713072307330743075307630773078307930803081308230833084308530863087308830893090309130923093309430953096309730983099310031013102310331043105310631073108310931103111311231133114311531163117311831193120312131223123312431253126312731283129313031313132313331343135313631373138313931403141314231433144314531463147314831493150315131523153315431553156315731583159316031613162316331643165316631673168316931703171317231733174317531763177317831793180318131823183318431853186318731883189319031913192319331943195319631973198319932003201320232033204320532063207320832093210321132123213321432153216321732183219322032213222322332243225322632273228322932303231323232333234323532363237323832393240324132423243324432453246324732483249325032513252325332543255325632573258325932603261326232633264326532663267326832693270327132723273327432753276327732783279328032813282328332843285328632873288328932903291329232933294329532963297329832993300330133023303330433053306330733083309331033113312331333143315331633173318331933203321332233233324332533263327332833293330333133323333333433353336333733383339334033413342334333443345334633473348334933503351335233533354335533563357335833593360336133623363336433653366336733683369337033713372337333743375337633773378337933803381338233833384338533863387338833893390339133923393339433953396339733983399340034013402340334043405340634073408340934103411341234133414341534163417341834193420342134223423342434253426342734283429343034313432343334343435343634373438343934403441344234433444344534463447344834493450345134523453345434553456345734583459346034613462346334643465346634673468346934703471347234733474347534763477347834793480348134823483348434853486348734883489349034913492349334943495349634973498349935003501350235033504350535063507350835093510351135123513351435153516351735183519352035213522352335243525352635273528352935303531353235333534353535363537353835393540354135423543354435453546354735483549355035513552355335543555355635573558355935603561356235633564356535663567356835693570357135723573357435753576357735783579358035813582358335843585358635873588358935903591359235933594359535963597359835993600360136023603360436053606360736083609361036113612361336143615361636173618361936203621362236233624362536263627362836293630363136323633363436353636363736383639364036413642364336443645364636473648364936503651365236533654365536563657365836593660366136623663366436653666366736683669367036713672367336743675367636773678367936803681368236833684368536863687
  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} = require("chrome");
  7. const {Utils: WebConsoleUtils, CONSOLE_WORKER_IDS} =
  8. require("devtools/client/webconsole/utils");
  9. const { getSourceNames } = require("devtools/client/shared/source-utils");
  10. const BrowserLoaderModule = {};
  11. Cu.import("resource://devtools/client/shared/browser-loader.js", BrowserLoaderModule);
  12. const promise = require("promise");
  13. const Services = require("Services");
  14. const ErrorDocs = require("devtools/server/actors/errordocs");
  15. const Telemetry = require("devtools/client/shared/telemetry");
  16. loader.lazyServiceGetter(this, "clipboardHelper",
  17. "@mozilla.org/widget/clipboardhelper;1",
  18. "nsIClipboardHelper");
  19. loader.lazyRequireGetter(this, "EventEmitter", "devtools/shared/event-emitter");
  20. loader.lazyRequireGetter(this, "AutocompletePopup", "devtools/client/shared/autocomplete-popup", true);
  21. loader.lazyRequireGetter(this, "ToolSidebar", "devtools/client/framework/sidebar", true);
  22. loader.lazyRequireGetter(this, "ConsoleOutput", "devtools/client/webconsole/console-output", true);
  23. loader.lazyRequireGetter(this, "Messages", "devtools/client/webconsole/console-output", true);
  24. loader.lazyRequireGetter(this, "EnvironmentClient", "devtools/shared/client/main", true);
  25. loader.lazyRequireGetter(this, "ObjectClient", "devtools/shared/client/main", true);
  26. loader.lazyRequireGetter(this, "system", "devtools/shared/system");
  27. loader.lazyRequireGetter(this, "JSTerm", "devtools/client/webconsole/jsterm", true);
  28. loader.lazyRequireGetter(this, "gSequenceId", "devtools/client/webconsole/jsterm", true);
  29. loader.lazyImporter(this, "VariablesView", "resource://devtools/client/shared/widgets/VariablesView.jsm");
  30. loader.lazyImporter(this, "VariablesViewController", "resource://devtools/client/shared/widgets/VariablesViewController.jsm");
  31. loader.lazyRequireGetter(this, "gDevTools", "devtools/client/framework/devtools", true);
  32. loader.lazyRequireGetter(this, "KeyShortcuts", "devtools/client/shared/key-shortcuts", true);
  33. loader.lazyRequireGetter(this, "ZoomKeys", "devtools/client/shared/zoom-keys");
  34. const {PluralForm} = require("devtools/shared/plural-form");
  35. const STRINGS_URI = "devtools/client/locales/webconsole.properties";
  36. var l10n = new WebConsoleUtils.L10n(STRINGS_URI);
  37. const XHTML_NS = "http://www.w3.org/1999/xhtml";
  38. const MIXED_CONTENT_LEARN_MORE = "https://developer.mozilla.org/docs/Web/Security/Mixed_content";
  39. const IGNORED_SOURCE_URLS = ["debugger eval code"];
  40. // The amount of time in milliseconds that we wait before performing a live
  41. // search.
  42. const SEARCH_DELAY = 200;
  43. // The number of lines that are displayed in the console output by default, for
  44. // each category. The user can change this number by adjusting the hidden
  45. // "devtools.hud.loglimit.{network,cssparser,exception,console}" preferences.
  46. const DEFAULT_LOG_LIMIT = 1000;
  47. // The various categories of messages. We start numbering at zero so we can
  48. // use these as indexes into the MESSAGE_PREFERENCE_KEYS matrix below.
  49. const CATEGORY_NETWORK = 0;
  50. const CATEGORY_CSS = 1;
  51. const CATEGORY_JS = 2;
  52. const CATEGORY_WEBDEV = 3;
  53. // always on
  54. const CATEGORY_INPUT = 4;
  55. // always on
  56. const CATEGORY_OUTPUT = 5;
  57. const CATEGORY_SECURITY = 6;
  58. const CATEGORY_SERVER = 7;
  59. // The possible message severities. As before, we start at zero so we can use
  60. // these as indexes into MESSAGE_PREFERENCE_KEYS.
  61. const SEVERITY_ERROR = 0;
  62. const SEVERITY_WARNING = 1;
  63. const SEVERITY_INFO = 2;
  64. const SEVERITY_LOG = 3;
  65. // The fragment of a CSS class name that identifies each category.
  66. const CATEGORY_CLASS_FRAGMENTS = [
  67. "network",
  68. "cssparser",
  69. "exception",
  70. "console",
  71. "input",
  72. "output",
  73. "security",
  74. "server",
  75. ];
  76. // The fragment of a CSS class name that identifies each severity.
  77. const SEVERITY_CLASS_FRAGMENTS = [
  78. "error",
  79. "warn",
  80. "info",
  81. "log",
  82. ];
  83. // The preference keys to use for each category/severity combination, indexed
  84. // first by category (rows) and then by severity (columns) in the following
  85. // order:
  86. //
  87. // [ Error, Warning, Info, Log ]
  88. //
  89. // Most of these rather idiosyncratic names are historical and predate the
  90. // division of message type into "category" and "severity".
  91. const MESSAGE_PREFERENCE_KEYS = [
  92. // Network
  93. [ "network", "netwarn", "netxhr", "networkinfo", ],
  94. // CSS
  95. [ "csserror", "cssparser", null, "csslog", ],
  96. // JS
  97. [ "exception", "jswarn", null, "jslog", ],
  98. // Web Developer
  99. [ "error", "warn", "info", "log", ],
  100. // Input
  101. [ null, null, null, null, ],
  102. // Output
  103. [ null, null, null, null, ],
  104. // Security
  105. [ "secerror", "secwarn", null, null, ],
  106. // Server Logging
  107. [ "servererror", "serverwarn", "serverinfo", "serverlog", ],
  108. ];
  109. // A mapping from the console API log event levels to the Web Console
  110. // severities.
  111. const LEVELS = {
  112. error: SEVERITY_ERROR,
  113. exception: SEVERITY_ERROR,
  114. assert: SEVERITY_ERROR,
  115. warn: SEVERITY_WARNING,
  116. info: SEVERITY_INFO,
  117. log: SEVERITY_LOG,
  118. clear: SEVERITY_LOG,
  119. trace: SEVERITY_LOG,
  120. table: SEVERITY_LOG,
  121. debug: SEVERITY_LOG,
  122. dir: SEVERITY_LOG,
  123. dirxml: SEVERITY_LOG,
  124. group: SEVERITY_LOG,
  125. groupCollapsed: SEVERITY_LOG,
  126. groupEnd: SEVERITY_LOG,
  127. time: SEVERITY_LOG,
  128. timeEnd: SEVERITY_LOG,
  129. count: SEVERITY_LOG
  130. };
  131. // This array contains the prefKey for the workers and it must keep them in the
  132. // same order as CONSOLE_WORKER_IDS
  133. const WORKERTYPES_PREFKEYS =
  134. [ "sharedworkers", "serviceworkers", "windowlessworkers" ];
  135. // The lowest HTTP response code (inclusive) that is considered an error.
  136. const MIN_HTTP_ERROR_CODE = 400;
  137. // The highest HTTP response code (inclusive) that is considered an error.
  138. const MAX_HTTP_ERROR_CODE = 599;
  139. // The indent of a console group in pixels.
  140. const GROUP_INDENT = 12;
  141. // The number of messages to display in a single display update. If we display
  142. // too many messages at once we slow down the Firefox UI too much.
  143. const MESSAGES_IN_INTERVAL = DEFAULT_LOG_LIMIT;
  144. // The delay (in milliseconds) between display updates - tells how often we
  145. // should *try* to push new messages to screen. This value is optimistic,
  146. // updates won't always happen. Keep this low so the Web Console output feels
  147. // live.
  148. const OUTPUT_INTERVAL = 20;
  149. // The maximum amount of time (in milliseconds) that can be spent doing cleanup
  150. // inside of the flush output callback. If things don't get cleaned up in this
  151. // time, then it will start again the next time it is called.
  152. const MAX_CLEANUP_TIME = 10;
  153. // When the output queue has more than MESSAGES_IN_INTERVAL items we throttle
  154. // output updates to this number of milliseconds. So during a lot of output we
  155. // update every N milliseconds given here.
  156. const THROTTLE_UPDATES = 1000;
  157. // The preference prefix for all of the Web Console filters.
  158. const FILTER_PREFS_PREFIX = "devtools.webconsole.filter.";
  159. // The minimum font size.
  160. const MIN_FONT_SIZE = 10;
  161. const PREF_CONNECTION_TIMEOUT = "devtools.debugger.remote-timeout";
  162. const PREF_PERSISTLOG = "devtools.webconsole.persistlog";
  163. const PREF_MESSAGE_TIMESTAMP = "devtools.webconsole.timestampMessages";
  164. const PREF_NEW_FRONTEND_ENABLED = "devtools.webconsole.new-frontend-enabled";
  165. /**
  166. * A WebConsoleFrame instance is an interactive console initialized *per target*
  167. * that displays console log data as well as provides an interactive terminal to
  168. * manipulate the target's document content.
  169. *
  170. * The WebConsoleFrame is responsible for the actual Web Console UI
  171. * implementation.
  172. *
  173. * @constructor
  174. * @param object webConsoleOwner
  175. * The WebConsole owner object.
  176. */
  177. function WebConsoleFrame(webConsoleOwner) {
  178. this.owner = webConsoleOwner;
  179. this.hudId = this.owner.hudId;
  180. this.isBrowserConsole = this.owner._browserConsole;
  181. this.window = this.owner.iframeWindow;
  182. this._repeatNodes = {};
  183. this._outputQueue = [];
  184. this._itemDestroyQueue = [];
  185. this._pruneCategoriesQueue = {};
  186. this.filterPrefs = {};
  187. this.output = new ConsoleOutput(this);
  188. this.unmountMessage = this.unmountMessage.bind(this);
  189. this._toggleFilter = this._toggleFilter.bind(this);
  190. this.resize = this.resize.bind(this);
  191. this._onPanelSelected = this._onPanelSelected.bind(this);
  192. this._flushMessageQueue = this._flushMessageQueue.bind(this);
  193. this._onToolboxPrefChanged = this._onToolboxPrefChanged.bind(this);
  194. this._onUpdateListeners = this._onUpdateListeners.bind(this);
  195. this._outputTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
  196. this._outputTimerInitialized = false;
  197. let require = BrowserLoaderModule.BrowserLoader({
  198. window: this.window,
  199. useOnlyShared: true
  200. }).require;
  201. this.React = require("devtools/client/shared/vendor/react");
  202. this.ReactDOM = require("devtools/client/shared/vendor/react-dom");
  203. this.FrameView = this.React.createFactory(require("devtools/client/shared/components/frame"));
  204. this.StackTraceView = this.React.createFactory(require("devtools/client/shared/components/stack-trace"));
  205. this._telemetry = new Telemetry();
  206. EventEmitter.decorate(this);
  207. }
  208. exports.WebConsoleFrame = WebConsoleFrame;
  209. WebConsoleFrame.prototype = {
  210. /**
  211. * The WebConsole instance that owns this frame.
  212. * @see hudservice.js::WebConsole
  213. * @type object
  214. */
  215. owner: null,
  216. /**
  217. * Proxy between the Web Console and the remote Web Console instance. This
  218. * object holds methods used for connecting, listening and disconnecting from
  219. * the remote server, using the remote debugging protocol.
  220. *
  221. * @see WebConsoleConnectionProxy
  222. * @type object
  223. */
  224. proxy: null,
  225. /**
  226. * Getter for the xul:popupset that holds any popups we open.
  227. * @type nsIDOMElement
  228. */
  229. get popupset() {
  230. return this.owner.mainPopupSet;
  231. },
  232. /**
  233. * Holds the initialization promise object.
  234. * @private
  235. * @type object
  236. */
  237. _initDefer: null,
  238. /**
  239. * Last time when we displayed any message in the output.
  240. *
  241. * @private
  242. * @type number
  243. * Timestamp in milliseconds since the Unix epoch.
  244. */
  245. _lastOutputFlush: 0,
  246. /**
  247. * Message nodes are stored here in a queue for later display.
  248. *
  249. * @private
  250. * @type array
  251. */
  252. _outputQueue: null,
  253. /**
  254. * Keep track of the categories we need to prune from time to time.
  255. *
  256. * @private
  257. * @type array
  258. */
  259. _pruneCategoriesQueue: null,
  260. /**
  261. * Function invoked whenever the output queue is emptied. This is used by some
  262. * tests.
  263. *
  264. * @private
  265. * @type function
  266. */
  267. _flushCallback: null,
  268. /**
  269. * Timer used for flushing the messages output queue.
  270. *
  271. * @private
  272. * @type nsITimer
  273. */
  274. _outputTimer: null,
  275. _outputTimerInitialized: null,
  276. /**
  277. * Store for tracking repeated nodes.
  278. * @private
  279. * @type object
  280. */
  281. _repeatNodes: null,
  282. /**
  283. * Preferences for filtering messages by type.
  284. * @see this._initDefaultFilterPrefs()
  285. * @type object
  286. */
  287. filterPrefs: null,
  288. /**
  289. * Prefix used for filter preferences.
  290. * @private
  291. * @type string
  292. */
  293. _filterPrefsPrefix: FILTER_PREFS_PREFIX,
  294. /**
  295. * The nesting depth of the currently active console group.
  296. */
  297. groupDepth: 0,
  298. /**
  299. * The current target location.
  300. * @type string
  301. */
  302. contentLocation: "",
  303. /**
  304. * The JSTerm object that manage the console's input.
  305. * @see JSTerm
  306. * @type object
  307. */
  308. jsterm: null,
  309. /**
  310. * The element that holds all of the messages we display.
  311. * @type nsIDOMElement
  312. */
  313. outputNode: null,
  314. /**
  315. * The ConsoleOutput instance that manages all output.
  316. * @type object
  317. */
  318. output: null,
  319. /**
  320. * The input element that allows the user to filter messages by string.
  321. * @type nsIDOMElement
  322. */
  323. filterBox: null,
  324. /**
  325. * Getter for the debugger WebConsoleClient.
  326. * @type object
  327. */
  328. get webConsoleClient() {
  329. return this.proxy ? this.proxy.webConsoleClient : null;
  330. },
  331. _destroyer: null,
  332. _saveRequestAndResponseBodies: true,
  333. _throttleData: null,
  334. // Chevron width at the starting of Web Console's input box.
  335. _chevronWidth: 0,
  336. // Width of the monospace characters in Web Console's input box.
  337. _inputCharWidth: 0,
  338. /**
  339. * Setter for saving of network request and response bodies.
  340. *
  341. * @param boolean value
  342. * The new value you want to set.
  343. */
  344. setSaveRequestAndResponseBodies: function (value) {
  345. if (!this.webConsoleClient) {
  346. // Don't continue if the webconsole disconnected.
  347. return promise.resolve(null);
  348. }
  349. let deferred = promise.defer();
  350. let newValue = !!value;
  351. let toSet = {
  352. "NetworkMonitor.saveRequestAndResponseBodies": newValue,
  353. };
  354. // Make sure the web console client connection is established first.
  355. this.webConsoleClient.setPreferences(toSet, response => {
  356. if (!response.error) {
  357. this._saveRequestAndResponseBodies = newValue;
  358. deferred.resolve(response);
  359. } else {
  360. deferred.reject(response.error);
  361. }
  362. });
  363. return deferred.promise;
  364. },
  365. /**
  366. * Setter for throttling data.
  367. *
  368. * @param boolean value
  369. * The new value you want to set; @see NetworkThrottleManager.
  370. */
  371. setThrottleData: function(value) {
  372. if (!this.webConsoleClient) {
  373. // Don't continue if the webconsole disconnected.
  374. return promise.resolve(null);
  375. }
  376. let deferred = promise.defer();
  377. let toSet = {
  378. "NetworkMonitor.throttleData": value,
  379. };
  380. // Make sure the web console client connection is established first.
  381. this.webConsoleClient.setPreferences(toSet, response => {
  382. if (!response.error) {
  383. this._throttleData = value;
  384. deferred.resolve(response);
  385. } else {
  386. deferred.reject(response.error);
  387. }
  388. });
  389. return deferred.promise;
  390. },
  391. /**
  392. * Getter for the persistent logging preference.
  393. * @type boolean
  394. */
  395. get persistLog() {
  396. // For the browser console, we receive tab navigation
  397. // when the original top level window we attached to is closed,
  398. // but we don't want to reset console history and just switch to
  399. // the next available window.
  400. return this.isBrowserConsole ||
  401. Services.prefs.getBoolPref(PREF_PERSISTLOG);
  402. },
  403. /**
  404. * Initialize the WebConsoleFrame instance.
  405. * @return object
  406. * A promise object that resolves once the frame is ready to use.
  407. */
  408. init: function () {
  409. this._initUI();
  410. let connectionInited = this._initConnection();
  411. // Don't reject if the history fails to load for some reason.
  412. // This would be fine, the panel will just start with empty history.
  413. let allReady = this.jsterm.historyLoaded.catch(() => {}).then(() => {
  414. return connectionInited;
  415. });
  416. // This notification is only used in tests. Don't chain it onto
  417. // the returned promise because the console panel needs to be attached
  418. // to the toolbox before the web-console-created event is receieved.
  419. let notifyObservers = () => {
  420. let id = WebConsoleUtils.supportsString(this.hudId);
  421. Services.obs.notifyObservers(id, "web-console-created", null);
  422. };
  423. allReady.then(notifyObservers, notifyObservers);
  424. if (this.NEW_CONSOLE_OUTPUT_ENABLED) {
  425. allReady.then(this.newConsoleOutput.init);
  426. }
  427. return allReady;
  428. },
  429. /**
  430. * Connect to the server using the remote debugging protocol.
  431. *
  432. * @private
  433. * @return object
  434. * A promise object that is resolved/reject based on the connection
  435. * result.
  436. */
  437. _initConnection: function () {
  438. if (this._initDefer) {
  439. return this._initDefer.promise;
  440. }
  441. this._initDefer = promise.defer();
  442. this.proxy = new WebConsoleConnectionProxy(this, this.owner.target);
  443. this.proxy.connect().then(() => {
  444. // on success
  445. this._initDefer.resolve(this);
  446. }, (reason) => {
  447. // on failure
  448. let node = this.createMessageNode(CATEGORY_JS, SEVERITY_ERROR,
  449. reason.error + ": " + reason.message);
  450. this.outputMessage(CATEGORY_JS, node, [reason]);
  451. this._initDefer.reject(reason);
  452. });
  453. return this._initDefer.promise;
  454. },
  455. /**
  456. * Find the Web Console UI elements and setup event listeners as needed.
  457. * @private
  458. */
  459. _initUI: function () {
  460. this.document = this.window.document;
  461. this.rootElement = this.document.documentElement;
  462. this.NEW_CONSOLE_OUTPUT_ENABLED = !this.isBrowserConsole
  463. && !this.owner.target.chrome
  464. && Services.prefs.getBoolPref(PREF_NEW_FRONTEND_ENABLED);
  465. this.outputNode = this.document.getElementById("output-container");
  466. this.outputWrapper = this.document.getElementById("output-wrapper");
  467. this.completeNode = this.document.querySelector(".jsterm-complete-node");
  468. this.inputNode = this.document.querySelector(".jsterm-input-node");
  469. // In the old frontend, the area that scrolls is outputWrapper, but in the new
  470. // frontend this will be reassigned.
  471. this.outputScroller = this.outputWrapper;
  472. // Update the character width and height needed for the popup offset
  473. // calculations.
  474. this._updateCharSize();
  475. let saveBodiesDisabled = !this.getFilterState("networkinfo") &&
  476. !this.getFilterState("netxhr") &&
  477. !this.getFilterState("network");
  478. let saveBodies = this.document.getElementById("saveBodies");
  479. saveBodies.disabled = saveBodiesDisabled;
  480. saveBodies.parentNode.addEventListener("popupshowing", () => {
  481. saveBodies.disabled = !this.getFilterState("networkinfo") &&
  482. !this.getFilterState("netxhr") &&
  483. !this.getFilterState("network");
  484. });
  485. this.jsterm = new JSTerm(this);
  486. this.jsterm.init();
  487. let toolbox = gDevTools.getToolbox(this.owner.target);
  488. if (this.NEW_CONSOLE_OUTPUT_ENABLED) {
  489. // @TODO Remove this once JSTerm is handled with React/Redux.
  490. this.window.jsterm = this.jsterm;
  491. // Remove context menu for now (see Bug 1307239).
  492. this.outputWrapper.removeAttribute("context");
  493. // XXX: We should actually stop output from happening on old output
  494. // panel, but for now let's just hide it.
  495. this.experimentalOutputNode = this.outputNode.cloneNode();
  496. this.experimentalOutputNode.removeAttribute("tabindex");
  497. this.outputNode.hidden = true;
  498. this.outputNode.parentNode.appendChild(this.experimentalOutputNode);
  499. // @TODO Once the toolbox has been converted to React, see if passing
  500. // in JSTerm is still necessary.
  501. this.newConsoleOutput = new this.window.NewConsoleOutput(
  502. this.experimentalOutputNode, this.jsterm, toolbox, this.owner, this.document);
  503. let filterToolbar = this.document.querySelector(".hud-console-filter-toolbar");
  504. filterToolbar.hidden = true;
  505. } else {
  506. // Register the controller to handle "select all" properly.
  507. this._commandController = new CommandController(this);
  508. this.window.controllers.insertControllerAt(0, this._commandController);
  509. this._contextMenuHandler = new ConsoleContextMenu(this);
  510. this._initDefaultFilterPrefs();
  511. this.filterBox = this.document.querySelector(".hud-filter-box");
  512. this._setFilterTextBoxEvents();
  513. this._initFilterButtons();
  514. let clearButton =
  515. this.document.getElementsByClassName("webconsole-clear-console-button")[0];
  516. clearButton.addEventListener("command", () => {
  517. this.owner._onClearButton();
  518. this.jsterm.clearOutput(true);
  519. });
  520. }
  521. this.resize();
  522. this.window.addEventListener("resize", this.resize, true);
  523. this.jsterm.on("sidebar-opened", this.resize);
  524. this.jsterm.on("sidebar-closed", this.resize);
  525. if (toolbox) {
  526. toolbox.on("webconsole-selected", this._onPanelSelected);
  527. }
  528. /*
  529. * Focus the input line whenever the output area is clicked.
  530. */
  531. this.outputWrapper.addEventListener("click", (event) => {
  532. // Do not focus on middle/right-click or 2+ clicks.
  533. if (event.detail !== 1 || event.button !== 0) {
  534. return;
  535. }
  536. // Do not focus if something is selected
  537. let selection = this.window.getSelection();
  538. if (selection && !selection.isCollapsed) {
  539. return;
  540. }
  541. // Do not focus if a link was clicked
  542. if (event.target.nodeName.toLowerCase() === "a" ||
  543. event.target.parentNode.nodeName.toLowerCase() === "a") {
  544. return;
  545. }
  546. // Do not focus if a search input was clicked on the new frontend
  547. if (this.NEW_CONSOLE_OUTPUT_ENABLED &&
  548. event.target.nodeName.toLowerCase() === "input" &&
  549. event.target.getAttribute("type").toLowerCase() === "search") {
  550. return;
  551. }
  552. this.jsterm.focus();
  553. });
  554. // Toggle the timestamp on preference change
  555. gDevTools.on("pref-changed", this._onToolboxPrefChanged);
  556. this._onToolboxPrefChanged("pref-changed", {
  557. pref: PREF_MESSAGE_TIMESTAMP,
  558. newValue: Services.prefs.getBoolPref(PREF_MESSAGE_TIMESTAMP),
  559. });
  560. this._initShortcuts();
  561. // focus input node
  562. this.jsterm.focus();
  563. },
  564. /**
  565. * Resizes the output node to fit the output wrapped.
  566. * We need this because it makes the layout a lot faster than
  567. * using -moz-box-flex and 100% width. See Bug 1237368.
  568. */
  569. resize: function () {
  570. if (this.NEW_CONSOLE_OUTPUT_ENABLED) {
  571. this.experimentalOutputNode.style.width =
  572. this.outputWrapper.clientWidth + "px";
  573. } else {
  574. this.outputNode.style.width = this.outputWrapper.clientWidth + "px";
  575. }
  576. },
  577. /**
  578. * Sets the focus to JavaScript input field when the web console tab is
  579. * selected or when there is a split console present.
  580. * @private
  581. */
  582. _onPanelSelected: function () {
  583. this.jsterm.focus();
  584. },
  585. /**
  586. * Initialize the default filter preferences.
  587. * @private
  588. */
  589. _initDefaultFilterPrefs: function () {
  590. let prefs = ["network", "networkinfo", "csserror", "cssparser", "csslog",
  591. "exception", "jswarn", "jslog", "error", "info", "warn", "log",
  592. "secerror", "secwarn", "netwarn", "netxhr", "saveBodies",
  593. "sharedworkers", "serviceworkers", "windowlessworkers",
  594. "servererror", "serverwarn", "serverinfo", "serverlog"];
  595. for (let pref of prefs) {
  596. this.filterPrefs[pref] = Services.prefs.getBoolPref(
  597. this._filterPrefsPrefix + pref);
  598. }
  599. },
  600. _initShortcuts: function() {
  601. var shortcuts = new KeyShortcuts({
  602. window: this.window
  603. });
  604. shortcuts.on(l10n.getStr("webconsole.find.key"),
  605. (name, event) => {
  606. this.filterBox.focus();
  607. event.preventDefault();
  608. });
  609. let clearShortcut;
  610. if (system.constants.platform === "macosx") {
  611. clearShortcut = l10n.getStr("webconsole.clear.keyOSX");
  612. } else {
  613. clearShortcut = l10n.getStr("webconsole.clear.key");
  614. }
  615. shortcuts.on(clearShortcut,
  616. () => this.jsterm.clearOutput(true));
  617. if (this.isBrowserConsole) {
  618. shortcuts.on(l10n.getStr("webconsole.close.key"),
  619. this.window.close.bind(this.window));
  620. ZoomKeys.register(this.window);
  621. }
  622. },
  623. /**
  624. * Attach / detach reflow listeners depending on the checked status
  625. * of the `CSS > Log` menuitem.
  626. *
  627. * @param function [callback=null]
  628. * Optional function to invoke when the listener has been
  629. * added/removed.
  630. */
  631. _updateReflowActivityListener: function (callback) {
  632. if (this.webConsoleClient) {
  633. let pref = this._filterPrefsPrefix + "csslog";
  634. if (Services.prefs.getBoolPref(pref)) {
  635. this.webConsoleClient.startListeners(["ReflowActivity"], callback);
  636. } else {
  637. this.webConsoleClient.stopListeners(["ReflowActivity"], callback);
  638. }
  639. }
  640. },
  641. /**
  642. * Attach / detach server logging listener depending on the filter
  643. * preferences. If the user isn't interested in the server logs at
  644. * all the listener is not registered.
  645. *
  646. * @param function [callback=null]
  647. * Optional function to invoke when the listener has been
  648. * added/removed.
  649. */
  650. _updateServerLoggingListener: function (callback) {
  651. if (!this.webConsoleClient) {
  652. return null;
  653. }
  654. let startListener = false;
  655. let prefs = ["servererror", "serverwarn", "serverinfo", "serverlog"];
  656. for (let i = 0; i < prefs.length; i++) {
  657. if (this.filterPrefs[prefs[i]]) {
  658. startListener = true;
  659. break;
  660. }
  661. }
  662. if (startListener) {
  663. this.webConsoleClient.startListeners(["ServerLogging"], callback);
  664. } else {
  665. this.webConsoleClient.stopListeners(["ServerLogging"], callback);
  666. }
  667. },
  668. /**
  669. * Sets the events for the filter input field.
  670. * @private
  671. */
  672. _setFilterTextBoxEvents: function () {
  673. let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
  674. let timerEvent = this.adjustVisibilityOnSearchStringChange.bind(this);
  675. let onChange = function _onChange() {
  676. // To improve responsiveness, we let the user finish typing before we
  677. // perform the search.
  678. timer.cancel();
  679. timer.initWithCallback(timerEvent, SEARCH_DELAY,
  680. Ci.nsITimer.TYPE_ONE_SHOT);
  681. };
  682. this.filterBox.addEventListener("command", onChange, false);
  683. this.filterBox.addEventListener("input", onChange, false);
  684. },
  685. /**
  686. * Creates one of the filter buttons on the toolbar.
  687. *
  688. * @private
  689. * @param nsIDOMNode aParent
  690. * The node to which the filter button should be appended.
  691. * @param object aDescriptor
  692. * A descriptor that contains info about the button. Contains "name",
  693. * "category", and "prefKey" properties, and optionally a "severities"
  694. * property.
  695. */
  696. _initFilterButtons: function () {
  697. let categories = this.document
  698. .querySelectorAll(".webconsole-filter-button[category]");
  699. Array.forEach(categories, function (button) {
  700. button.addEventListener("contextmenu", () => {
  701. button.open = true;
  702. }, false);
  703. button.addEventListener("click", this._toggleFilter, false);
  704. let someChecked = false;
  705. let severities = button.querySelectorAll("menuitem[prefKey]");
  706. Array.forEach(severities, function (menuItem) {
  707. menuItem.addEventListener("command", this._toggleFilter, false);
  708. let prefKey = menuItem.getAttribute("prefKey");
  709. let checked = this.filterPrefs[prefKey];
  710. menuItem.setAttribute("checked", checked);
  711. someChecked = someChecked || checked;
  712. }, this);
  713. button.setAttribute("checked", someChecked);
  714. button.setAttribute("aria-pressed", someChecked);
  715. }, this);
  716. if (!this.isBrowserConsole) {
  717. // The Browser Console displays nsIConsoleMessages which are messages that
  718. // end up in the JS category, but they are not errors or warnings, they
  719. // are just log messages. The Web Console does not show such messages.
  720. let jslog = this.document.querySelector("menuitem[prefKey=jslog]");
  721. jslog.hidden = true;
  722. }
  723. if (Services.appinfo.OS == "Darwin") {
  724. let net = this.document.querySelector("toolbarbutton[category=net]");
  725. let accesskey = net.getAttribute("accesskeyMacOSX");
  726. net.setAttribute("accesskey", accesskey);
  727. let logging =
  728. this.document.querySelector("toolbarbutton[category=logging]");
  729. logging.removeAttribute("accesskey");
  730. let serverLogging =
  731. this.document.querySelector("toolbarbutton[category=server]");
  732. serverLogging.removeAttribute("accesskey");
  733. }
  734. },
  735. /**
  736. * Calculates the width and height of a single character of the input box.
  737. * This will be used in opening the popup at the correct offset.
  738. *
  739. * @private
  740. */
  741. _updateCharSize: function () {
  742. let doc = this.document;
  743. let tempLabel = doc.createElementNS(XHTML_NS, "span");
  744. let style = tempLabel.style;
  745. style.position = "fixed";
  746. style.padding = "0";
  747. style.margin = "0";
  748. style.width = "auto";
  749. style.color = "transparent";
  750. WebConsoleUtils.copyTextStyles(this.inputNode, tempLabel);
  751. tempLabel.textContent = "x";
  752. doc.documentElement.appendChild(tempLabel);
  753. this._inputCharWidth = tempLabel.offsetWidth;
  754. tempLabel.parentNode.removeChild(tempLabel);
  755. // Calculate the width of the chevron placed at the beginning of the input
  756. // box. Remove 4 more pixels to accomodate the padding of the popup.
  757. this._chevronWidth = +doc.defaultView.getComputedStyle(this.inputNode)
  758. .paddingLeft.replace(/[^0-9.]/g, "") - 4;
  759. },
  760. /**
  761. * The event handler that is called whenever a user switches a filter on or
  762. * off.
  763. *
  764. * @private
  765. * @param nsIDOMEvent event
  766. * The event that triggered the filter change.
  767. */
  768. _toggleFilter: function (event) {
  769. let target = event.target;
  770. let tagName = target.tagName;
  771. // Prevent toggle if generated from a contextmenu event (right click)
  772. let isRightClick = (event.button === 2);
  773. if (tagName != event.currentTarget.tagName || isRightClick) {
  774. return;
  775. }
  776. switch (tagName) {
  777. case "toolbarbutton": {
  778. let originalTarget = event.originalTarget;
  779. let classes = originalTarget.classList;
  780. if (originalTarget.localName !== "toolbarbutton") {
  781. // Oddly enough, the click event is sent to the menu button when
  782. // selecting a menu item with the mouse. Detect this case and bail
  783. // out.
  784. break;
  785. }
  786. if (!classes.contains("toolbarbutton-menubutton-button") &&
  787. originalTarget.getAttribute("type") === "menu-button") {
  788. // This is a filter button with a drop-down. The user clicked the
  789. // drop-down, so do nothing. (The menu will automatically appear
  790. // without our intervention.)
  791. break;
  792. }
  793. // Toggle on the targeted filter button, and if the user alt clicked,
  794. // toggle off all other filter buttons and their associated filters.
  795. let state = target.getAttribute("checked") !== "true";
  796. if (event.getModifierState("Alt")) {
  797. let buttons = this.document
  798. .querySelectorAll(".webconsole-filter-button");
  799. Array.forEach(buttons, (button) => {
  800. if (button !== target) {
  801. button.setAttribute("checked", false);
  802. button.setAttribute("aria-pressed", false);
  803. this._setMenuState(button, false);
  804. }
  805. });
  806. state = true;
  807. }
  808. target.setAttribute("checked", state);
  809. target.setAttribute("aria-pressed", state);
  810. // This is a filter button with a drop-down, and the user clicked the
  811. // main part of the button. Go through all the severities and toggle
  812. // their associated filters.
  813. this._setMenuState(target, state);
  814. // CSS reflow logging can decrease web page performance.
  815. // Make sure the option is always unchecked when the CSS filter button
  816. // is selected. See bug 971798.
  817. if (target.getAttribute("category") == "css" && state) {
  818. let csslogMenuItem = target.querySelector("menuitem[prefKey=csslog]");
  819. csslogMenuItem.setAttribute("checked", false);
  820. this.setFilterState("csslog", false);
  821. }
  822. break;
  823. }
  824. case "menuitem": {
  825. let state = target.getAttribute("checked") !== "true";
  826. target.setAttribute("checked", state);
  827. let prefKey = target.getAttribute("prefKey");
  828. this.setFilterState(prefKey, state);
  829. // Disable the log response and request body if network logging is off.
  830. if (prefKey == "networkinfo" ||
  831. prefKey == "netxhr" ||
  832. prefKey == "network") {
  833. let checkState = !this.getFilterState("networkinfo") &&
  834. !this.getFilterState("netxhr") &&
  835. !this.getFilterState("network");
  836. this.document.getElementById("saveBodies").disabled = checkState;
  837. }
  838. // Adjust the state of the button appropriately.
  839. let menuPopup = target.parentNode;
  840. let someChecked = false;
  841. let menuItem = menuPopup.firstChild;
  842. while (menuItem) {
  843. if (menuItem.hasAttribute("prefKey") &&
  844. menuItem.getAttribute("checked") === "true") {
  845. someChecked = true;
  846. break;
  847. }
  848. menuItem = menuItem.nextSibling;
  849. }
  850. let toolbarButton = menuPopup.parentNode;
  851. toolbarButton.setAttribute("checked", someChecked);
  852. toolbarButton.setAttribute("aria-pressed", someChecked);
  853. break;
  854. }
  855. }
  856. },
  857. /**
  858. * Set the menu attributes for a specific toggle button.
  859. *
  860. * @private
  861. * @param XULElement target
  862. * Button with drop down items to be toggled.
  863. * @param boolean state
  864. * True if the menu item is being toggled on, and false otherwise.
  865. */
  866. _setMenuState: function (target, state) {
  867. let menuItems = target.querySelectorAll("menuitem");
  868. Array.forEach(menuItems, (item) => {
  869. let prefKey = item.getAttribute("prefKey");
  870. // If not a separate switch only.
  871. if (prefKey != "saveBodies") {
  872. item.setAttribute("checked", state);
  873. this.setFilterState(prefKey, state);
  874. }
  875. });
  876. },
  877. /**
  878. * Set the filter state for a specific toggle button.
  879. *
  880. * @param string toggleType
  881. * @param boolean state
  882. * @returns void
  883. */
  884. setFilterState: function (toggleType, state) {
  885. this.filterPrefs[toggleType] = state;
  886. this.adjustVisibilityForMessageType(toggleType, state);
  887. Services.prefs.setBoolPref(this._filterPrefsPrefix + toggleType, state);
  888. if (toggleType == "saveBodies") {
  889. this.setSaveRequestAndResponseBodies(state);
  890. }
  891. if (this._updateListenersTimeout) {
  892. clearTimeout(this._updateListenersTimeout);
  893. }
  894. this._updateListenersTimeout = setTimeout(
  895. this._onUpdateListeners, 200);
  896. },
  897. /**
  898. * Get the filter state for a specific toggle button.
  899. *
  900. * @param string toggleType
  901. * @returns boolean
  902. */
  903. getFilterState: function (toggleType) {
  904. return this.filterPrefs[toggleType];
  905. },
  906. /**
  907. * Called when a logging filter changes. Allows to stop/start
  908. * listeners according to the current filter state.
  909. */
  910. _onUpdateListeners: function () {
  911. this._updateReflowActivityListener();
  912. this._updateServerLoggingListener();
  913. },
  914. /**
  915. * Check that the passed string matches the filter arguments.
  916. *
  917. * @param String str
  918. * to search for filter words in.
  919. * @param String filter
  920. * is a string containing all of the words to filter on.
  921. * @returns boolean
  922. */
  923. stringMatchesFilters: function (str, filter) {
  924. if (!filter || !str) {
  925. return true;
  926. }
  927. let searchStr = str.toLowerCase();
  928. let filterStrings = filter.toLowerCase().split(/\s+/);
  929. return !filterStrings.some(function (f) {
  930. return searchStr.indexOf(f) == -1;
  931. });
  932. },
  933. /**
  934. * Turns the display of log nodes on and off appropriately to reflect the
  935. * adjustment of the message type filter named by @prefKey.
  936. *
  937. * @param string prefKey
  938. * The preference key for the message type being filtered: one of the
  939. * values in the MESSAGE_PREFERENCE_KEYS table.
  940. * @param boolean state
  941. * True if the filter named by @messageType is being turned on; false
  942. * otherwise.
  943. * @returns void
  944. */
  945. adjustVisibilityForMessageType: function (prefKey, state) {
  946. let outputNode = this.outputNode;
  947. let doc = this.document;
  948. // Look for message nodes (".message") with the given preference key
  949. // (filter="error", filter="cssparser", etc.) and add or remove the
  950. // "filtered-by-type" class, which turns on or off the display.
  951. let attribute = WORKERTYPES_PREFKEYS.indexOf(prefKey) == -1
  952. ? "filter" : "workerType";
  953. let xpath = ".//*[contains(@class, 'message') and " +
  954. "@" + attribute + "='" + prefKey + "']";
  955. let result = doc.evaluate(xpath, outputNode, null,
  956. Ci.nsIDOMXPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
  957. for (let i = 0; i < result.snapshotLength; i++) {
  958. let node = result.snapshotItem(i);
  959. if (state) {
  960. node.classList.remove("filtered-by-type");
  961. } else {
  962. node.classList.add("filtered-by-type");
  963. }
  964. }
  965. },
  966. /**
  967. * Turns the display of log nodes on and off appropriately to reflect the
  968. * adjustment of the search string.
  969. */
  970. adjustVisibilityOnSearchStringChange: function () {
  971. let nodes = this.outputNode.getElementsByClassName("message");
  972. let searchString = this.filterBox.value;
  973. for (let i = 0, n = nodes.length; i < n; ++i) {
  974. let node = nodes[i];
  975. // hide nodes that match the strings
  976. let text = node.textContent;
  977. // if the text matches the words in aSearchString...
  978. if (this.stringMatchesFilters(text, searchString)) {
  979. node.classList.remove("filtered-by-string");
  980. } else {
  981. node.classList.add("filtered-by-string");
  982. }
  983. }
  984. this.resize();
  985. },
  986. /**
  987. * Applies the user's filters to a newly-created message node via CSS
  988. * classes.
  989. *
  990. * @param nsIDOMNode node
  991. * The newly-created message node.
  992. * @return boolean
  993. * True if the message was filtered or false otherwise.
  994. */
  995. filterMessageNode: function (node) {
  996. let isFiltered = false;
  997. // Filter by the message type.
  998. let prefKey = MESSAGE_PREFERENCE_KEYS[node.category][node.severity];
  999. if (prefKey && !this.getFilterState(prefKey)) {
  1000. // The node is filtered by type.
  1001. node.classList.add("filtered-by-type");
  1002. isFiltered = true;
  1003. }
  1004. // Filter by worker type
  1005. if ("workerType" in node && !this.getFilterState(node.workerType)) {
  1006. node.classList.add("filtered-by-type");
  1007. isFiltered = true;
  1008. }
  1009. // Filter on the search string.
  1010. let search = this.filterBox.value;
  1011. let text = node.clipboardText;
  1012. // if string matches the filter text
  1013. if (!this.stringMatchesFilters(text, search)) {
  1014. node.classList.add("filtered-by-string");
  1015. isFiltered = true;
  1016. }
  1017. if (isFiltered && node.classList.contains("inlined-variables-view")) {
  1018. node.classList.add("hidden-message");
  1019. }
  1020. return isFiltered;
  1021. },
  1022. /**
  1023. * Merge the attributes of repeated nodes.
  1024. *
  1025. * @param nsIDOMNode original
  1026. * The Original Node. The one being merged into.
  1027. */
  1028. mergeFilteredMessageNode: function (original) {
  1029. let repeatNode = original.getElementsByClassName("message-repeats")[0];
  1030. if (!repeatNode) {
  1031. // no repeat node, return early.
  1032. return;
  1033. }
  1034. let occurrences = parseInt(repeatNode.getAttribute("value"), 10) + 1;
  1035. repeatNode.setAttribute("value", occurrences);
  1036. repeatNode.textContent = occurrences;
  1037. let str = l10n.getStr("messageRepeats.tooltip2");
  1038. repeatNode.title = PluralForm.get(occurrences, str)
  1039. .replace("#1", occurrences);
  1040. },
  1041. /**
  1042. * Filter the message node from the output if it is a repeat.
  1043. *
  1044. * @private
  1045. * @param nsIDOMNode node
  1046. * The message node to be filtered or not.
  1047. * @returns nsIDOMNode|null
  1048. * Returns the duplicate node if the message was filtered, null
  1049. * otherwise.
  1050. */
  1051. _filterRepeatedMessage: function (node) {
  1052. let repeatNode = node.getElementsByClassName("message-repeats")[0];
  1053. if (!repeatNode) {
  1054. return null;
  1055. }
  1056. let uid = repeatNode._uid;
  1057. let dupeNode = null;
  1058. if (node.category == CATEGORY_CSS ||
  1059. node.category == CATEGORY_SECURITY) {
  1060. dupeNode = this._repeatNodes[uid];
  1061. if (!dupeNode) {
  1062. this._repeatNodes[uid] = node;
  1063. }
  1064. } else if ((node.category == CATEGORY_WEBDEV ||
  1065. node.category == CATEGORY_JS) &&
  1066. node.category != CATEGORY_NETWORK &&
  1067. !node.classList.contains("inlined-variables-view")) {
  1068. let lastMessage = this.outputNode.lastChild;
  1069. if (!lastMessage) {
  1070. return null;
  1071. }
  1072. let lastRepeatNode =
  1073. lastMessage.getElementsByClassName("message-repeats")[0];
  1074. if (lastRepeatNode && lastRepeatNode._uid == uid) {
  1075. dupeNode = lastMessage;
  1076. }
  1077. }
  1078. if (dupeNode) {
  1079. this.mergeFilteredMessageNode(dupeNode);
  1080. // Even though this node was never rendered, we create the location
  1081. // nodes before rendering, so we still have to clean up any
  1082. // React components
  1083. this.unmountMessage(node);
  1084. return dupeNode;
  1085. }
  1086. return null;
  1087. },
  1088. /**
  1089. * Display cached messages that may have been collected before the UI is
  1090. * displayed.
  1091. *
  1092. * @param array remoteMessages
  1093. * Array of cached messages coming from the remote Web Console
  1094. * content instance.
  1095. */
  1096. displayCachedMessages: function (remoteMessages) {
  1097. if (!remoteMessages.length) {
  1098. return;
  1099. }
  1100. remoteMessages.forEach(function (message) {
  1101. switch (message._type) {
  1102. case "PageError": {
  1103. let category = Utils.categoryForScriptError(message);
  1104. this.outputMessage(category, this.reportPageError,
  1105. [category, message]);
  1106. break;
  1107. }
  1108. case "LogMessage":
  1109. this.handleLogMessage(message);
  1110. break;
  1111. case "ConsoleAPI":
  1112. this.outputMessage(CATEGORY_WEBDEV, this.logConsoleAPIMessage,
  1113. [message]);
  1114. break;
  1115. case "NetworkEvent":
  1116. this.outputMessage(CATEGORY_NETWORK, this.logNetEvent, [message]);
  1117. break;
  1118. }
  1119. }, this);
  1120. },
  1121. /**
  1122. * Logs a message to the Web Console that originates from the Web Console
  1123. * server.
  1124. *
  1125. * @param object message
  1126. * The message received from the server.
  1127. * @return nsIDOMElement|null
  1128. * The message element to display in the Web Console output.
  1129. */
  1130. logConsoleAPIMessage: function (message) {
  1131. let body = null;
  1132. let clipboardText = null;
  1133. let sourceURL = message.filename;
  1134. let sourceLine = message.lineNumber;
  1135. let level = message.level;
  1136. let args = message.arguments;
  1137. let objectActors = new Set();
  1138. let node = null;
  1139. // Gather the actor IDs.
  1140. args.forEach((value) => {
  1141. if (WebConsoleUtils.isActorGrip(value)) {
  1142. objectActors.add(value.actor);
  1143. }
  1144. });
  1145. switch (level) {
  1146. case "log":
  1147. case "info":
  1148. case "warn":
  1149. case "error":
  1150. case "exception":
  1151. case "assert":
  1152. case "debug": {
  1153. let msg = new Messages.ConsoleGeneric(message);
  1154. node = msg.init(this.output).render().element;
  1155. break;
  1156. }
  1157. case "table": {
  1158. let msg = new Messages.ConsoleTable(message);
  1159. node = msg.init(this.output).render().element;
  1160. break;
  1161. }
  1162. case "trace": {
  1163. let msg = new Messages.ConsoleTrace(message);
  1164. node = msg.init(this.output).render().element;
  1165. break;
  1166. }
  1167. case "clear": {
  1168. body = l10n.getStr("consoleCleared");
  1169. clipboardText = body;
  1170. break;
  1171. }
  1172. case "dir": {
  1173. body = { arguments: args };
  1174. let clipboardArray = [];
  1175. args.forEach((value) => {
  1176. clipboardArray.push(VariablesView.getString(value));
  1177. });
  1178. clipboardText = clipboardArray.join(" ");
  1179. break;
  1180. }
  1181. case "dirxml": {
  1182. // We just alias console.dirxml() with console.log().
  1183. message.level = "log";
  1184. return this.logConsoleAPIMessage(message);
  1185. }
  1186. case "group":
  1187. case "groupCollapsed":
  1188. clipboardText = body = message.groupName;
  1189. this.groupDepth++;
  1190. break;
  1191. case "groupEnd":
  1192. if (this.groupDepth > 0) {
  1193. this.groupDepth--;
  1194. }
  1195. break;
  1196. case "time": {
  1197. let timer = message.timer;
  1198. if (!timer) {
  1199. return null;
  1200. }
  1201. if (timer.error) {
  1202. console.error(new Error(l10n.getStr(timer.error)));
  1203. return null;
  1204. }
  1205. body = l10n.getFormatStr("timerStarted", [timer.name]);
  1206. clipboardText = body;
  1207. break;
  1208. }
  1209. case "timeEnd": {
  1210. let timer = message.timer;
  1211. if (!timer) {
  1212. return null;
  1213. }
  1214. let duration = Math.round(timer.duration * 100) / 100;
  1215. body = l10n.getFormatStr("timeEnd", [timer.name, duration]);
  1216. clipboardText = body;
  1217. break;
  1218. }
  1219. case "count": {
  1220. let counter = message.counter;
  1221. if (!counter) {
  1222. return null;
  1223. }
  1224. if (counter.error) {
  1225. console.error(l10n.getStr(counter.error));
  1226. return null;
  1227. }
  1228. let msg = new Messages.ConsoleGeneric(message);
  1229. node = msg.init(this.output).render().element;
  1230. break;
  1231. }
  1232. case "timeStamp": {
  1233. // console.timeStamp() doesn't need to display anything.
  1234. return null;
  1235. }
  1236. default:
  1237. console.error(new Error("Unknown Console API log level: " + level));
  1238. return null;
  1239. }
  1240. // Release object actors for arguments coming from console API methods that
  1241. // we ignore their arguments.
  1242. switch (level) {
  1243. case "group":
  1244. case "groupCollapsed":
  1245. case "groupEnd":
  1246. case "time":
  1247. case "timeEnd":
  1248. case "count":
  1249. for (let actor of objectActors) {
  1250. this._releaseObject(actor);
  1251. }
  1252. objectActors.clear();
  1253. }
  1254. if (level == "groupEnd") {
  1255. // no need to continue
  1256. return null;
  1257. }
  1258. if (!node) {
  1259. node = this.createMessageNode(CATEGORY_WEBDEV, LEVELS[level], body,
  1260. sourceURL, sourceLine, clipboardText,
  1261. level, message.timeStamp);
  1262. if (message.private) {
  1263. node.setAttribute("private", true);
  1264. }
  1265. }
  1266. if (objectActors.size > 0) {
  1267. node._objectActors = objectActors;
  1268. if (!node._messageObject) {
  1269. let repeatNode = node.getElementsByClassName("message-repeats")[0];
  1270. repeatNode._uid += [...objectActors].join("-");
  1271. }
  1272. }
  1273. let workerTypeID = CONSOLE_WORKER_IDS.indexOf(message.workerType);
  1274. if (workerTypeID != -1) {
  1275. node.workerType = WORKERTYPES_PREFKEYS[workerTypeID];
  1276. node.setAttribute("workerType", WORKERTYPES_PREFKEYS[workerTypeID]);
  1277. }
  1278. return node;
  1279. },
  1280. /**
  1281. * Handle ConsoleAPICall objects received from the server. This method outputs
  1282. * the window.console API call.
  1283. *
  1284. * @param object message
  1285. * The console API message received from the server.
  1286. */
  1287. handleConsoleAPICall: function (message) {
  1288. this.outputMessage(CATEGORY_WEBDEV, this.logConsoleAPIMessage, [message]);
  1289. },
  1290. /**
  1291. * Reports an error in the page source, either JavaScript or CSS.
  1292. *
  1293. * @param nsIScriptError scriptError
  1294. * The error message to report.
  1295. * @return nsIDOMElement|undefined
  1296. * The message element to display in the Web Console output.
  1297. */
  1298. reportPageError: function (category, scriptError) {
  1299. // Warnings and legacy strict errors become warnings; other types become
  1300. // errors.
  1301. let severity = "error";
  1302. if (scriptError.warning || scriptError.strict) {
  1303. severity = "warning";
  1304. } else if (scriptError.info) {
  1305. severity = "log";
  1306. }
  1307. switch (category) {
  1308. case CATEGORY_CSS:
  1309. category = "css";
  1310. break;
  1311. case CATEGORY_SECURITY:
  1312. category = "security";
  1313. break;
  1314. default:
  1315. category = "js";
  1316. break;
  1317. }
  1318. let objectActors = new Set();
  1319. // Gather the actor IDs.
  1320. for (let prop of ["errorMessage", "lineText"]) {
  1321. let grip = scriptError[prop];
  1322. if (WebConsoleUtils.isActorGrip(grip)) {
  1323. objectActors.add(grip.actor);
  1324. }
  1325. }
  1326. let errorMessage = scriptError.errorMessage;
  1327. if (errorMessage.type && errorMessage.type == "longString") {
  1328. errorMessage = errorMessage.initial;
  1329. }
  1330. let displayOrigin = scriptError.sourceName;
  1331. // TLS errors are related to the connection and not the resource; therefore
  1332. // it makes sense to only display the protcol, host and port (prePath).
  1333. // This also means messages are grouped for a single origin.
  1334. if (scriptError.category && scriptError.category == "SHA-1 Signature") {
  1335. let sourceURI = Services.io.newURI(scriptError.sourceName, null, null)
  1336. .QueryInterface(Ci.nsIURL);
  1337. displayOrigin = sourceURI.prePath;
  1338. }
  1339. // Create a new message
  1340. let msg = new Messages.Simple(errorMessage, {
  1341. location: {
  1342. url: displayOrigin,
  1343. line: scriptError.lineNumber,
  1344. column: scriptError.columnNumber
  1345. },
  1346. stack: scriptError.stacktrace,
  1347. category: category,
  1348. severity: severity,
  1349. timestamp: scriptError.timeStamp,
  1350. private: scriptError.private,
  1351. filterDuplicates: true
  1352. });
  1353. let node = msg.init(this.output).render().element;
  1354. // Select the body of the message node that is displayed in the console
  1355. let msgBody = node.getElementsByClassName("message-body")[0];
  1356. // Add the more info link node to messages that belong to certain categories
  1357. if (scriptError.exceptionDocURL) {
  1358. this.addLearnMoreWarningNode(msgBody, scriptError.exceptionDocURL);
  1359. }
  1360. // Collect telemetry data regarding JavaScript errors
  1361. this._telemetry.logKeyed("DEVTOOLS_JAVASCRIPT_ERROR_DISPLAYED",
  1362. scriptError.errorMessageName,
  1363. true);
  1364. if (objectActors.size > 0) {
  1365. node._objectActors = objectActors;
  1366. }
  1367. return node;
  1368. },
  1369. /**
  1370. * Handle PageError objects received from the server. This method outputs the
  1371. * given error.
  1372. *
  1373. * @param nsIScriptError pageError
  1374. * The error received from the server.
  1375. */
  1376. handlePageError: function (pageError) {
  1377. let category = Utils.categoryForScriptError(pageError);
  1378. this.outputMessage(category, this.reportPageError, [category, pageError]);
  1379. },
  1380. /**
  1381. * Handle log messages received from the server. This method outputs the given
  1382. * message.
  1383. *
  1384. * @param object packet
  1385. * The message packet received from the server.
  1386. */
  1387. handleLogMessage: function (packet) {
  1388. if (packet.message) {
  1389. this.outputMessage(CATEGORY_JS, this._reportLogMessage, [packet]);
  1390. }
  1391. },
  1392. /**
  1393. * Display log messages received from the server.
  1394. *
  1395. * @private
  1396. * @param object packet
  1397. * The message packet received from the server.
  1398. * @return nsIDOMElement
  1399. * The message element to render for the given log message.
  1400. */
  1401. _reportLogMessage: function (packet) {
  1402. let msg = packet.message;
  1403. if (msg.type && msg.type == "longString") {
  1404. msg = msg.initial;
  1405. }
  1406. let node = this.createMessageNode(CATEGORY_JS, SEVERITY_LOG, msg, null,
  1407. null, null, null, packet.timeStamp);
  1408. if (WebConsoleUtils.isActorGrip(packet.message)) {
  1409. node._objectActors = new Set([packet.message.actor]);
  1410. }
  1411. return node;
  1412. },
  1413. /**
  1414. * Log network event.
  1415. *
  1416. * @param object networkInfo
  1417. * The network request information to log.
  1418. * @return nsIDOMElement|null
  1419. * The message element to display in the Web Console output.
  1420. */
  1421. logNetEvent: function (networkInfo) {
  1422. let actorId = networkInfo.actor;
  1423. let request = networkInfo.request;
  1424. let clipboardText = request.method + " " + request.url;
  1425. let severity = SEVERITY_LOG;
  1426. if (networkInfo.isXHR) {
  1427. clipboardText = request.method + " XHR " + request.url;
  1428. severity = SEVERITY_INFO;
  1429. }
  1430. let mixedRequest =
  1431. WebConsoleUtils.isMixedHTTPSRequest(request.url, this.contentLocation);
  1432. if (mixedRequest) {
  1433. severity = SEVERITY_WARNING;
  1434. }
  1435. let methodNode = this.document.createElementNS(XHTML_NS, "span");
  1436. methodNode.className = "method";
  1437. methodNode.textContent = request.method + " ";
  1438. let messageNode = this.createMessageNode(CATEGORY_NETWORK, severity,
  1439. methodNode, null, null,
  1440. clipboardText, null,
  1441. networkInfo.timeStamp);
  1442. if (networkInfo.private) {
  1443. messageNode.setAttribute("private", true);
  1444. }
  1445. messageNode._connectionId = actorId;
  1446. messageNode.url = request.url;
  1447. let body = methodNode.parentNode;
  1448. body.setAttribute("aria-haspopup", true);
  1449. if (networkInfo.isXHR) {
  1450. let xhrNode = this.document.createElementNS(XHTML_NS, "span");
  1451. xhrNode.className = "xhr";
  1452. xhrNode.textContent = l10n.getStr("webConsoleXhrIndicator");
  1453. body.appendChild(xhrNode);
  1454. body.appendChild(this.document.createTextNode(" "));
  1455. }
  1456. let displayUrl = request.url;
  1457. let pos = displayUrl.indexOf("?");
  1458. if (pos > -1) {
  1459. displayUrl = displayUrl.substr(0, pos);
  1460. }
  1461. let urlNode = this.document.createElementNS(XHTML_NS, "a");
  1462. urlNode.className = "url";
  1463. urlNode.setAttribute("title", request.url);
  1464. urlNode.href = request.url;
  1465. urlNode.textContent = displayUrl;
  1466. urlNode.draggable = false;
  1467. body.appendChild(urlNode);
  1468. body.appendChild(this.document.createTextNode(" "));
  1469. if (mixedRequest) {
  1470. messageNode.classList.add("mixed-content");
  1471. this.makeMixedContentNode(body);
  1472. }
  1473. let statusNode = this.document.createElementNS(XHTML_NS, "a");
  1474. statusNode.className = "status";
  1475. body.appendChild(statusNode);
  1476. let onClick = () => this.openNetworkPanel(networkInfo.actor);
  1477. this._addMessageLinkCallback(urlNode, onClick);
  1478. this._addMessageLinkCallback(statusNode, onClick);
  1479. networkInfo.node = messageNode;
  1480. this._updateNetMessage(actorId);
  1481. if (this.window.NetRequest) {
  1482. this.window.NetRequest.onNetworkEvent({
  1483. consoleFrame: this,
  1484. response: networkInfo,
  1485. node: messageNode,
  1486. update: false
  1487. });
  1488. }
  1489. return messageNode;
  1490. },
  1491. /**
  1492. * Create a mixed content warning Node.
  1493. *
  1494. * @param linkNode
  1495. * Parent to the requested urlNode.
  1496. */
  1497. makeMixedContentNode: function (linkNode) {
  1498. let mixedContentWarning =
  1499. "[" + l10n.getStr("webConsoleMixedContentWarning") + "]";
  1500. // Mixed content warning message links to a Learn More page
  1501. let mixedContentWarningNode = this.document.createElementNS(XHTML_NS, "a");
  1502. mixedContentWarningNode.title = MIXED_CONTENT_LEARN_MORE;
  1503. mixedContentWarningNode.href = MIXED_CONTENT_LEARN_MORE;
  1504. mixedContentWarningNode.className = "learn-more-link";
  1505. mixedContentWarningNode.textContent = mixedContentWarning;
  1506. mixedContentWarningNode.draggable = false;
  1507. linkNode.appendChild(mixedContentWarningNode);
  1508. this._addMessageLinkCallback(mixedContentWarningNode, (event) => {
  1509. event.stopPropagation();
  1510. this.owner.openLink(MIXED_CONTENT_LEARN_MORE);
  1511. });
  1512. },
  1513. /*
  1514. * Appends a clickable warning node to the node passed
  1515. * as a parameter to the function. When a user clicks on the appended
  1516. * warning node, the browser navigates to the provided url.
  1517. *
  1518. * @param node
  1519. * The node to which we will be adding a clickable warning node.
  1520. * @param url
  1521. * The url which points to the page where the user can learn more
  1522. * about security issues associated with the specific message that's
  1523. * being logged.
  1524. */
  1525. addLearnMoreWarningNode: function (node, url) {
  1526. let moreInfoLabel = "[" + l10n.getStr("webConsoleMoreInfoLabel") + "]";
  1527. let warningNode = this.document.createElementNS(XHTML_NS, "a");
  1528. warningNode.title = url.split("?")[0];
  1529. warningNode.href = url;
  1530. warningNode.draggable = false;
  1531. warningNode.textContent = moreInfoLabel;
  1532. warningNode.className = "learn-more-link";
  1533. this._addMessageLinkCallback(warningNode, (event) => {
  1534. event.stopPropagation();
  1535. this.owner.openLink(url);
  1536. });
  1537. node.appendChild(warningNode);
  1538. },
  1539. /**
  1540. * Log file activity.
  1541. *
  1542. * @param string fileURI
  1543. * The file URI that was loaded.
  1544. * @return nsIDOMElement|undefined
  1545. * The message element to display in the Web Console output.
  1546. */
  1547. logFileActivity: function (fileURI) {
  1548. let urlNode = this.document.createElementNS(XHTML_NS, "a");
  1549. urlNode.setAttribute("title", fileURI);
  1550. urlNode.className = "url";
  1551. urlNode.textContent = fileURI;
  1552. urlNode.draggable = false;
  1553. urlNode.href = fileURI;
  1554. let outputNode = this.createMessageNode(CATEGORY_NETWORK, SEVERITY_LOG,
  1555. urlNode, null, null, fileURI);
  1556. this._addMessageLinkCallback(urlNode, () => {
  1557. this.owner.viewSource(fileURI);
  1558. });
  1559. return outputNode;
  1560. },
  1561. /**
  1562. * Handle the file activity messages coming from the remote Web Console.
  1563. *
  1564. * @param string fileURI
  1565. * The file URI that was requested.
  1566. */
  1567. handleFileActivity: function (fileURI) {
  1568. this.outputMessage(CATEGORY_NETWORK, this.logFileActivity, [fileURI]);
  1569. },
  1570. /**
  1571. * Handle the reflow activity messages coming from the remote Web Console.
  1572. *
  1573. * @param object msg
  1574. * An object holding information about a reflow batch.
  1575. */
  1576. logReflowActivity: function (message) {
  1577. let {start, end, sourceURL, sourceLine} = message;
  1578. let duration = Math.round((end - start) * 100) / 100;
  1579. let node = this.document.createElementNS(XHTML_NS, "span");
  1580. if (sourceURL) {
  1581. node.textContent =
  1582. l10n.getFormatStr("reflow.messageWithLink", [duration]);
  1583. let a = this.document.createElementNS(XHTML_NS, "a");
  1584. a.href = "#";
  1585. a.draggable = "false";
  1586. let filename = getSourceNames(sourceURL).short;
  1587. let functionName = message.functionName ||
  1588. l10n.getStr("stacktrace.anonymousFunction");
  1589. a.textContent = l10n.getFormatStr("reflow.messageLinkText",
  1590. [functionName, filename, sourceLine]);
  1591. this._addMessageLinkCallback(a, () => {
  1592. this.owner.viewSourceInDebugger(sourceURL, sourceLine);
  1593. });
  1594. node.appendChild(a);
  1595. } else {
  1596. node.textContent =
  1597. l10n.getFormatStr("reflow.messageWithNoLink", [duration]);
  1598. }
  1599. return this.createMessageNode(CATEGORY_CSS, SEVERITY_LOG, node);
  1600. },
  1601. handleReflowActivity: function (message) {
  1602. this.outputMessage(CATEGORY_CSS, this.logReflowActivity, [message]);
  1603. },
  1604. /**
  1605. * Inform user that the window.console API has been replaced by a script
  1606. * in a content page.
  1607. */
  1608. logWarningAboutReplacedAPI: function () {
  1609. let node = this.createMessageNode(CATEGORY_JS, SEVERITY_WARNING,
  1610. l10n.getStr("ConsoleAPIDisabled"));
  1611. this.outputMessage(CATEGORY_JS, node);
  1612. },
  1613. /**
  1614. * Handle the network events coming from the remote Web Console.
  1615. *
  1616. * @param object networkInfo
  1617. * The network request information.
  1618. */
  1619. handleNetworkEvent: function (networkInfo) {
  1620. this.outputMessage(CATEGORY_NETWORK, this.logNetEvent, [networkInfo]);
  1621. },
  1622. /**
  1623. * Handle network event updates coming from the server.
  1624. *
  1625. * @param object networkInfo
  1626. * The network request information.
  1627. * @param object packet
  1628. * Update details.
  1629. */
  1630. handleNetworkEventUpdate: function (networkInfo, packet) {
  1631. if (networkInfo.node && this._updateNetMessage(packet.from)) {
  1632. if (this.window.NetRequest) {
  1633. this.window.NetRequest.onNetworkEvent({
  1634. client: this.webConsoleClient,
  1635. response: packet,
  1636. node: networkInfo.node,
  1637. update: true
  1638. });
  1639. }
  1640. this.emit("new-messages", new Set([{
  1641. update: true,
  1642. node: networkInfo.node,
  1643. response: packet,
  1644. }]));
  1645. }
  1646. // For unit tests we pass the HTTP activity object to the test callback,
  1647. // once requests complete.
  1648. if (this.owner.lastFinishedRequestCallback &&
  1649. networkInfo.updates.indexOf("responseContent") > -1 &&
  1650. networkInfo.updates.indexOf("eventTimings") > -1) {
  1651. this.owner.lastFinishedRequestCallback(networkInfo, this);
  1652. }
  1653. },
  1654. /**
  1655. * Update an output message to reflect the latest state of a network request,
  1656. * given a network event actor ID.
  1657. *
  1658. * @private
  1659. * @param string actorId
  1660. * The network event actor ID for which you want to update the message.
  1661. * @return boolean
  1662. * |true| if the message node was updated, or |false| otherwise.
  1663. */
  1664. _updateNetMessage: function (actorId) {
  1665. let networkInfo = this.webConsoleClient.getNetworkRequest(actorId);
  1666. if (!networkInfo || !networkInfo.node) {
  1667. return false;
  1668. }
  1669. let messageNode = networkInfo.node;
  1670. let updates = networkInfo.updates;
  1671. let hasEventTimings = updates.indexOf("eventTimings") > -1;
  1672. let hasResponseStart = updates.indexOf("responseStart") > -1;
  1673. let request = networkInfo.request;
  1674. let methodText = (networkInfo.isXHR) ?
  1675. request.method + " XHR" : request.method;
  1676. let response = networkInfo.response;
  1677. let updated = false;
  1678. if (hasEventTimings || hasResponseStart) {
  1679. let status = [];
  1680. if (response.httpVersion && response.status) {
  1681. status = [response.httpVersion, response.status, response.statusText];
  1682. }
  1683. if (hasEventTimings) {
  1684. status.push(l10n.getFormatStr("NetworkPanel.durationMS",
  1685. [networkInfo.totalTime]));
  1686. }
  1687. let statusText = "[" + status.join(" ") + "]";
  1688. let statusNode = messageNode.getElementsByClassName("status")[0];
  1689. statusNode.textContent = statusText;
  1690. messageNode.clipboardText = [methodText, request.url, statusText]
  1691. .join(" ");
  1692. if (hasResponseStart && response.status >= MIN_HTTP_ERROR_CODE &&
  1693. response.status <= MAX_HTTP_ERROR_CODE) {
  1694. this.setMessageType(messageNode, CATEGORY_NETWORK, SEVERITY_ERROR);
  1695. }
  1696. updated = true;
  1697. }
  1698. if (messageNode._netPanel) {
  1699. messageNode._netPanel.update();
  1700. }
  1701. return updated;
  1702. },
  1703. /**
  1704. * Opens the network monitor and highlights the specified request.
  1705. *
  1706. * @param string requestId
  1707. * The actor ID of the network request.
  1708. */
  1709. openNetworkPanel: function (requestId) {
  1710. let toolbox = gDevTools.getToolbox(this.owner.target);
  1711. // The browser console doesn't have a toolbox.
  1712. if (!toolbox) {
  1713. return;
  1714. }
  1715. return toolbox.selectTool("netmonitor").then(panel => {
  1716. return panel.panelWin.NetMonitorController.inspectRequest(requestId);
  1717. });
  1718. },
  1719. /**
  1720. * Handler for page location changes.
  1721. *
  1722. * @param string uri
  1723. * New page location.
  1724. * @param string title
  1725. * New page title.
  1726. */
  1727. onLocationChange: function (uri, title) {
  1728. this.contentLocation = uri;
  1729. if (this.owner.onLocationChange) {
  1730. this.owner.onLocationChange(uri, title);
  1731. }
  1732. },
  1733. /**
  1734. * Handler for the tabNavigated notification.
  1735. *
  1736. * @param string event
  1737. * Event name.
  1738. * @param object packet
  1739. * Notification packet received from the server.
  1740. */
  1741. handleTabNavigated: function (event, packet) {
  1742. if (event == "will-navigate") {
  1743. if (this.persistLog) {
  1744. if (this.NEW_CONSOLE_OUTPUT_ENABLED) {
  1745. // Add a _type to hit convertCachedPacket.
  1746. packet._type = true;
  1747. this.newConsoleOutput.dispatchMessageAdd(packet);
  1748. } else {
  1749. let marker = new Messages.NavigationMarker(packet, Date.now());
  1750. this.output.addMessage(marker);
  1751. }
  1752. } else {
  1753. this.jsterm.clearOutput();
  1754. }
  1755. }
  1756. if (packet.url) {
  1757. this.onLocationChange(packet.url, packet.title);
  1758. }
  1759. if (event == "navigate" && !packet.nativeConsoleAPI) {
  1760. this.logWarningAboutReplacedAPI();
  1761. }
  1762. },
  1763. /**
  1764. * Output a message node. This filters a node appropriately, then sends it to
  1765. * the output, regrouping and pruning output as necessary.
  1766. *
  1767. * Note: this call is async - the given message node may not be displayed when
  1768. * you call this method.
  1769. *
  1770. * @param integer category
  1771. * The category of the message you want to output. See the CATEGORY_*
  1772. * constants.
  1773. * @param function|nsIDOMElement methodOrNode
  1774. * The method that creates the message element to send to the output or
  1775. * the actual element. If a method is given it will be bound to the HUD
  1776. * object and the arguments will be |args|.
  1777. * @param array [args]
  1778. * If a method is given to output the message element then the method
  1779. * will be invoked with the list of arguments given here. The last
  1780. * object in this array should be the packet received from the
  1781. * back end.
  1782. */
  1783. outputMessage: function (category, methodOrNode, args) {
  1784. if (!this._outputQueue.length) {
  1785. // If the queue is empty we consider that now was the last output flush.
  1786. // This avoid an immediate output flush when the timer executes.
  1787. this._lastOutputFlush = Date.now();
  1788. }
  1789. this._outputQueue.push([category, methodOrNode, args]);
  1790. this._initOutputTimer();
  1791. },
  1792. /**
  1793. * Try to flush the output message queue. This takes the messages in the
  1794. * output queue and displays them. Outputting stops at MESSAGES_IN_INTERVAL.
  1795. * Further output is queued to happen later - see OUTPUT_INTERVAL.
  1796. *
  1797. * @private
  1798. */
  1799. _flushMessageQueue: function () {
  1800. this._outputTimerInitialized = false;
  1801. if (!this._outputTimer) {
  1802. return;
  1803. }
  1804. let startTime = Date.now();
  1805. let timeSinceFlush = startTime - this._lastOutputFlush;
  1806. let shouldThrottle = this._outputQueue.length > MESSAGES_IN_INTERVAL &&
  1807. timeSinceFlush < THROTTLE_UPDATES;
  1808. // Determine how many messages we can display now.
  1809. let toDisplay = Math.min(this._outputQueue.length, MESSAGES_IN_INTERVAL);
  1810. // If there aren't any messages to display (because of throttling or an
  1811. // empty queue), then take care of some cleanup. Destroy items that were
  1812. // pruned from the outputQueue before being displayed.
  1813. if (shouldThrottle || toDisplay < 1) {
  1814. while (this._itemDestroyQueue.length) {
  1815. if ((Date.now() - startTime) > MAX_CLEANUP_TIME) {
  1816. break;
  1817. }
  1818. this._destroyItem(this._itemDestroyQueue.pop());
  1819. }
  1820. this._initOutputTimer();
  1821. return;
  1822. }
  1823. // Try to prune the message queue.
  1824. let shouldPrune = false;
  1825. if (this._outputQueue.length > toDisplay && this._pruneOutputQueue()) {
  1826. toDisplay = Math.min(this._outputQueue.length, toDisplay);
  1827. shouldPrune = true;
  1828. }
  1829. let batch = this._outputQueue.splice(0, toDisplay);
  1830. let outputNode = this.outputNode;
  1831. let lastVisibleNode = null;
  1832. let scrollNode = this.outputWrapper;
  1833. let hudIdSupportsString = WebConsoleUtils.supportsString(this.hudId);
  1834. // We won't bother to try to restore scroll position if this is showing
  1835. // a lot of messages at once (and there are still items in the queue).
  1836. // It is going to purge whatever you were looking at anyway.
  1837. let scrolledToBottom =
  1838. shouldPrune || Utils.isOutputScrolledToBottom(outputNode, scrollNode);
  1839. // Output the current batch of messages.
  1840. let messages = new Set();
  1841. for (let i = 0; i < batch.length; i++) {
  1842. let item = batch[i];
  1843. let result = this._outputMessageFromQueue(hudIdSupportsString, item);
  1844. if (result) {
  1845. messages.add({
  1846. node: result.isRepeated ? result.isRepeated : result.node,
  1847. response: result.message,
  1848. update: !!result.isRepeated,
  1849. });
  1850. if (result.visible && result.node == this.outputNode.lastChild) {
  1851. lastVisibleNode = result.node;
  1852. }
  1853. }
  1854. }
  1855. let oldScrollHeight = 0;
  1856. let removedNodes = 0;
  1857. // Prune messages from the DOM, but only if needed.
  1858. if (shouldPrune || !this._outputQueue.length) {
  1859. // Only bother measuring the scrollHeight if not scrolled to bottom,
  1860. // since the oldScrollHeight will not be used if it is.
  1861. if (!scrolledToBottom) {
  1862. oldScrollHeight = scrollNode.scrollHeight;
  1863. }
  1864. let categories = Object.keys(this._pruneCategoriesQueue);
  1865. categories.forEach(function _pruneOutput(category) {
  1866. removedNodes += this.pruneOutputIfNecessary(category);
  1867. }, this);
  1868. this._pruneCategoriesQueue = {};
  1869. }
  1870. let isInputOutput = lastVisibleNode &&
  1871. (lastVisibleNode.category == CATEGORY_INPUT ||
  1872. lastVisibleNode.category == CATEGORY_OUTPUT);
  1873. // Scroll to the new node if it is not filtered, and if the output node is
  1874. // scrolled at the bottom or if the new node is a jsterm input/output
  1875. // message.
  1876. if (lastVisibleNode && (scrolledToBottom || isInputOutput)) {
  1877. Utils.scrollToVisible(lastVisibleNode);
  1878. } else if (!scrolledToBottom && removedNodes > 0 &&
  1879. oldScrollHeight != scrollNode.scrollHeight) {
  1880. // If there were pruned messages and if scroll is not at the bottom, then
  1881. // we need to adjust the scroll location.
  1882. scrollNode.scrollTop -= oldScrollHeight - scrollNode.scrollHeight;
  1883. }
  1884. if (messages.size) {
  1885. this.emit("new-messages", messages);
  1886. }
  1887. // If the output queue is empty, then run _flushCallback.
  1888. if (this._outputQueue.length === 0 && this._flushCallback) {
  1889. if (this._flushCallback() === false) {
  1890. this._flushCallback = null;
  1891. }
  1892. }
  1893. this._initOutputTimer();
  1894. // Resize the output area in case a vertical scrollbar has been added
  1895. this.resize();
  1896. this._lastOutputFlush = Date.now();
  1897. },
  1898. /**
  1899. * Initialize the output timer.
  1900. * @private
  1901. */
  1902. _initOutputTimer: function () {
  1903. let panelIsDestroyed = !this._outputTimer;
  1904. let alreadyScheduled = this._outputTimerInitialized;
  1905. let nothingToDo = !this._itemDestroyQueue.length &&
  1906. !this._outputQueue.length;
  1907. // Don't schedule a callback in the following cases:
  1908. if (panelIsDestroyed || alreadyScheduled || nothingToDo) {
  1909. return;
  1910. }
  1911. this._outputTimerInitialized = true;
  1912. this._outputTimer.initWithCallback(this._flushMessageQueue,
  1913. OUTPUT_INTERVAL,
  1914. Ci.nsITimer.TYPE_ONE_SHOT);
  1915. },
  1916. /**
  1917. * Output a message from the queue.
  1918. *
  1919. * @private
  1920. * @param nsISupportsString hudIdSupportsString
  1921. * The HUD ID as an nsISupportsString.
  1922. * @param array item
  1923. * An item from the output queue - this item represents a message.
  1924. * @return object
  1925. * An object that holds the following properties:
  1926. * - node: the DOM element of the message.
  1927. * - isRepeated: the DOM element of the original message, if this is
  1928. * a repeated message, otherwise null.
  1929. * - visible: boolean that tells if the message is visible.
  1930. */
  1931. _outputMessageFromQueue: function (hudIdSupportsString, item) {
  1932. let [, methodOrNode, args] = item;
  1933. // The last object in the args array should be message
  1934. // object or response packet received from the server.
  1935. let message = (args && args.length) ? args[args.length - 1] : null;
  1936. let node = typeof methodOrNode == "function" ?
  1937. methodOrNode.apply(this, args || []) :
  1938. methodOrNode;
  1939. if (!node) {
  1940. return null;
  1941. }
  1942. let isFiltered = this.filterMessageNode(node);
  1943. let isRepeated = this._filterRepeatedMessage(node);
  1944. // If a clear message is processed while the webconsole is opened, the UI
  1945. // should be cleared.
  1946. // Do not clear the output if the current frame is owned by a Browser Console.
  1947. if (message && message.level == "clear" && !this.isBrowserConsole) {
  1948. // Do not clear the consoleStorage here as it has been cleared already
  1949. // by the clear method, only clear the UI.
  1950. this.jsterm.clearOutput(false);
  1951. }
  1952. let visible = !isRepeated && !isFiltered;
  1953. if (!isRepeated) {
  1954. this.outputNode.appendChild(node);
  1955. this._pruneCategoriesQueue[node.category] = true;
  1956. let nodeID = node.getAttribute("id");
  1957. Services.obs.notifyObservers(hudIdSupportsString,
  1958. "web-console-message-created", nodeID);
  1959. }
  1960. if (node._onOutput) {
  1961. node._onOutput();
  1962. delete node._onOutput;
  1963. }
  1964. return {
  1965. visible: visible,
  1966. node: node,
  1967. isRepeated: isRepeated,
  1968. message: message
  1969. };
  1970. },
  1971. /**
  1972. * Prune the queue of messages to display. This avoids displaying messages
  1973. * that will be removed at the end of the queue anyway.
  1974. * @private
  1975. */
  1976. _pruneOutputQueue: function () {
  1977. let nodes = {};
  1978. // Group the messages per category.
  1979. this._outputQueue.forEach(function (item, index) {
  1980. let [category] = item;
  1981. if (!(category in nodes)) {
  1982. nodes[category] = [];
  1983. }
  1984. nodes[category].push(index);
  1985. }, this);
  1986. let pruned = 0;
  1987. // Loop through the categories we found and prune if needed.
  1988. for (let category in nodes) {
  1989. let limit = Utils.logLimitForCategory(category);
  1990. let indexes = nodes[category];
  1991. if (indexes.length > limit) {
  1992. let n = Math.max(0, indexes.length - limit);
  1993. pruned += n;
  1994. for (let i = n - 1; i >= 0; i--) {
  1995. this._itemDestroyQueue.push(this._outputQueue[indexes[i]]);
  1996. this._outputQueue.splice(indexes[i], 1);
  1997. }
  1998. }
  1999. }
  2000. return pruned;
  2001. },
  2002. /**
  2003. * Destroy an item that was once in the outputQueue but isn't needed
  2004. * after all.
  2005. *
  2006. * @private
  2007. * @param array item
  2008. * The item you want to destroy. Does not remove it from the output
  2009. * queue.
  2010. */
  2011. _destroyItem: function (item) {
  2012. // TODO: handle object releasing in a more elegant way once all console
  2013. // messages use the new API - bug 778766.
  2014. let [category, methodOrNode, args] = item;
  2015. if (typeof methodOrNode != "function" && methodOrNode._objectActors) {
  2016. for (let actor of methodOrNode._objectActors) {
  2017. this._releaseObject(actor);
  2018. }
  2019. methodOrNode._objectActors.clear();
  2020. }
  2021. if (methodOrNode == this.output._flushMessageQueue &&
  2022. args[0]._objectActors) {
  2023. for (let arg of args) {
  2024. if (!arg._objectActors) {
  2025. continue;
  2026. }
  2027. for (let actor of arg._objectActors) {
  2028. this._releaseObject(actor);
  2029. }
  2030. arg._objectActors.clear();
  2031. }
  2032. }
  2033. if (category == CATEGORY_NETWORK) {
  2034. let connectionId = null;
  2035. if (methodOrNode == this.logNetEvent) {
  2036. connectionId = args[0].actor;
  2037. } else if (typeof methodOrNode != "function") {
  2038. connectionId = methodOrNode._connectionId;
  2039. }
  2040. if (connectionId &&
  2041. this.webConsoleClient.hasNetworkRequest(connectionId)) {
  2042. this.webConsoleClient.removeNetworkRequest(connectionId);
  2043. this._releaseObject(connectionId);
  2044. }
  2045. } else if (category == CATEGORY_WEBDEV &&
  2046. methodOrNode == this.logConsoleAPIMessage) {
  2047. args[0].arguments.forEach((value) => {
  2048. if (WebConsoleUtils.isActorGrip(value)) {
  2049. this._releaseObject(value.actor);
  2050. }
  2051. });
  2052. } else if (category == CATEGORY_JS &&
  2053. methodOrNode == this.reportPageError) {
  2054. let pageError = args[1];
  2055. for (let prop of ["errorMessage", "lineText"]) {
  2056. let grip = pageError[prop];
  2057. if (WebConsoleUtils.isActorGrip(grip)) {
  2058. this._releaseObject(grip.actor);
  2059. }
  2060. }
  2061. } else if (category == CATEGORY_JS &&
  2062. methodOrNode == this._reportLogMessage) {
  2063. if (WebConsoleUtils.isActorGrip(args[0].message)) {
  2064. this._releaseObject(args[0].message.actor);
  2065. }
  2066. }
  2067. },
  2068. /**
  2069. * Cleans up a message via a node that may or may not
  2070. * have actually been rendered in the DOM. Currently, only
  2071. * cleans up React components.
  2072. *
  2073. * @param nsIDOMNode node
  2074. * The message node you want to clean up.
  2075. */
  2076. unmountMessage(node) {
  2077. // Unmount the Frame component with the message location
  2078. let locationNode = node.querySelector(".message-location");
  2079. if (locationNode) {
  2080. this.ReactDOM.unmountComponentAtNode(locationNode);
  2081. }
  2082. // Unmount the StackTrace component if present in the message
  2083. let stacktraceNode = node.querySelector(".stacktrace");
  2084. if (stacktraceNode) {
  2085. this.ReactDOM.unmountComponentAtNode(stacktraceNode);
  2086. }
  2087. },
  2088. /**
  2089. * Ensures that the number of message nodes of type category don't exceed that
  2090. * category's line limit by removing old messages as needed.
  2091. *
  2092. * @param integer category
  2093. * The category of message nodes to prune if needed.
  2094. * @return number
  2095. * The number of removed nodes.
  2096. */
  2097. pruneOutputIfNecessary: function (category) {
  2098. let logLimit = Utils.logLimitForCategory(category);
  2099. let messageNodes = this.outputNode.querySelectorAll(".message[category=" +
  2100. CATEGORY_CLASS_FRAGMENTS[category] + "]");
  2101. let n = Math.max(0, messageNodes.length - logLimit);
  2102. [...messageNodes].slice(0, n).forEach(this.removeOutputMessage, this);
  2103. return n;
  2104. },
  2105. /**
  2106. * Remove a given message from the output.
  2107. *
  2108. * @param nsIDOMNode node
  2109. * The message node you want to remove.
  2110. */
  2111. removeOutputMessage: function (node) {
  2112. if (node._messageObject) {
  2113. node._messageObject.destroy();
  2114. }
  2115. if (node._objectActors) {
  2116. for (let actor of node._objectActors) {
  2117. this._releaseObject(actor);
  2118. }
  2119. node._objectActors.clear();
  2120. }
  2121. if (node.category == CATEGORY_CSS ||
  2122. node.category == CATEGORY_SECURITY) {
  2123. let repeatNode = node.getElementsByClassName("message-repeats")[0];
  2124. if (repeatNode && repeatNode._uid) {
  2125. delete this._repeatNodes[repeatNode._uid];
  2126. }
  2127. } else if (node._connectionId &&
  2128. node.category == CATEGORY_NETWORK) {
  2129. this.webConsoleClient.removeNetworkRequest(node._connectionId);
  2130. this._releaseObject(node._connectionId);
  2131. } else if (node.classList.contains("inlined-variables-view")) {
  2132. let view = node._variablesView;
  2133. if (view) {
  2134. view.controller.releaseActors();
  2135. }
  2136. node._variablesView = null;
  2137. }
  2138. this.unmountMessage(node);
  2139. node.remove();
  2140. },
  2141. /**
  2142. * Given a category and message body, creates a DOM node to represent an
  2143. * incoming message. The timestamp is automatically added.
  2144. *
  2145. * @param number category
  2146. * The category of the message: one of the CATEGORY_* constants.
  2147. * @param number severity
  2148. * The severity of the message: one of the SEVERITY_* constants;
  2149. * @param string|nsIDOMNode body
  2150. * The body of the message, either a simple string or a DOM node.
  2151. * @param string sourceURL [optional]
  2152. * The URL of the source file that emitted the error.
  2153. * @param number sourceLine [optional]
  2154. * The line number on which the error occurred. If zero or omitted,
  2155. * there is no line number associated with this message.
  2156. * @param string clipboardText [optional]
  2157. * The text that should be copied to the clipboard when this node is
  2158. * copied. If omitted, defaults to the body text. If `body` is not
  2159. * a string, then the clipboard text must be supplied.
  2160. * @param number level [optional]
  2161. * The level of the console API message.
  2162. * @param number timestamp [optional]
  2163. * The timestamp to use for this message node. If omitted, the current
  2164. * date and time is used.
  2165. * @return nsIDOMNode
  2166. * The message node: a DIV ready to be inserted into the Web Console
  2167. * output node.
  2168. */
  2169. createMessageNode: function (category, severity, body, sourceURL, sourceLine,
  2170. clipboardText, level, timestamp) {
  2171. if (typeof body != "string" && clipboardText == null && body.innerText) {
  2172. clipboardText = body.innerText;
  2173. }
  2174. let indentNode = this.document.createElementNS(XHTML_NS, "span");
  2175. indentNode.className = "indent";
  2176. // Apply the current group by indenting appropriately.
  2177. let indent = this.groupDepth * GROUP_INDENT;
  2178. indentNode.style.width = indent + "px";
  2179. // Make the icon container, which is a vertical box. Its purpose is to
  2180. // ensure that the icon stays anchored at the top of the message even for
  2181. // long multi-line messages.
  2182. let iconContainer = this.document.createElementNS(XHTML_NS, "span");
  2183. iconContainer.className = "icon";
  2184. // Create the message body, which contains the actual text of the message.
  2185. let bodyNode = this.document.createElementNS(XHTML_NS, "span");
  2186. bodyNode.className = "message-body-wrapper message-body devtools-monospace";
  2187. // Store the body text, since it is needed later for the variables view.
  2188. let storedBody = body;
  2189. // If a string was supplied for the body, turn it into a DOM node and an
  2190. // associated clipboard string now.
  2191. clipboardText = clipboardText ||
  2192. (body + (sourceURL ? " @ " + sourceURL : "") +
  2193. (sourceLine ? ":" + sourceLine : ""));
  2194. timestamp = timestamp || Date.now();
  2195. // Create the containing node and append all its elements to it.
  2196. let node = this.document.createElementNS(XHTML_NS, "div");
  2197. node.id = "console-msg-" + gSequenceId();
  2198. node.className = "message";
  2199. node.clipboardText = clipboardText;
  2200. node.timestamp = timestamp;
  2201. this.setMessageType(node, category, severity);
  2202. if (body instanceof Ci.nsIDOMNode) {
  2203. bodyNode.appendChild(body);
  2204. } else {
  2205. let str = undefined;
  2206. if (level == "dir") {
  2207. str = VariablesView.getString(body.arguments[0]);
  2208. } else {
  2209. str = body;
  2210. }
  2211. if (str !== undefined) {
  2212. body = this.document.createTextNode(str);
  2213. bodyNode.appendChild(body);
  2214. }
  2215. }
  2216. // Add the message repeats node only when needed.
  2217. let repeatNode = null;
  2218. if (category != CATEGORY_INPUT &&
  2219. category != CATEGORY_OUTPUT &&
  2220. category != CATEGORY_NETWORK &&
  2221. !(category == CATEGORY_CSS && severity == SEVERITY_LOG)) {
  2222. repeatNode = this.document.createElementNS(XHTML_NS, "span");
  2223. repeatNode.setAttribute("value", "1");
  2224. repeatNode.className = "message-repeats";
  2225. repeatNode.textContent = 1;
  2226. repeatNode._uid = [bodyNode.textContent, category, severity, level,
  2227. sourceURL, sourceLine].join(":");
  2228. }
  2229. // Create the timestamp.
  2230. let timestampNode = this.document.createElementNS(XHTML_NS, "span");
  2231. timestampNode.className = "timestamp devtools-monospace";
  2232. let timestampString = l10n.timestampString(timestamp);
  2233. timestampNode.textContent = timestampString + " ";
  2234. // Create the source location (e.g. www.example.com:6) that sits on the
  2235. // right side of the message, if applicable.
  2236. let locationNode;
  2237. if (sourceURL && IGNORED_SOURCE_URLS.indexOf(sourceURL) == -1) {
  2238. locationNode = this.createLocationNode({url: sourceURL,
  2239. line: sourceLine});
  2240. }
  2241. node.appendChild(timestampNode);
  2242. node.appendChild(indentNode);
  2243. node.appendChild(iconContainer);
  2244. // Display the variables view after the message node.
  2245. if (level == "dir") {
  2246. let options = {
  2247. objectActor: storedBody.arguments[0],
  2248. targetElement: bodyNode,
  2249. hideFilterInput: true,
  2250. };
  2251. this.jsterm.openVariablesView(options).then((view) => {
  2252. node._variablesView = view;
  2253. if (node.classList.contains("hidden-message")) {
  2254. node.classList.remove("hidden-message");
  2255. }
  2256. });
  2257. node.classList.add("inlined-variables-view");
  2258. }
  2259. node.appendChild(bodyNode);
  2260. if (repeatNode) {
  2261. node.appendChild(repeatNode);
  2262. }
  2263. if (locationNode) {
  2264. node.appendChild(locationNode);
  2265. }
  2266. node.appendChild(this.document.createTextNode("\n"));
  2267. return node;
  2268. },
  2269. /**
  2270. * Creates the anchor that displays the textual location of an incoming
  2271. * message.
  2272. *
  2273. * @param {Object} location
  2274. * An object containing url, line and column number of the message source.
  2275. * @return {Element}
  2276. * The new anchor element, ready to be added to the message node.
  2277. */
  2278. createLocationNode: function (location) {
  2279. let locationNode = this.document.createElementNS(XHTML_NS, "div");
  2280. locationNode.className = "message-location devtools-monospace";
  2281. // Make the location clickable.
  2282. let onClick = ({ url, line }) => {
  2283. let category = locationNode.closest(".message").category;
  2284. let target = null;
  2285. if (/^Scratchpad\/\d+$/.test(url)) {
  2286. target = "scratchpad";
  2287. } else if (category === CATEGORY_CSS) {
  2288. target = "styleeditor";
  2289. } else if (category === CATEGORY_JS || category === CATEGORY_WEBDEV) {
  2290. target = "jsdebugger";
  2291. } else if (/\.js$/.test(url)) {
  2292. // If it ends in .js, let's attempt to open in debugger
  2293. // anyway, as this falls back to normal view-source.
  2294. target = "jsdebugger";
  2295. } else {
  2296. // Point everything else to debugger, if source not available,
  2297. // it will fall back to view-source.
  2298. target = "jsdebugger";
  2299. }
  2300. switch (target) {
  2301. case "scratchpad":
  2302. this.owner.viewSourceInScratchpad(url, line);
  2303. return;
  2304. case "jsdebugger":
  2305. this.owner.viewSourceInDebugger(url, line);
  2306. return;
  2307. case "styleeditor":
  2308. this.owner.viewSourceInStyleEditor(url, line);
  2309. return;
  2310. }
  2311. // No matching tool found; use old school view-source
  2312. this.owner.viewSource(url, line);
  2313. };
  2314. const toolbox = gDevTools.getToolbox(this.owner.target);
  2315. let { url, line, column } = location;
  2316. let source = url ? url.split(" -> ").pop() : "";
  2317. this.ReactDOM.render(this.FrameView({
  2318. frame: { source, line, column },
  2319. showEmptyPathAsHost: true,
  2320. onClick,
  2321. sourceMapService: toolbox ? toolbox._sourceMapService : null,
  2322. }), locationNode);
  2323. return locationNode;
  2324. },
  2325. /**
  2326. * Adjusts the category and severity of the given message.
  2327. *
  2328. * @param nsIDOMNode messageNode
  2329. * The message node to alter.
  2330. * @param number category
  2331. * The category for the message; one of the CATEGORY_ constants.
  2332. * @param number severity
  2333. * The severity for the message; one of the SEVERITY_ constants.
  2334. * @return void
  2335. */
  2336. setMessageType: function (messageNode, category, severity) {
  2337. messageNode.category = category;
  2338. messageNode.severity = severity;
  2339. messageNode.setAttribute("category", CATEGORY_CLASS_FRAGMENTS[category]);
  2340. messageNode.setAttribute("severity", SEVERITY_CLASS_FRAGMENTS[severity]);
  2341. messageNode.setAttribute("filter",
  2342. MESSAGE_PREFERENCE_KEYS[category][severity]);
  2343. },
  2344. /**
  2345. * Add the mouse event handlers needed to make a link.
  2346. *
  2347. * @private
  2348. * @param nsIDOMNode node
  2349. * The node for which you want to add the event handlers.
  2350. * @param function callback
  2351. * The function you want to invoke on click.
  2352. */
  2353. _addMessageLinkCallback: function (node, callback) {
  2354. node.addEventListener("mousedown", (event) => {
  2355. this._mousedown = true;
  2356. this._startX = event.clientX;
  2357. this._startY = event.clientY;
  2358. }, false);
  2359. node.addEventListener("click", (event) => {
  2360. let mousedown = this._mousedown;
  2361. this._mousedown = false;
  2362. event.preventDefault();
  2363. // Do not allow middle/right-click or 2+ clicks.
  2364. if (event.detail != 1 || event.button != 0) {
  2365. return;
  2366. }
  2367. // If this event started with a mousedown event and it ends at a different
  2368. // location, we consider this text selection.
  2369. if (mousedown &&
  2370. (this._startX != event.clientX) &&
  2371. (this._startY != event.clientY)) {
  2372. this._startX = this._startY = undefined;
  2373. return;
  2374. }
  2375. this._startX = this._startY = undefined;
  2376. callback.call(this, event);
  2377. }, false);
  2378. },
  2379. /**
  2380. * Handler for the pref-changed event coming from the toolbox.
  2381. * Currently this function only handles the timestamps preferences.
  2382. *
  2383. * @private
  2384. * @param object event
  2385. * This parameter is a string that holds the event name
  2386. * pref-changed in this case.
  2387. * @param object data
  2388. * This is the pref-changed data object.
  2389. */
  2390. _onToolboxPrefChanged: function (event, data) {
  2391. if (data.pref == PREF_MESSAGE_TIMESTAMP) {
  2392. if (data.newValue) {
  2393. this.outputNode.classList.remove("hideTimestamps");
  2394. } else {
  2395. this.outputNode.classList.add("hideTimestamps");
  2396. }
  2397. }
  2398. },
  2399. /**
  2400. * Copies the selected items to the system clipboard.
  2401. *
  2402. * @param object options
  2403. * - linkOnly:
  2404. * An optional flag to copy only URL without other meta-information.
  2405. * Default is false.
  2406. * - contextmenu:
  2407. * An optional flag to copy the last clicked item which brought
  2408. * up the context menu if nothing is selected. Default is false.
  2409. */
  2410. copySelectedItems: function (options) {
  2411. options = options || { linkOnly: false, contextmenu: false };
  2412. // Gather up the selected items and concatenate their clipboard text.
  2413. let strings = [];
  2414. let children = this.output.getSelectedMessages();
  2415. if (!children.length && options.contextmenu) {
  2416. children = [this._contextMenuHandler.lastClickedMessage];
  2417. }
  2418. for (let item of children) {
  2419. // Ensure the selected item hasn't been filtered by type or string.
  2420. if (!item.classList.contains("filtered-by-type") &&
  2421. !item.classList.contains("filtered-by-string")) {
  2422. if (options.linkOnly) {
  2423. strings.push(item.url);
  2424. } else {
  2425. strings.push(item.clipboardText);
  2426. }
  2427. }
  2428. }
  2429. clipboardHelper.copyString(strings.join("\n"));
  2430. },
  2431. /**
  2432. * Object properties provider. This function gives you the properties of the
  2433. * remote object you want.
  2434. *
  2435. * @param string actor
  2436. * The object actor ID from which you want the properties.
  2437. * @param function callback
  2438. * Function you want invoked once the properties are received.
  2439. */
  2440. objectPropertiesProvider: function (actor, callback) {
  2441. this.webConsoleClient.inspectObjectProperties(actor,
  2442. function (response) {
  2443. if (response.error) {
  2444. console.error("Failed to retrieve the object properties from the " +
  2445. "server. Error: " + response.error);
  2446. return;
  2447. }
  2448. callback(response.properties);
  2449. });
  2450. },
  2451. /**
  2452. * Release an actor.
  2453. *
  2454. * @private
  2455. * @param string actor
  2456. * The actor ID you want to release.
  2457. */
  2458. _releaseObject: function (actor) {
  2459. if (this.proxy) {
  2460. this.proxy.releaseActor(actor);
  2461. }
  2462. },
  2463. /**
  2464. * Open the selected item's URL in a new tab.
  2465. */
  2466. openSelectedItemInTab: function () {
  2467. let item = this.output.getSelectedMessages(1)[0] ||
  2468. this._contextMenuHandler.lastClickedMessage;
  2469. if (!item || !item.url) {
  2470. return;
  2471. }
  2472. this.owner.openLink(item.url);
  2473. },
  2474. /**
  2475. * Destroy the WebConsoleFrame object. Call this method to avoid memory leaks
  2476. * when the Web Console is closed.
  2477. *
  2478. * @return object
  2479. * A promise that is resolved when the WebConsoleFrame instance is
  2480. * destroyed.
  2481. */
  2482. destroy: function () {
  2483. if (this._destroyer) {
  2484. return this._destroyer.promise;
  2485. }
  2486. this._destroyer = promise.defer();
  2487. let toolbox = gDevTools.getToolbox(this.owner.target);
  2488. if (toolbox) {
  2489. toolbox.off("webconsole-selected", this._onPanelSelected);
  2490. }
  2491. gDevTools.off("pref-changed", this._onToolboxPrefChanged);
  2492. this.window.removeEventListener("resize", this.resize, true);
  2493. this._repeatNodes = {};
  2494. this._outputQueue.forEach(this._destroyItem, this);
  2495. this._outputQueue = [];
  2496. this._itemDestroyQueue.forEach(this._destroyItem, this);
  2497. this._itemDestroyQueue = [];
  2498. this._pruneCategoriesQueue = {};
  2499. this.webConsoleClient.clearNetworkRequests();
  2500. // Unmount any currently living frame components in DOM, since
  2501. // currently we only clean up messages in `this.removeOutputMessage`,
  2502. // via `this.pruneOutputIfNecessary`.
  2503. let liveMessages = this.outputNode.querySelectorAll(".message");
  2504. Array.prototype.forEach.call(liveMessages, this.unmountMessage);
  2505. if (this._outputTimerInitialized) {
  2506. this._outputTimerInitialized = false;
  2507. this._outputTimer.cancel();
  2508. }
  2509. this._outputTimer = null;
  2510. if (this.jsterm) {
  2511. this.jsterm.off("sidebar-opened", this.resize);
  2512. this.jsterm.off("sidebar-closed", this.resize);
  2513. this.jsterm.destroy();
  2514. this.jsterm = null;
  2515. }
  2516. this.output.destroy();
  2517. this.output = null;
  2518. this.React = this.ReactDOM = this.FrameView = null;
  2519. if (this._contextMenuHandler) {
  2520. this._contextMenuHandler.destroy();
  2521. this._contextMenuHandler = null;
  2522. }
  2523. this._commandController = null;
  2524. let onDestroy = () => {
  2525. this._destroyer.resolve(null);
  2526. };
  2527. if (this.proxy) {
  2528. this.proxy.disconnect().then(onDestroy);
  2529. this.proxy = null;
  2530. } else {
  2531. onDestroy();
  2532. }
  2533. return this._destroyer.promise;
  2534. },
  2535. };
  2536. /**
  2537. * Utils: a collection of globally used functions.
  2538. */
  2539. var Utils = {
  2540. /**
  2541. * Scrolls a node so that it's visible in its containing element.
  2542. *
  2543. * @param nsIDOMNode node
  2544. * The node to make visible.
  2545. * @returns void
  2546. */
  2547. scrollToVisible: function (node) {
  2548. node.scrollIntoView(false);
  2549. },
  2550. /**
  2551. * Check if the given output node is scrolled to the bottom.
  2552. *
  2553. * @param nsIDOMNode outputNode
  2554. * @param nsIDOMNode scrollNode
  2555. * @return boolean
  2556. * True if the output node is scrolled to the bottom, or false
  2557. * otherwise.
  2558. */
  2559. isOutputScrolledToBottom: function (outputNode, scrollNode) {
  2560. let lastNodeHeight = outputNode.lastChild ?
  2561. outputNode.lastChild.clientHeight : 0;
  2562. return scrollNode.scrollTop + scrollNode.clientHeight >=
  2563. scrollNode.scrollHeight - lastNodeHeight / 2;
  2564. },
  2565. /**
  2566. * Determine the category of a given nsIScriptError.
  2567. *
  2568. * @param nsIScriptError scriptError
  2569. * The script error you want to determine the category for.
  2570. * @return CATEGORY_JS|CATEGORY_CSS|CATEGORY_SECURITY
  2571. * Depending on the script error CATEGORY_JS, CATEGORY_CSS, or
  2572. * CATEGORY_SECURITY can be returned.
  2573. */
  2574. categoryForScriptError: function (scriptError) {
  2575. let category = scriptError.category;
  2576. if (/^(?:CSS|Layout)\b/.test(category)) {
  2577. return CATEGORY_CSS;
  2578. }
  2579. switch (category) {
  2580. case "Mixed Content Blocker":
  2581. case "Mixed Content Message":
  2582. case "CSP":
  2583. case "Invalid HSTS Headers":
  2584. case "Invalid HPKP Headers":
  2585. case "SHA-1 Signature":
  2586. case "Insecure Password Field":
  2587. case "SSL":
  2588. case "CORS":
  2589. case "Iframe Sandbox":
  2590. case "Tracking Protection":
  2591. case "Sub-resource Integrity":
  2592. return CATEGORY_SECURITY;
  2593. default:
  2594. return CATEGORY_JS;
  2595. }
  2596. },
  2597. /**
  2598. * Retrieve the limit of messages for a specific category.
  2599. *
  2600. * @param number category
  2601. * The category of messages you want to retrieve the limit for. See the
  2602. * CATEGORY_* constants.
  2603. * @return number
  2604. * The number of messages allowed for the specific category.
  2605. */
  2606. logLimitForCategory: function (category) {
  2607. let logLimit = DEFAULT_LOG_LIMIT;
  2608. try {
  2609. let prefName = CATEGORY_CLASS_FRAGMENTS[category];
  2610. logLimit = Services.prefs.getIntPref("devtools.hud.loglimit." + prefName);
  2611. logLimit = Math.max(logLimit, 1);
  2612. } catch (e) {
  2613. // Ignore any exceptions
  2614. }
  2615. return logLimit;
  2616. },
  2617. };
  2618. // CommandController
  2619. /**
  2620. * A controller (an instance of nsIController) that makes editing actions
  2621. * behave appropriately in the context of the Web Console.
  2622. */
  2623. function CommandController(webConsole) {
  2624. this.owner = webConsole;
  2625. }
  2626. CommandController.prototype = {
  2627. /**
  2628. * Selects all the text in the HUD output.
  2629. */
  2630. selectAll: function () {
  2631. this.owner.output.selectAllMessages();
  2632. },
  2633. /**
  2634. * Open the URL of the selected message in a new tab.
  2635. */
  2636. openURL: function () {
  2637. this.owner.openSelectedItemInTab();
  2638. },
  2639. copyURL: function () {
  2640. this.owner.copySelectedItems({ linkOnly: true, contextmenu: true });
  2641. },
  2642. /**
  2643. * Copies the last clicked message.
  2644. */
  2645. copyLastClicked: function () {
  2646. this.owner.copySelectedItems({ linkOnly: false, contextmenu: true });
  2647. },
  2648. supportsCommand: function (command) {
  2649. if (!this.owner || !this.owner.output) {
  2650. return false;
  2651. }
  2652. return this.isCommandEnabled(command);
  2653. },
  2654. isCommandEnabled: function (command) {
  2655. switch (command) {
  2656. case "consoleCmd_openURL":
  2657. case "consoleCmd_copyURL": {
  2658. // Only enable URL-related actions if node is Net Activity.
  2659. let selectedItem = this.owner.output.getSelectedMessages(1)[0] ||
  2660. this.owner._contextMenuHandler.lastClickedMessage;
  2661. return selectedItem && "url" in selectedItem;
  2662. }
  2663. case "cmd_copy": {
  2664. // Only copy if we right-clicked the console and there's no selected
  2665. // text. With text selected, we want to fall back onto the default
  2666. // copy behavior.
  2667. return this.owner._contextMenuHandler.lastClickedMessage &&
  2668. !this.owner.output.getSelectedMessages(1)[0];
  2669. }
  2670. case "cmd_selectAll":
  2671. return true;
  2672. }
  2673. return false;
  2674. },
  2675. doCommand: function (command) {
  2676. switch (command) {
  2677. case "consoleCmd_openURL":
  2678. this.openURL();
  2679. break;
  2680. case "consoleCmd_copyURL":
  2681. this.copyURL();
  2682. break;
  2683. case "cmd_copy":
  2684. this.copyLastClicked();
  2685. break;
  2686. case "cmd_selectAll":
  2687. this.selectAll();
  2688. break;
  2689. }
  2690. }
  2691. };
  2692. // Web Console connection proxy
  2693. /**
  2694. * The WebConsoleConnectionProxy handles the connection between the Web Console
  2695. * and the application we connect to through the remote debug protocol.
  2696. *
  2697. * @constructor
  2698. * @param object webConsoleFrame
  2699. * The WebConsoleFrame object that owns this connection proxy.
  2700. * @param RemoteTarget target
  2701. * The target that the console will connect to.
  2702. */
  2703. function WebConsoleConnectionProxy(webConsoleFrame, target) {
  2704. this.webConsoleFrame = webConsoleFrame;
  2705. this.target = target;
  2706. this._onPageError = this._onPageError.bind(this);
  2707. this._onLogMessage = this._onLogMessage.bind(this);
  2708. this._onConsoleAPICall = this._onConsoleAPICall.bind(this);
  2709. this._onNetworkEvent = this._onNetworkEvent.bind(this);
  2710. this._onNetworkEventUpdate = this._onNetworkEventUpdate.bind(this);
  2711. this._onFileActivity = this._onFileActivity.bind(this);
  2712. this._onReflowActivity = this._onReflowActivity.bind(this);
  2713. this._onServerLogCall = this._onServerLogCall.bind(this);
  2714. this._onTabNavigated = this._onTabNavigated.bind(this);
  2715. this._onAttachConsole = this._onAttachConsole.bind(this);
  2716. this._onCachedMessages = this._onCachedMessages.bind(this);
  2717. this._connectionTimeout = this._connectionTimeout.bind(this);
  2718. this._onLastPrivateContextExited =
  2719. this._onLastPrivateContextExited.bind(this);
  2720. }
  2721. WebConsoleConnectionProxy.prototype = {
  2722. /**
  2723. * The owning Web Console Frame instance.
  2724. *
  2725. * @see WebConsoleFrame
  2726. * @type object
  2727. */
  2728. webConsoleFrame: null,
  2729. /**
  2730. * The target that the console connects to.
  2731. * @type RemoteTarget
  2732. */
  2733. target: null,
  2734. /**
  2735. * The DebuggerClient object.
  2736. *
  2737. * @see DebuggerClient
  2738. * @type object
  2739. */
  2740. client: null,
  2741. /**
  2742. * The WebConsoleClient object.
  2743. *
  2744. * @see WebConsoleClient
  2745. * @type object
  2746. */
  2747. webConsoleClient: null,
  2748. /**
  2749. * Tells if the connection is established.
  2750. * @type boolean
  2751. */
  2752. connected: false,
  2753. /**
  2754. * Timer used for the connection.
  2755. * @private
  2756. * @type object
  2757. */
  2758. _connectTimer: null,
  2759. _connectDefer: null,
  2760. _disconnecter: null,
  2761. /**
  2762. * The WebConsoleActor ID.
  2763. *
  2764. * @private
  2765. * @type string
  2766. */
  2767. _consoleActor: null,
  2768. /**
  2769. * Tells if the window.console object of the remote web page is the native
  2770. * object or not.
  2771. * @private
  2772. * @type boolean
  2773. */
  2774. _hasNativeConsoleAPI: false,
  2775. /**
  2776. * Initialize a debugger client and connect it to the debugger server.
  2777. *
  2778. * @return object
  2779. * A promise object that is resolved/rejected based on the success of
  2780. * the connection initialization.
  2781. */
  2782. connect: function () {
  2783. if (this._connectDefer) {
  2784. return this._connectDefer.promise;
  2785. }
  2786. this._connectDefer = promise.defer();
  2787. let timeout = Services.prefs.getIntPref(PREF_CONNECTION_TIMEOUT);
  2788. this._connectTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
  2789. this._connectTimer.initWithCallback(this._connectionTimeout,
  2790. timeout, Ci.nsITimer.TYPE_ONE_SHOT);
  2791. let connPromise = this._connectDefer.promise;
  2792. connPromise.then(() => {
  2793. this._connectTimer.cancel();
  2794. this._connectTimer = null;
  2795. }, () => {
  2796. this._connectTimer = null;
  2797. });
  2798. let client = this.client = this.target.client;
  2799. if (this.target.isWorkerTarget) {
  2800. // XXXworkers: Not Console API yet inside of workers (Bug 1209353).
  2801. } else {
  2802. client.addListener("logMessage", this._onLogMessage);
  2803. client.addListener("pageError", this._onPageError);
  2804. client.addListener("consoleAPICall", this._onConsoleAPICall);
  2805. client.addListener("fileActivity", this._onFileActivity);
  2806. client.addListener("reflowActivity", this._onReflowActivity);
  2807. client.addListener("serverLogCall", this._onServerLogCall);
  2808. client.addListener("lastPrivateContextExited",
  2809. this._onLastPrivateContextExited);
  2810. }
  2811. this.target.on("will-navigate", this._onTabNavigated);
  2812. this.target.on("navigate", this._onTabNavigated);
  2813. this._consoleActor = this.target.form.consoleActor;
  2814. if (this.target.isTabActor) {
  2815. let tab = this.target.form;
  2816. this.webConsoleFrame.onLocationChange(tab.url, tab.title);
  2817. }
  2818. this._attachConsole();
  2819. return connPromise;
  2820. },
  2821. /**
  2822. * Connection timeout handler.
  2823. * @private
  2824. */
  2825. _connectionTimeout: function () {
  2826. let error = {
  2827. error: "timeout",
  2828. message: l10n.getStr("connectionTimeout"),
  2829. };
  2830. this._connectDefer.reject(error);
  2831. },
  2832. /**
  2833. * Attach to the Web Console actor.
  2834. * @private
  2835. */
  2836. _attachConsole: function () {
  2837. let listeners = ["PageError", "ConsoleAPI", "NetworkActivity",
  2838. "FileActivity"];
  2839. this.client.attachConsole(this._consoleActor, listeners,
  2840. this._onAttachConsole);
  2841. },
  2842. /**
  2843. * The "attachConsole" response handler.
  2844. *
  2845. * @private
  2846. * @param object response
  2847. * The JSON response object received from the server.
  2848. * @param object webConsoleClient
  2849. * The WebConsoleClient instance for the attached console, for the
  2850. * specific tab we work with.
  2851. */
  2852. _onAttachConsole: function (response, webConsoleClient) {
  2853. if (response.error) {
  2854. console.error("attachConsole failed: " + response.error + " " +
  2855. response.message);
  2856. this._connectDefer.reject(response);
  2857. return;
  2858. }
  2859. this.webConsoleClient = webConsoleClient;
  2860. this._hasNativeConsoleAPI = response.nativeConsoleAPI;
  2861. let saveBodiesPref = this.webConsoleFrame._filterPrefsPrefix + "saveBodies";
  2862. let saveBodies = Services.prefs.getBoolPref(saveBodiesPref);
  2863. this.webConsoleFrame.setSaveRequestAndResponseBodies(saveBodies);
  2864. this.webConsoleClient.on("networkEvent", this._onNetworkEvent);
  2865. this.webConsoleClient.on("networkEventUpdate", this._onNetworkEventUpdate);
  2866. let msgs = ["PageError", "ConsoleAPI"];
  2867. this.webConsoleClient.getCachedMessages(msgs, this._onCachedMessages);
  2868. this.webConsoleFrame._onUpdateListeners();
  2869. },
  2870. /**
  2871. * Dispatch a message add on the new frontend and emit an event for tests.
  2872. */
  2873. dispatchMessageAdd: function(packet) {
  2874. this.webConsoleFrame.newConsoleOutput.dispatchMessageAdd(packet);
  2875. },
  2876. /**
  2877. * Batched dispatch of messages.
  2878. */
  2879. dispatchMessagesAdd: function(packets) {
  2880. this.webConsoleFrame.newConsoleOutput.dispatchMessagesAdd(packets);
  2881. },
  2882. /**
  2883. * The "cachedMessages" response handler.
  2884. *
  2885. * @private
  2886. * @param object response
  2887. * The JSON response object received from the server.
  2888. */
  2889. _onCachedMessages: function (response) {
  2890. if (response.error) {
  2891. console.error("Web Console getCachedMessages error: " + response.error +
  2892. " " + response.message);
  2893. this._connectDefer.reject(response);
  2894. return;
  2895. }
  2896. if (!this._connectTimer) {
  2897. // This happens if the promise is rejected (eg. a timeout), but the
  2898. // connection attempt is successful, nonetheless.
  2899. console.error("Web Console getCachedMessages error: invalid state.");
  2900. }
  2901. let messages =
  2902. response.messages.concat(...this.webConsoleClient.getNetworkEvents());
  2903. messages.sort((a, b) => a.timeStamp - b.timeStamp);
  2904. if (this.webConsoleFrame.NEW_CONSOLE_OUTPUT_ENABLED) {
  2905. // Filter out CSS page errors.
  2906. messages = messages.filter(message => !(message._type == "PageError"
  2907. && Utils.categoryForScriptError(message) === CATEGORY_CSS));
  2908. this.dispatchMessagesAdd(messages);
  2909. } else {
  2910. this.webConsoleFrame.displayCachedMessages(messages);
  2911. if (!this._hasNativeConsoleAPI) {
  2912. this.webConsoleFrame.logWarningAboutReplacedAPI();
  2913. }
  2914. }
  2915. this.connected = true;
  2916. this._connectDefer.resolve(this);
  2917. },
  2918. /**
  2919. * The "pageError" message type handler. We redirect any page errors to the UI
  2920. * for displaying.
  2921. *
  2922. * @private
  2923. * @param string type
  2924. * Message type.
  2925. * @param object packet
  2926. * The message received from the server.
  2927. */
  2928. _onPageError: function (type, packet) {
  2929. if (this.webConsoleFrame && packet.from == this._consoleActor) {
  2930. if (this.webConsoleFrame.NEW_CONSOLE_OUTPUT_ENABLED) {
  2931. let category = Utils.categoryForScriptError(packet.pageError);
  2932. if (category !== CATEGORY_CSS) {
  2933. this.dispatchMessageAdd(packet);
  2934. }
  2935. return;
  2936. }
  2937. this.webConsoleFrame.handlePageError(packet.pageError);
  2938. }
  2939. },
  2940. /**
  2941. * The "logMessage" message type handler. We redirect any message to the UI
  2942. * for displaying.
  2943. *
  2944. * @private
  2945. * @param string type
  2946. * Message type.
  2947. * @param object packet
  2948. * The message received from the server.
  2949. */
  2950. _onLogMessage: function (type, packet) {
  2951. if (this.webConsoleFrame && packet.from == this._consoleActor) {
  2952. this.webConsoleFrame.handleLogMessage(packet);
  2953. }
  2954. },
  2955. /**
  2956. * The "consoleAPICall" message type handler. We redirect any message to
  2957. * the UI for displaying.
  2958. *
  2959. * @private
  2960. * @param string type
  2961. * Message type.
  2962. * @param object packet
  2963. * The message received from the server.
  2964. */
  2965. _onConsoleAPICall: function (type, packet) {
  2966. if (this.webConsoleFrame && packet.from == this._consoleActor) {
  2967. if (this.webConsoleFrame.NEW_CONSOLE_OUTPUT_ENABLED) {
  2968. this.dispatchMessageAdd(packet);
  2969. } else {
  2970. this.webConsoleFrame.handleConsoleAPICall(packet.message);
  2971. }
  2972. }
  2973. },
  2974. /**
  2975. * The "networkEvent" message type handler. We redirect any message to
  2976. * the UI for displaying.
  2977. *
  2978. * @private
  2979. * @param string type
  2980. * Message type.
  2981. * @param object networkInfo
  2982. * The network request information.
  2983. */
  2984. _onNetworkEvent: function (type, networkInfo) {
  2985. if (this.webConsoleFrame) {
  2986. if (this.webConsoleFrame.NEW_CONSOLE_OUTPUT_ENABLED) {
  2987. this.dispatchMessageAdd(networkInfo);
  2988. } else {
  2989. this.webConsoleFrame.handleNetworkEvent(networkInfo);
  2990. }
  2991. }
  2992. },
  2993. /**
  2994. * The "networkEventUpdate" message type handler. We redirect any message to
  2995. * the UI for displaying.
  2996. *
  2997. * @private
  2998. * @param string type
  2999. * Message type.
  3000. * @param object packet
  3001. * The message received from the server.
  3002. * @param object networkInfo
  3003. * The network request information.
  3004. */
  3005. _onNetworkEventUpdate: function (type, { packet, networkInfo }) {
  3006. if (this.webConsoleFrame) {
  3007. this.webConsoleFrame.handleNetworkEventUpdate(networkInfo, packet);
  3008. }
  3009. },
  3010. /**
  3011. * The "fileActivity" message type handler. We redirect any message to
  3012. * the UI for displaying.
  3013. *
  3014. * @private
  3015. * @param string type
  3016. * Message type.
  3017. * @param object packet
  3018. * The message received from the server.
  3019. */
  3020. _onFileActivity: function (type, packet) {
  3021. if (this.webConsoleFrame && packet.from == this._consoleActor) {
  3022. this.webConsoleFrame.handleFileActivity(packet.uri);
  3023. }
  3024. },
  3025. _onReflowActivity: function (type, packet) {
  3026. if (this.webConsoleFrame && packet.from == this._consoleActor) {
  3027. this.webConsoleFrame.handleReflowActivity(packet);
  3028. }
  3029. },
  3030. /**
  3031. * The "serverLogCall" message type handler. We redirect any message to
  3032. * the UI for displaying.
  3033. *
  3034. * @private
  3035. * @param string type
  3036. * Message type.
  3037. * @param object packet
  3038. * The message received from the server.
  3039. */
  3040. _onServerLogCall: function (type, packet) {
  3041. if (this.webConsoleFrame && packet.from == this._consoleActor) {
  3042. this.webConsoleFrame.handleConsoleAPICall(packet.message);
  3043. }
  3044. },
  3045. /**
  3046. * The "lastPrivateContextExited" message type handler. When this message is
  3047. * received the Web Console UI is cleared.
  3048. *
  3049. * @private
  3050. * @param string type
  3051. * Message type.
  3052. * @param object packet
  3053. * The message received from the server.
  3054. */
  3055. _onLastPrivateContextExited: function (type, packet) {
  3056. if (this.webConsoleFrame && packet.from == this._consoleActor) {
  3057. this.webConsoleFrame.jsterm.clearPrivateMessages();
  3058. }
  3059. },
  3060. /**
  3061. * The "will-navigate" and "navigate" event handlers. We redirect any message
  3062. * to the UI for displaying.
  3063. *
  3064. * @private
  3065. * @param string event
  3066. * Event type.
  3067. * @param object packet
  3068. * The message received from the server.
  3069. */
  3070. _onTabNavigated: function (event, packet) {
  3071. if (!this.webConsoleFrame) {
  3072. return;
  3073. }
  3074. this.webConsoleFrame.handleTabNavigated(event, packet);
  3075. },
  3076. /**
  3077. * Release an object actor.
  3078. *
  3079. * @param string actor
  3080. * The actor ID to send the request to.
  3081. */
  3082. releaseActor: function (actor) {
  3083. if (this.client) {
  3084. this.client.release(actor);
  3085. }
  3086. },
  3087. /**
  3088. * Disconnect the Web Console from the remote server.
  3089. *
  3090. * @return object
  3091. * A promise object that is resolved when disconnect completes.
  3092. */
  3093. disconnect: function () {
  3094. if (this._disconnecter) {
  3095. return this._disconnecter.promise;
  3096. }
  3097. this._disconnecter = promise.defer();
  3098. if (!this.client) {
  3099. this._disconnecter.resolve(null);
  3100. return this._disconnecter.promise;
  3101. }
  3102. this.client.removeListener("logMessage", this._onLogMessage);
  3103. this.client.removeListener("pageError", this._onPageError);
  3104. this.client.removeListener("consoleAPICall", this._onConsoleAPICall);
  3105. this.client.removeListener("fileActivity", this._onFileActivity);
  3106. this.client.removeListener("reflowActivity", this._onReflowActivity);
  3107. this.client.removeListener("serverLogCall", this._onServerLogCall);
  3108. this.client.removeListener("lastPrivateContextExited",
  3109. this._onLastPrivateContextExited);
  3110. this.webConsoleClient.off("networkEvent", this._onNetworkEvent);
  3111. this.webConsoleClient.off("networkEventUpdate", this._onNetworkEventUpdate);
  3112. this.target.off("will-navigate", this._onTabNavigated);
  3113. this.target.off("navigate", this._onTabNavigated);
  3114. this.client = null;
  3115. this.webConsoleClient = null;
  3116. this.target = null;
  3117. this.connected = false;
  3118. this.webConsoleFrame = null;
  3119. this._disconnecter.resolve(null);
  3120. return this._disconnecter.promise;
  3121. },
  3122. };
  3123. // Context Menu
  3124. /*
  3125. * ConsoleContextMenu this used to handle the visibility of context menu items.
  3126. *
  3127. * @constructor
  3128. * @param object owner
  3129. * The WebConsoleFrame instance that owns this object.
  3130. */
  3131. function ConsoleContextMenu(owner) {
  3132. this.owner = owner;
  3133. this.popup = this.owner.document.getElementById("output-contextmenu");
  3134. this.build = this.build.bind(this);
  3135. this.popup.addEventListener("popupshowing", this.build);
  3136. }
  3137. ConsoleContextMenu.prototype = {
  3138. lastClickedMessage: null,
  3139. /*
  3140. * Handle to show/hide context menu item.
  3141. */
  3142. build: function (event) {
  3143. let metadata = this.getSelectionMetadata(event.rangeParent);
  3144. for (let element of this.popup.children) {
  3145. element.hidden = this.shouldHideMenuItem(element, metadata);
  3146. }
  3147. },
  3148. /*
  3149. * Get selection information from the view.
  3150. *
  3151. * @param nsIDOMElement clickElement
  3152. * The DOM element the user clicked on.
  3153. * @return object
  3154. * Selection metadata.
  3155. */
  3156. getSelectionMetadata: function (clickElement) {
  3157. let metadata = {
  3158. selectionType: "",
  3159. selection: new Set(),
  3160. };
  3161. let selectedItems = this.owner.output.getSelectedMessages();
  3162. if (!selectedItems.length) {
  3163. let clickedItem = this.owner.output.getMessageForElement(clickElement);
  3164. if (clickedItem) {
  3165. this.lastClickedMessage = clickedItem;
  3166. selectedItems = [clickedItem];
  3167. }
  3168. }
  3169. metadata.selectionType = selectedItems.length > 1 ? "multiple" : "single";
  3170. let selection = metadata.selection;
  3171. for (let item of selectedItems) {
  3172. switch (item.category) {
  3173. case CATEGORY_NETWORK:
  3174. selection.add("network");
  3175. break;
  3176. case CATEGORY_CSS:
  3177. selection.add("css");
  3178. break;
  3179. case CATEGORY_JS:
  3180. selection.add("js");
  3181. break;
  3182. case CATEGORY_WEBDEV:
  3183. selection.add("webdev");
  3184. break;
  3185. case CATEGORY_SERVER:
  3186. selection.add("server");
  3187. break;
  3188. }
  3189. }
  3190. return metadata;
  3191. },
  3192. /*
  3193. * Determine if an item should be hidden.
  3194. *
  3195. * @param nsIDOMElement menuItem
  3196. * @param object metadata
  3197. * @return boolean
  3198. * Whether the given item should be hidden or not.
  3199. */
  3200. shouldHideMenuItem: function (menuItem, metadata) {
  3201. let selectionType = menuItem.getAttribute("selectiontype");
  3202. if (selectionType && !metadata.selectionType == selectionType) {
  3203. return true;
  3204. }
  3205. let selection = menuItem.getAttribute("selection");
  3206. if (!selection) {
  3207. return false;
  3208. }
  3209. let shouldHide = true;
  3210. let itemData = selection.split("|");
  3211. for (let type of metadata.selection) {
  3212. // check whether this menu item should show or not.
  3213. if (itemData.indexOf(type) !== -1) {
  3214. shouldHide = false;
  3215. break;
  3216. }
  3217. }
  3218. return shouldHide;
  3219. },
  3220. /**
  3221. * Destroy the ConsoleContextMenu object instance.
  3222. */
  3223. destroy: function () {
  3224. this.popup.removeEventListener("popupshowing", this.build);
  3225. this.popup = null;
  3226. this.owner = null;
  3227. this.lastClickedMessage = null;
  3228. },
  3229. };