inspector.js 98 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199
  1. /* This Source Code Form is subject to the terms of the Mozilla Public
  2. * License, v. 2.0. If a copy of the MPL was not distributed with this
  3. * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
  4. "use strict";
  5. /**
  6. * Here's the server side of the remote inspector.
  7. *
  8. * The WalkerActor is the client's view of the debuggee's DOM. It's gives
  9. * the client a tree of NodeActor objects.
  10. *
  11. * The walker presents the DOM tree mostly unmodified from the source DOM
  12. * tree, but with a few key differences:
  13. *
  14. * - Empty text nodes are ignored. This is pretty typical of developer
  15. * tools, but maybe we should reconsider that on the server side.
  16. * - iframes with documents loaded have the loaded document as the child,
  17. * the walker provides one big tree for the whole document tree.
  18. *
  19. * There are a few ways to get references to NodeActors:
  20. *
  21. * - When you first get a WalkerActor reference, it comes with a free
  22. * reference to the root document's node.
  23. * - Given a node, you can ask for children, siblings, and parents.
  24. * - You can issue querySelector and querySelectorAll requests to find
  25. * other elements.
  26. * - Requests that return arbitrary nodes from the tree (like querySelector
  27. * and querySelectorAll) will also return any nodes the client hasn't
  28. * seen in order to have a complete set of parents.
  29. *
  30. * Once you have a NodeFront, you should be able to answer a few questions
  31. * without further round trips, like the node's name, namespace/tagName,
  32. * attributes, etc. Other questions (like a text node's full nodeValue)
  33. * might require another round trip.
  34. *
  35. * The protocol guarantees that the client will always know the parent of
  36. * any node that is returned by the server. This means that some requests
  37. * (like querySelector) will include the extra nodes needed to satisfy this
  38. * requirement. The client keeps track of this parent relationship, so the
  39. * node fronts form a tree that is a subset of the actual DOM tree.
  40. *
  41. *
  42. * We maintain this guarantee to support the ability to release subtrees on
  43. * the client - when a node is disconnected from the DOM tree we want to be
  44. * able to free the client objects for all the children nodes.
  45. *
  46. * So to be able to answer "all the children of a given node that we have
  47. * seen on the client side", we guarantee that every time we've seen a node,
  48. * we connect it up through its parents.
  49. */
  50. const {Cc, Ci, Cu} = require("chrome");
  51. const Services = require("Services");
  52. const protocol = require("devtools/shared/protocol");
  53. const {LayoutActor} = require("devtools/server/actors/layout");
  54. const {LongStringActor} = require("devtools/server/actors/string");
  55. const promise = require("promise");
  56. const {Task} = require("devtools/shared/task");
  57. const events = require("sdk/event/core");
  58. const {WalkerSearch} = require("devtools/server/actors/utils/walker-search");
  59. const {PageStyleActor, getFontPreviewData} = require("devtools/server/actors/styles");
  60. const {
  61. HighlighterActor,
  62. CustomHighlighterActor,
  63. isTypeRegistered,
  64. HighlighterEnvironment
  65. } = require("devtools/server/actors/highlighters");
  66. const {EyeDropper} = require("devtools/server/actors/highlighters/eye-dropper");
  67. const {
  68. isAnonymous,
  69. isNativeAnonymous,
  70. isXBLAnonymous,
  71. isShadowAnonymous,
  72. getFrameElement
  73. } = require("devtools/shared/layout/utils");
  74. const {getLayoutChangesObserver, releaseLayoutChangesObserver} = require("devtools/server/actors/reflow");
  75. const nodeFilterConstants = require("devtools/shared/dom-node-filter-constants");
  76. const {EventParsers} = require("devtools/server/event-parsers");
  77. const {nodeSpec, nodeListSpec, walkerSpec, inspectorSpec} = require("devtools/shared/specs/inspector");
  78. const FONT_FAMILY_PREVIEW_TEXT = "The quick brown fox jumps over the lazy dog";
  79. const FONT_FAMILY_PREVIEW_TEXT_SIZE = 20;
  80. const PSEUDO_CLASSES = [":hover", ":active", ":focus"];
  81. const HIDDEN_CLASS = "__fx-devtools-hide-shortcut__";
  82. const XHTML_NS = "http://www.w3.org/1999/xhtml";
  83. const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
  84. const IMAGE_FETCHING_TIMEOUT = 500;
  85. const RX_FUNC_NAME =
  86. /((var|const|let)\s+)?([\w$.]+\s*[:=]\s*)*(function)?\s*\*?\s*([\w$]+)?\s*$/;
  87. // The possible completions to a ':' with added score to give certain values
  88. // some preference.
  89. const PSEUDO_SELECTORS = [
  90. [":active", 1],
  91. [":hover", 1],
  92. [":focus", 1],
  93. [":visited", 0],
  94. [":link", 0],
  95. [":first-letter", 0],
  96. [":first-child", 2],
  97. [":before", 2],
  98. [":after", 2],
  99. [":lang(", 0],
  100. [":not(", 3],
  101. [":first-of-type", 0],
  102. [":last-of-type", 0],
  103. [":only-of-type", 0],
  104. [":only-child", 2],
  105. [":nth-child(", 3],
  106. [":nth-last-child(", 0],
  107. [":nth-of-type(", 0],
  108. [":nth-last-of-type(", 0],
  109. [":last-child", 2],
  110. [":root", 0],
  111. [":empty", 0],
  112. [":target", 0],
  113. [":enabled", 0],
  114. [":disabled", 0],
  115. [":checked", 1],
  116. ["::selection", 0]
  117. ];
  118. var HELPER_SHEET = `
  119. .__fx-devtools-hide-shortcut__ {
  120. visibility: hidden !important;
  121. }
  122. :-moz-devtools-highlighted {
  123. outline: 2px dashed #F06!important;
  124. outline-offset: -2px !important;
  125. }
  126. `;
  127. const flags = require("devtools/shared/flags");
  128. loader.lazyRequireGetter(this, "DevToolsUtils",
  129. "devtools/shared/DevToolsUtils");
  130. loader.lazyRequireGetter(this, "AsyncUtils", "devtools/shared/async-utils");
  131. loader.lazyGetter(this, "DOMParser", function () {
  132. return Cc["@mozilla.org/xmlextras/domparser;1"]
  133. .createInstance(Ci.nsIDOMParser);
  134. });
  135. loader.lazyGetter(this, "eventListenerService", function () {
  136. return Cc["@mozilla.org/eventlistenerservice;1"]
  137. .getService(Ci.nsIEventListenerService);
  138. });
  139. loader.lazyGetter(this, "CssLogic", () => require("devtools/server/css-logic").CssLogic);
  140. /**
  141. * We only send nodeValue up to a certain size by default. This stuff
  142. * controls that size.
  143. */
  144. exports.DEFAULT_VALUE_SUMMARY_LENGTH = 50;
  145. var gValueSummaryLength = exports.DEFAULT_VALUE_SUMMARY_LENGTH;
  146. exports.getValueSummaryLength = function () {
  147. return gValueSummaryLength;
  148. };
  149. exports.setValueSummaryLength = function (val) {
  150. gValueSummaryLength = val;
  151. };
  152. // When the user selects a node to inspect in e10s, the parent process
  153. // has a CPOW that wraps the node being inspected. It uses the
  154. // message manager to send this node to the child, which stores the
  155. // node in gInspectingNode. Then a findInspectingNode request is sent
  156. // over the remote debugging protocol, and gInspectingNode is returned
  157. // to the parent as a NodeFront.
  158. var gInspectingNode = null;
  159. // We expect this function to be called from the child.js frame script
  160. // when it receives the node to be inspected over the message manager.
  161. exports.setInspectingNode = function (val) {
  162. gInspectingNode = val;
  163. };
  164. /**
  165. * Returns the properly cased version of the node's tag name, which can be
  166. * used when displaying said name in the UI.
  167. *
  168. * @param {Node} rawNode
  169. * Node for which we want the display name
  170. * @return {String}
  171. * Properly cased version of the node tag name
  172. */
  173. const getNodeDisplayName = function (rawNode) {
  174. if (rawNode.nodeName && !rawNode.localName) {
  175. // The localName & prefix APIs have been moved from the Node interface to the Element
  176. // interface. Use Node.nodeName as a fallback.
  177. return rawNode.nodeName;
  178. }
  179. return (rawNode.prefix ? rawNode.prefix + ":" : "") + rawNode.localName;
  180. };
  181. exports.getNodeDisplayName = getNodeDisplayName;
  182. /**
  183. * Server side of the node actor.
  184. */
  185. var NodeActor = exports.NodeActor = protocol.ActorClassWithSpec(nodeSpec, {
  186. initialize: function (walker, node) {
  187. protocol.Actor.prototype.initialize.call(this, null);
  188. this.walker = walker;
  189. this.rawNode = node;
  190. this._eventParsers = new EventParsers().parsers;
  191. // Storing the original display of the node, to track changes when reflows
  192. // occur
  193. this.wasDisplayed = this.isDisplayed;
  194. },
  195. toString: function () {
  196. return "[NodeActor " + this.actorID + " for " +
  197. this.rawNode.toString() + "]";
  198. },
  199. /**
  200. * Instead of storing a connection object, the NodeActor gets its connection
  201. * from its associated walker.
  202. */
  203. get conn() {
  204. return this.walker.conn;
  205. },
  206. isDocumentElement: function () {
  207. return this.rawNode.ownerDocument &&
  208. this.rawNode.ownerDocument.documentElement === this.rawNode;
  209. },
  210. destroy: function () {
  211. protocol.Actor.prototype.destroy.call(this);
  212. if (this.mutationObserver) {
  213. if (!Cu.isDeadWrapper(this.mutationObserver)) {
  214. this.mutationObserver.disconnect();
  215. }
  216. this.mutationObserver = null;
  217. }
  218. this.rawNode = null;
  219. this.walker = null;
  220. },
  221. // Returns the JSON representation of this object over the wire.
  222. form: function (detail) {
  223. if (detail === "actorid") {
  224. return this.actorID;
  225. }
  226. let parentNode = this.walker.parentNode(this);
  227. let inlineTextChild = this.walker.inlineTextChild(this);
  228. let form = {
  229. actor: this.actorID,
  230. baseURI: this.rawNode.baseURI,
  231. parent: parentNode ? parentNode.actorID : undefined,
  232. nodeType: this.rawNode.nodeType,
  233. namespaceURI: this.rawNode.namespaceURI,
  234. nodeName: this.rawNode.nodeName,
  235. nodeValue: this.rawNode.nodeValue,
  236. displayName: getNodeDisplayName(this.rawNode),
  237. numChildren: this.numChildren,
  238. inlineTextChild: inlineTextChild ? inlineTextChild.form() : undefined,
  239. // doctype attributes
  240. name: this.rawNode.name,
  241. publicId: this.rawNode.publicId,
  242. systemId: this.rawNode.systemId,
  243. attrs: this.writeAttrs(),
  244. isBeforePseudoElement: this.isBeforePseudoElement,
  245. isAfterPseudoElement: this.isAfterPseudoElement,
  246. isAnonymous: isAnonymous(this.rawNode),
  247. isNativeAnonymous: isNativeAnonymous(this.rawNode),
  248. isXBLAnonymous: isXBLAnonymous(this.rawNode),
  249. isShadowAnonymous: isShadowAnonymous(this.rawNode),
  250. pseudoClassLocks: this.writePseudoClassLocks(),
  251. isDisplayed: this.isDisplayed,
  252. isInHTMLDocument: this.rawNode.ownerDocument &&
  253. this.rawNode.ownerDocument.contentType === "text/html",
  254. hasEventListeners: this._hasEventListeners,
  255. };
  256. if (this.isDocumentElement()) {
  257. form.isDocumentElement = true;
  258. }
  259. // Add an extra API for custom properties added by other
  260. // modules/extensions.
  261. form.setFormProperty = (name, value) => {
  262. if (!form.props) {
  263. form.props = {};
  264. }
  265. form.props[name] = value;
  266. };
  267. // Fire an event so, other modules can create its own properties
  268. // that should be passed to the client (within the form.props field).
  269. events.emit(NodeActor, "form", {
  270. target: this,
  271. data: form
  272. });
  273. return form;
  274. },
  275. /**
  276. * Watch the given document node for mutations using the DOM observer
  277. * API.
  278. */
  279. watchDocument: function (callback) {
  280. let node = this.rawNode;
  281. // Create the observer on the node's actor. The node will make sure
  282. // the observer is cleaned up when the actor is released.
  283. let observer = new node.defaultView.MutationObserver(callback);
  284. observer.mergeAttributeRecords = true;
  285. observer.observe(node, {
  286. nativeAnonymousChildList: true,
  287. attributes: true,
  288. characterData: true,
  289. characterDataOldValue: true,
  290. childList: true,
  291. subtree: true
  292. });
  293. this.mutationObserver = observer;
  294. },
  295. get isBeforePseudoElement() {
  296. return this.rawNode.nodeName === "_moz_generated_content_before";
  297. },
  298. get isAfterPseudoElement() {
  299. return this.rawNode.nodeName === "_moz_generated_content_after";
  300. },
  301. // Estimate the number of children that the walker will return without making
  302. // a call to children() if possible.
  303. get numChildren() {
  304. // For pseudo elements, childNodes.length returns 1, but the walker
  305. // will return 0.
  306. if (this.isBeforePseudoElement || this.isAfterPseudoElement) {
  307. return 0;
  308. }
  309. let rawNode = this.rawNode;
  310. let numChildren = rawNode.childNodes.length;
  311. let hasAnonChildren = rawNode.nodeType === Ci.nsIDOMNode.ELEMENT_NODE &&
  312. rawNode.ownerDocument.getAnonymousNodes(rawNode);
  313. let hasContentDocument = rawNode.contentDocument;
  314. let hasSVGDocument = rawNode.getSVGDocument && rawNode.getSVGDocument();
  315. if (numChildren === 0 && (hasContentDocument || hasSVGDocument)) {
  316. // This might be an iframe with virtual children.
  317. numChildren = 1;
  318. }
  319. // Normal counting misses ::before/::after. Also, some anonymous children
  320. // may ultimately be skipped, so we have to consult with the walker.
  321. if (numChildren === 0 || hasAnonChildren) {
  322. numChildren = this.walker.children(this).nodes.length;
  323. }
  324. return numChildren;
  325. },
  326. get computedStyle() {
  327. return CssLogic.getComputedStyle(this.rawNode);
  328. },
  329. /**
  330. * Is the node's display computed style value other than "none"
  331. */
  332. get isDisplayed() {
  333. // Consider all non-element nodes as displayed.
  334. if (isNodeDead(this) ||
  335. this.rawNode.nodeType !== Ci.nsIDOMNode.ELEMENT_NODE ||
  336. this.isAfterPseudoElement ||
  337. this.isBeforePseudoElement) {
  338. return true;
  339. }
  340. let style = this.computedStyle;
  341. if (!style) {
  342. return true;
  343. }
  344. return style.display !== "none";
  345. },
  346. /**
  347. * Are there event listeners that are listening on this node? This method
  348. * uses all parsers registered via event-parsers.js.registerEventParser() to
  349. * check if there are any event listeners.
  350. */
  351. get _hasEventListeners() {
  352. let parsers = this._eventParsers;
  353. for (let [, {hasListeners}] of parsers) {
  354. try {
  355. if (hasListeners && hasListeners(this.rawNode)) {
  356. return true;
  357. }
  358. } catch (e) {
  359. // An object attached to the node looked like a listener but wasn't...
  360. // do nothing.
  361. }
  362. }
  363. return false;
  364. },
  365. writeAttrs: function () {
  366. if (!this.rawNode.attributes) {
  367. return undefined;
  368. }
  369. return [...this.rawNode.attributes].map(attr => {
  370. return {namespace: attr.namespace, name: attr.name, value: attr.value };
  371. });
  372. },
  373. writePseudoClassLocks: function () {
  374. if (this.rawNode.nodeType !== Ci.nsIDOMNode.ELEMENT_NODE) {
  375. return undefined;
  376. }
  377. let ret = undefined;
  378. for (let pseudo of PSEUDO_CLASSES) {
  379. if (DOMUtils.hasPseudoClassLock(this.rawNode, pseudo)) {
  380. ret = ret || [];
  381. ret.push(pseudo);
  382. }
  383. }
  384. return ret;
  385. },
  386. /**
  387. * Gets event listeners and adds their information to the events array.
  388. *
  389. * @param {Node} node
  390. * Node for which we are to get listeners.
  391. */
  392. getEventListeners: function (node) {
  393. let parsers = this._eventParsers;
  394. let dbg = this.parent().tabActor.makeDebugger();
  395. let listeners = [];
  396. for (let [, {getListeners, normalizeHandler}] of parsers) {
  397. try {
  398. let eventInfos = getListeners(node);
  399. if (!eventInfos) {
  400. continue;
  401. }
  402. for (let eventInfo of eventInfos) {
  403. if (normalizeHandler) {
  404. eventInfo.normalizeHandler = normalizeHandler;
  405. }
  406. this.processHandlerForEvent(node, listeners, dbg, eventInfo);
  407. }
  408. } catch (e) {
  409. // An object attached to the node looked like a listener but wasn't...
  410. // do nothing.
  411. }
  412. }
  413. listeners.sort((a, b) => {
  414. return a.type.localeCompare(b.type);
  415. });
  416. return listeners;
  417. },
  418. /**
  419. * Process a handler
  420. *
  421. * @param {Node} node
  422. * The node for which we want information.
  423. * @param {Array} events
  424. * The events array contains all event objects that we have gathered
  425. * so far.
  426. * @param {Debugger} dbg
  427. * JSDebugger instance.
  428. * @param {Object} eventInfo
  429. * See event-parsers.js.registerEventParser() for a description of the
  430. * eventInfo object.
  431. *
  432. * @return {Array}
  433. * An array of objects where a typical object looks like this:
  434. * {
  435. * type: "click",
  436. * handler: function() { doSomething() },
  437. * origin: "http://www.mozilla.com",
  438. * searchString: 'onclick="doSomething()"',
  439. * tags: tags,
  440. * DOM0: true,
  441. * capturing: true,
  442. * hide: {
  443. * dom0: true
  444. * }
  445. * }
  446. */
  447. processHandlerForEvent: function (node, listeners, dbg, eventInfo) {
  448. let type = eventInfo.type || "";
  449. let handler = eventInfo.handler;
  450. let tags = eventInfo.tags || "";
  451. let hide = eventInfo.hide || {};
  452. let override = eventInfo.override || {};
  453. let global = Cu.getGlobalForObject(handler);
  454. let globalDO = dbg.addDebuggee(global);
  455. let listenerDO = globalDO.makeDebuggeeValue(handler);
  456. if (eventInfo.normalizeHandler) {
  457. listenerDO = eventInfo.normalizeHandler(listenerDO);
  458. }
  459. // If the listener is an object with a 'handleEvent' method, use that.
  460. if (listenerDO.class === "Object" || listenerDO.class === "XULElement") {
  461. let desc;
  462. while (!desc && listenerDO) {
  463. desc = listenerDO.getOwnPropertyDescriptor("handleEvent");
  464. listenerDO = listenerDO.proto;
  465. }
  466. if (desc && desc.value) {
  467. listenerDO = desc.value;
  468. }
  469. }
  470. if (listenerDO.isBoundFunction) {
  471. listenerDO = listenerDO.boundTargetFunction;
  472. }
  473. let script = listenerDO.script;
  474. let scriptSource = script.source.text;
  475. let functionSource =
  476. scriptSource.substr(script.sourceStart, script.sourceLength);
  477. /*
  478. The script returned is the whole script and
  479. scriptSource.substr(script.sourceStart, script.sourceLength) returns
  480. something like this:
  481. () { doSomething(); }
  482. So we need to use some regex magic to get the appropriate function info
  483. e.g.:
  484. () => { ... }
  485. function doit() { ... }
  486. doit: function() { ... }
  487. es6func() { ... }
  488. var|let|const foo = function () { ... }
  489. function generator*() { ... }
  490. */
  491. let scriptBeforeFunc = scriptSource.substr(0, script.sourceStart);
  492. let matches = scriptBeforeFunc.match(RX_FUNC_NAME);
  493. if (matches && matches.length > 0) {
  494. functionSource = matches[0].trim() + functionSource;
  495. }
  496. let dom0 = false;
  497. if (typeof node.hasAttribute !== "undefined") {
  498. dom0 = !!node.hasAttribute("on" + type);
  499. } else {
  500. dom0 = !!node["on" + type];
  501. }
  502. let line = script.startLine;
  503. let url = script.url;
  504. let origin = url + (dom0 ? "" : ":" + line);
  505. let searchString;
  506. if (dom0) {
  507. searchString = "on" + type + "=\"" + script.source.text + "\"";
  508. } else {
  509. scriptSource = " " + scriptSource;
  510. }
  511. let eventObj = {
  512. type: typeof override.type !== "undefined" ? override.type : type,
  513. handler: functionSource.trim(),
  514. origin: typeof override.origin !== "undefined" ?
  515. override.origin : origin,
  516. searchString: typeof override.searchString !== "undefined" ?
  517. override.searchString : searchString,
  518. tags: tags,
  519. DOM0: typeof override.dom0 !== "undefined" ? override.dom0 : dom0,
  520. capturing: typeof override.capturing !== "undefined" ?
  521. override.capturing : eventInfo.capturing,
  522. hide: hide
  523. };
  524. listeners.push(eventObj);
  525. dbg.removeDebuggee(globalDO);
  526. },
  527. /**
  528. * Returns a LongStringActor with the node's value.
  529. */
  530. getNodeValue: function () {
  531. return new LongStringActor(this.conn, this.rawNode.nodeValue || "");
  532. },
  533. /**
  534. * Set the node's value to a given string.
  535. */
  536. setNodeValue: function (value) {
  537. this.rawNode.nodeValue = value;
  538. },
  539. /**
  540. * Get a unique selector string for this node.
  541. */
  542. getUniqueSelector: function () {
  543. if (Cu.isDeadWrapper(this.rawNode)) {
  544. return "";
  545. }
  546. return CssLogic.findCssSelector(this.rawNode);
  547. },
  548. /**
  549. * Get the full CSS path for this node.
  550. *
  551. * @return {String} A CSS selector with a part for the node and each of its ancestors.
  552. */
  553. getCssPath: function () {
  554. if (Cu.isDeadWrapper(this.rawNode)) {
  555. return "";
  556. }
  557. return CssLogic.getCssPath(this.rawNode);
  558. },
  559. /**
  560. * Scroll the selected node into view.
  561. */
  562. scrollIntoView: function () {
  563. this.rawNode.scrollIntoView(true);
  564. },
  565. /**
  566. * Get the node's image data if any (for canvas and img nodes).
  567. * Returns an imageData object with the actual data being a LongStringActor
  568. * and a size json object.
  569. * The image data is transmitted as a base64 encoded png data-uri.
  570. * The method rejects if the node isn't an image or if the image is missing
  571. *
  572. * Accepts a maxDim request parameter to resize images that are larger. This
  573. * is important as the resizing occurs server-side so that image-data being
  574. * transfered in the longstring back to the client will be that much smaller
  575. */
  576. getImageData: function (maxDim) {
  577. return imageToImageData(this.rawNode, maxDim).then(imageData => {
  578. return {
  579. data: LongStringActor(this.conn, imageData.data),
  580. size: imageData.size
  581. };
  582. });
  583. },
  584. /**
  585. * Get all event listeners that are listening on this node.
  586. */
  587. getEventListenerInfo: function () {
  588. if (this.rawNode.nodeName.toLowerCase() === "html") {
  589. return this.getEventListeners(this.rawNode.ownerGlobal);
  590. }
  591. return this.getEventListeners(this.rawNode);
  592. },
  593. /**
  594. * Modify a node's attributes. Passed an array of modifications
  595. * similar in format to "attributes" mutations.
  596. * {
  597. * attributeName: <string>
  598. * attributeNamespace: <optional string>
  599. * newValue: <optional string> - If null or undefined, the attribute
  600. * will be removed.
  601. * }
  602. *
  603. * Returns when the modifications have been made. Mutations will
  604. * be queued for any changes made.
  605. */
  606. modifyAttributes: function (modifications) {
  607. let rawNode = this.rawNode;
  608. for (let change of modifications) {
  609. if (change.newValue == null) {
  610. if (change.attributeNamespace) {
  611. rawNode.removeAttributeNS(change.attributeNamespace,
  612. change.attributeName);
  613. } else {
  614. rawNode.removeAttribute(change.attributeName);
  615. }
  616. } else if (change.attributeNamespace) {
  617. rawNode.setAttributeNS(change.attributeNamespace, change.attributeName,
  618. change.newValue);
  619. } else {
  620. rawNode.setAttribute(change.attributeName, change.newValue);
  621. }
  622. }
  623. },
  624. /**
  625. * Given the font and fill style, get the image data of a canvas with the
  626. * preview text and font.
  627. * Returns an imageData object with the actual data being a LongStringActor
  628. * and the width of the text as a string.
  629. * The image data is transmitted as a base64 encoded png data-uri.
  630. */
  631. getFontFamilyDataURL: function (font, fillStyle = "black") {
  632. let doc = this.rawNode.ownerDocument;
  633. let options = {
  634. previewText: FONT_FAMILY_PREVIEW_TEXT,
  635. previewFontSize: FONT_FAMILY_PREVIEW_TEXT_SIZE,
  636. fillStyle: fillStyle
  637. };
  638. let { dataURL, size } = getFontPreviewData(font, doc, options);
  639. return { data: LongStringActor(this.conn, dataURL), size: size };
  640. }
  641. });
  642. /**
  643. * Server side of a node list as returned by querySelectorAll()
  644. */
  645. var NodeListActor = exports.NodeListActor = protocol.ActorClassWithSpec(nodeListSpec, {
  646. typeName: "domnodelist",
  647. initialize: function (walker, nodeList) {
  648. protocol.Actor.prototype.initialize.call(this);
  649. this.walker = walker;
  650. this.nodeList = nodeList || [];
  651. },
  652. destroy: function () {
  653. protocol.Actor.prototype.destroy.call(this);
  654. },
  655. /**
  656. * Instead of storing a connection object, the NodeActor gets its connection
  657. * from its associated walker.
  658. */
  659. get conn() {
  660. return this.walker.conn;
  661. },
  662. /**
  663. * Items returned by this actor should belong to the parent walker.
  664. */
  665. marshallPool: function () {
  666. return this.walker;
  667. },
  668. // Returns the JSON representation of this object over the wire.
  669. form: function () {
  670. return {
  671. actor: this.actorID,
  672. length: this.nodeList ? this.nodeList.length : 0
  673. };
  674. },
  675. /**
  676. * Get a single node from the node list.
  677. */
  678. item: function (index) {
  679. return this.walker.attachElement(this.nodeList[index]);
  680. },
  681. /**
  682. * Get a range of the items from the node list.
  683. */
  684. items: function (start = 0, end = this.nodeList.length) {
  685. let items = Array.prototype.slice.call(this.nodeList, start, end)
  686. .map(item => this.walker._ref(item));
  687. return this.walker.attachElements(items);
  688. },
  689. release: function () {}
  690. });
  691. /**
  692. * Server side of the DOM walker.
  693. */
  694. var WalkerActor = protocol.ActorClassWithSpec(walkerSpec, {
  695. /**
  696. * Create the WalkerActor
  697. * @param DebuggerServerConnection conn
  698. * The server connection.
  699. */
  700. initialize: function (conn, tabActor, options) {
  701. protocol.Actor.prototype.initialize.call(this, conn);
  702. this.tabActor = tabActor;
  703. this.rootWin = tabActor.window;
  704. this.rootDoc = this.rootWin.document;
  705. this._refMap = new Map();
  706. this._pendingMutations = [];
  707. this._activePseudoClassLocks = new Set();
  708. this.showAllAnonymousContent = options.showAllAnonymousContent;
  709. this.walkerSearch = new WalkerSearch(this);
  710. // Nodes which have been removed from the client's known
  711. // ownership tree are considered "orphaned", and stored in
  712. // this set.
  713. this._orphaned = new Set();
  714. // The client can tell the walker that it is interested in a node
  715. // even when it is orphaned with the `retainNode` method. This
  716. // list contains orphaned nodes that were so retained.
  717. this._retainedOrphans = new Set();
  718. this.onMutations = this.onMutations.bind(this);
  719. this.onFrameLoad = this.onFrameLoad.bind(this);
  720. this.onFrameUnload = this.onFrameUnload.bind(this);
  721. events.on(tabActor, "will-navigate", this.onFrameUnload);
  722. events.on(tabActor, "navigate", this.onFrameLoad);
  723. // Ensure that the root document node actor is ready and
  724. // managed.
  725. this.rootNode = this.document();
  726. this.layoutChangeObserver = getLayoutChangesObserver(this.tabActor);
  727. this._onReflows = this._onReflows.bind(this);
  728. this.layoutChangeObserver.on("reflows", this._onReflows);
  729. this._onResize = this._onResize.bind(this);
  730. this.layoutChangeObserver.on("resize", this._onResize);
  731. this._onEventListenerChange = this._onEventListenerChange.bind(this);
  732. eventListenerService.addListenerChangeListener(this._onEventListenerChange);
  733. },
  734. /**
  735. * Callback for eventListenerService.addListenerChangeListener
  736. * @param nsISimpleEnumerator changesEnum
  737. * enumerator of nsIEventListenerChange
  738. */
  739. _onEventListenerChange: function (changesEnum) {
  740. let changes = changesEnum.enumerate();
  741. while (changes.hasMoreElements()) {
  742. let current = changes.getNext().QueryInterface(Ci.nsIEventListenerChange);
  743. let target = current.target;
  744. if (this._refMap.has(target)) {
  745. let actor = this.getNode(target);
  746. let mutation = {
  747. type: "events",
  748. target: actor.actorID,
  749. hasEventListeners: actor._hasEventListeners
  750. };
  751. this.queueMutation(mutation);
  752. }
  753. }
  754. },
  755. // Returns the JSON representation of this object over the wire.
  756. form: function () {
  757. return {
  758. actor: this.actorID,
  759. root: this.rootNode.form(),
  760. traits: {
  761. // FF42+ Inspector starts managing the Walker, while the inspector also
  762. // starts cleaning itself up automatically on client disconnection.
  763. // So that there is no need to manually release the walker anymore.
  764. autoReleased: true,
  765. // XXX: It seems silly that we need to tell the front which capabilities
  766. // its actor has in this way when the target can use actorHasMethod. If
  767. // this was ported to the protocol (Bug 1157048) we could call that
  768. // inside of custom front methods and not need to do traits for this.
  769. multiFrameQuerySelectorAll: true,
  770. textSearch: true,
  771. }
  772. };
  773. },
  774. toString: function () {
  775. return "[WalkerActor " + this.actorID + "]";
  776. },
  777. getDocumentWalker: function (node, whatToShow) {
  778. // Allow native anon content (like <video> controls) if preffed on
  779. let nodeFilter = this.showAllAnonymousContent
  780. ? allAnonymousContentTreeWalkerFilter
  781. : standardTreeWalkerFilter;
  782. return new DocumentWalker(node, this.rootWin, whatToShow, nodeFilter);
  783. },
  784. destroy: function () {
  785. if (this._destroyed) {
  786. return;
  787. }
  788. this._destroyed = true;
  789. protocol.Actor.prototype.destroy.call(this);
  790. try {
  791. this.clearPseudoClassLocks();
  792. this._activePseudoClassLocks = null;
  793. this._hoveredNode = null;
  794. this.rootWin = null;
  795. this.rootDoc = null;
  796. this.rootNode = null;
  797. this.layoutHelpers = null;
  798. this._orphaned = null;
  799. this._retainedOrphans = null;
  800. this._refMap = null;
  801. events.off(this.tabActor, "will-navigate", this.onFrameUnload);
  802. events.off(this.tabActor, "navigate", this.onFrameLoad);
  803. this.onFrameLoad = null;
  804. this.onFrameUnload = null;
  805. this.walkerSearch.destroy();
  806. this.layoutChangeObserver.off("reflows", this._onReflows);
  807. this.layoutChangeObserver.off("resize", this._onResize);
  808. this.layoutChangeObserver = null;
  809. releaseLayoutChangesObserver(this.tabActor);
  810. eventListenerService.removeListenerChangeListener(
  811. this._onEventListenerChange);
  812. this.onMutations = null;
  813. this.layoutActor = null;
  814. this.tabActor = null;
  815. events.emit(this, "destroyed");
  816. } catch (e) {
  817. console.error(e);
  818. }
  819. },
  820. release: function () {},
  821. unmanage: function (actor) {
  822. if (actor instanceof NodeActor) {
  823. if (this._activePseudoClassLocks &&
  824. this._activePseudoClassLocks.has(actor)) {
  825. this.clearPseudoClassLocks(actor);
  826. }
  827. this._refMap.delete(actor.rawNode);
  828. }
  829. protocol.Actor.prototype.unmanage.call(this, actor);
  830. },
  831. /**
  832. * Determine if the walker has come across this DOM node before.
  833. * @param {DOMNode} rawNode
  834. * @return {Boolean}
  835. */
  836. hasNode: function (rawNode) {
  837. return this._refMap.has(rawNode);
  838. },
  839. /**
  840. * If the walker has come across this DOM node before, then get the
  841. * corresponding node actor.
  842. * @param {DOMNode} rawNode
  843. * @return {NodeActor}
  844. */
  845. getNode: function (rawNode) {
  846. return this._refMap.get(rawNode);
  847. },
  848. _ref: function (node) {
  849. let actor = this.getNode(node);
  850. if (actor) {
  851. return actor;
  852. }
  853. actor = new NodeActor(this, node);
  854. // Add the node actor as a child of this walker actor, assigning
  855. // it an actorID.
  856. this.manage(actor);
  857. this._refMap.set(node, actor);
  858. if (node.nodeType === Ci.nsIDOMNode.DOCUMENT_NODE) {
  859. actor.watchDocument(this.onMutations);
  860. }
  861. return actor;
  862. },
  863. _onReflows: function (reflows) {
  864. // Going through the nodes the walker knows about, see which ones have
  865. // had their display changed and send a display-change event if any
  866. let changes = [];
  867. for (let [node, actor] of this._refMap) {
  868. if (Cu.isDeadWrapper(node)) {
  869. continue;
  870. }
  871. let isDisplayed = actor.isDisplayed;
  872. if (isDisplayed !== actor.wasDisplayed) {
  873. changes.push(actor);
  874. // Updating the original value
  875. actor.wasDisplayed = isDisplayed;
  876. }
  877. }
  878. if (changes.length) {
  879. events.emit(this, "display-change", changes);
  880. }
  881. },
  882. /**
  883. * When the browser window gets resized, relay the event to the front.
  884. */
  885. _onResize: function () {
  886. events.emit(this, "resize");
  887. },
  888. /**
  889. * This is kept for backward-compatibility reasons with older remote targets.
  890. * Targets prior to bug 916443.
  891. *
  892. * pick/cancelPick are used to pick a node on click on the content
  893. * document. But in their implementation prior to bug 916443, they don't allow
  894. * highlighting on hover.
  895. * The client-side now uses the highlighter actor's pick and cancelPick
  896. * methods instead. The client-side uses the the highlightable trait found in
  897. * the root actor to determine which version of pick to use.
  898. *
  899. * As for highlight, the new highlighter actor is used instead of the walker's
  900. * highlight method. Same here though, the client-side uses the highlightable
  901. * trait to dertermine which to use.
  902. *
  903. * Keeping these actor methods for now allows newer client-side debuggers to
  904. * inspect fxos 1.2 remote targets or older firefox desktop remote targets.
  905. */
  906. pick: function () {},
  907. cancelPick: function () {},
  908. highlight: function (node) {},
  909. /**
  910. * Ensures that the node is attached and it can be accessed from the root.
  911. *
  912. * @param {(Node|NodeActor)} nodes The nodes
  913. * @return {Object} An object compatible with the disconnectedNode type.
  914. */
  915. attachElement: function (node) {
  916. let { nodes, newParents } = this.attachElements([node]);
  917. return {
  918. node: nodes[0],
  919. newParents: newParents
  920. };
  921. },
  922. /**
  923. * Ensures that the nodes are attached and they can be accessed from the root.
  924. *
  925. * @param {(Node[]|NodeActor[])} nodes The nodes
  926. * @return {Object} An object compatible with the disconnectedNodeArray type.
  927. */
  928. attachElements: function (nodes) {
  929. let nodeActors = [];
  930. let newParents = new Set();
  931. for (let node of nodes) {
  932. if (!(node instanceof NodeActor)) {
  933. // If an anonymous node was passed in and we aren't supposed to know
  934. // about it, then consult with the document walker as the source of
  935. // truth about which elements exist.
  936. if (!this.showAllAnonymousContent && isAnonymous(node)) {
  937. node = this.getDocumentWalker(node).currentNode;
  938. }
  939. node = this._ref(node);
  940. }
  941. this.ensurePathToRoot(node, newParents);
  942. // If nodes may be an array of raw nodes, we're sure to only have
  943. // NodeActors with the following array.
  944. nodeActors.push(node);
  945. }
  946. return {
  947. nodes: nodeActors,
  948. newParents: [...newParents]
  949. };
  950. },
  951. /**
  952. * Return the document node that contains the given node,
  953. * or the root node if no node is specified.
  954. * @param NodeActor node
  955. * The node whose document is needed, or null to
  956. * return the root.
  957. */
  958. document: function (node) {
  959. let doc = isNodeDead(node) ? this.rootDoc : nodeDocument(node.rawNode);
  960. return this._ref(doc);
  961. },
  962. /**
  963. * Return the documentElement for the document containing the
  964. * given node.
  965. * @param NodeActor node
  966. * The node whose documentElement is requested, or null
  967. * to use the root document.
  968. */
  969. documentElement: function (node) {
  970. let elt = isNodeDead(node)
  971. ? this.rootDoc.documentElement
  972. : nodeDocument(node.rawNode).documentElement;
  973. return this._ref(elt);
  974. },
  975. /**
  976. * Return all parents of the given node, ordered from immediate parent
  977. * to root.
  978. * @param NodeActor node
  979. * The node whose parents are requested.
  980. * @param object options
  981. * Named options, including:
  982. * `sameDocument`: If true, parents will be restricted to the same
  983. * document as the node.
  984. * `sameTypeRootTreeItem`: If true, this will not traverse across
  985. * different types of docshells.
  986. */
  987. parents: function (node, options = {}) {
  988. if (isNodeDead(node)) {
  989. return [];
  990. }
  991. let walker = this.getDocumentWalker(node.rawNode);
  992. let parents = [];
  993. let cur;
  994. while ((cur = walker.parentNode())) {
  995. if (options.sameDocument &&
  996. nodeDocument(cur) != nodeDocument(node.rawNode)) {
  997. break;
  998. }
  999. if (options.sameTypeRootTreeItem &&
  1000. nodeDocshell(cur).sameTypeRootTreeItem !=
  1001. nodeDocshell(node.rawNode).sameTypeRootTreeItem) {
  1002. break;
  1003. }
  1004. parents.push(this._ref(cur));
  1005. }
  1006. return parents;
  1007. },
  1008. parentNode: function (node) {
  1009. let walker = this.getDocumentWalker(node.rawNode);
  1010. let parent = walker.parentNode();
  1011. if (parent) {
  1012. return this._ref(parent);
  1013. }
  1014. return null;
  1015. },
  1016. /**
  1017. * If the given NodeActor only has a single text node as a child with a text
  1018. * content small enough to be inlined, return that child's NodeActor.
  1019. *
  1020. * @param NodeActor node
  1021. */
  1022. inlineTextChild: function (node) {
  1023. // Quick checks to prevent creating a new walker if possible.
  1024. if (node.isBeforePseudoElement ||
  1025. node.isAfterPseudoElement ||
  1026. node.rawNode.nodeType != Ci.nsIDOMNode.ELEMENT_NODE ||
  1027. node.rawNode.children.length > 0) {
  1028. return undefined;
  1029. }
  1030. let docWalker = this.getDocumentWalker(node.rawNode);
  1031. let firstChild = docWalker.firstChild();
  1032. // Bail out if:
  1033. // - more than one child
  1034. // - unique child is not a text node
  1035. // - unique child is a text node, but is too long to be inlined
  1036. if (!firstChild ||
  1037. docWalker.nextSibling() ||
  1038. firstChild.nodeType !== Ci.nsIDOMNode.TEXT_NODE ||
  1039. firstChild.nodeValue.length > gValueSummaryLength
  1040. ) {
  1041. return undefined;
  1042. }
  1043. return this._ref(firstChild);
  1044. },
  1045. /**
  1046. * Mark a node as 'retained'.
  1047. *
  1048. * A retained node is not released when `releaseNode` is called on its
  1049. * parent, or when a parent is released with the `cleanup` option to
  1050. * `getMutations`.
  1051. *
  1052. * When a retained node's parent is released, a retained mode is added to
  1053. * the walker's "retained orphans" list.
  1054. *
  1055. * Retained nodes can be deleted by providing the `force` option to
  1056. * `releaseNode`. They will also be released when their document
  1057. * has been destroyed.
  1058. *
  1059. * Retaining a node makes no promise about its children; They can
  1060. * still be removed by normal means.
  1061. */
  1062. retainNode: function (node) {
  1063. node.retained = true;
  1064. },
  1065. /**
  1066. * Remove the 'retained' mark from a node. If the node was a
  1067. * retained orphan, release it.
  1068. */
  1069. unretainNode: function (node) {
  1070. node.retained = false;
  1071. if (this._retainedOrphans.has(node)) {
  1072. this._retainedOrphans.delete(node);
  1073. this.releaseNode(node);
  1074. }
  1075. },
  1076. /**
  1077. * Release actors for a node and all child nodes.
  1078. */
  1079. releaseNode: function (node, options = {}) {
  1080. if (isNodeDead(node)) {
  1081. return;
  1082. }
  1083. if (node.retained && !options.force) {
  1084. this._retainedOrphans.add(node);
  1085. return;
  1086. }
  1087. if (node.retained) {
  1088. // Forcing a retained node to go away.
  1089. this._retainedOrphans.delete(node);
  1090. }
  1091. let walker = this.getDocumentWalker(node.rawNode);
  1092. let child = walker.firstChild();
  1093. while (child) {
  1094. let childActor = this.getNode(child);
  1095. if (childActor) {
  1096. this.releaseNode(childActor, options);
  1097. }
  1098. child = walker.nextSibling();
  1099. }
  1100. node.destroy();
  1101. },
  1102. /**
  1103. * Add any nodes between `node` and the walker's root node that have not
  1104. * yet been seen by the client.
  1105. */
  1106. ensurePathToRoot: function (node, newParents = new Set()) {
  1107. if (!node) {
  1108. return newParents;
  1109. }
  1110. let walker = this.getDocumentWalker(node.rawNode);
  1111. let cur;
  1112. while ((cur = walker.parentNode())) {
  1113. let parent = this.getNode(cur);
  1114. if (!parent) {
  1115. // This parent didn't exist, so hasn't been seen by the client yet.
  1116. newParents.add(this._ref(cur));
  1117. } else {
  1118. // This parent did exist, so the client knows about it.
  1119. return newParents;
  1120. }
  1121. }
  1122. return newParents;
  1123. },
  1124. /**
  1125. * Return children of the given node. By default this method will return
  1126. * all children of the node, but there are options that can restrict this
  1127. * to a more manageable subset.
  1128. *
  1129. * @param NodeActor node
  1130. * The node whose children you're curious about.
  1131. * @param object options
  1132. * Named options:
  1133. * `maxNodes`: The set of nodes returned by the method will be no longer
  1134. * than maxNodes.
  1135. * `start`: If a node is specified, the list of nodes will start
  1136. * with the given child. Mutally exclusive with `center`.
  1137. * `center`: If a node is specified, the given node will be as centered
  1138. * as possible in the list, given how close to the ends of the child
  1139. * list it is. Mutually exclusive with `start`.
  1140. * `whatToShow`: A bitmask of node types that should be included. See
  1141. * https://developer.mozilla.org/en-US/docs/Web/API/NodeFilter.
  1142. *
  1143. * @returns an object with three items:
  1144. * hasFirst: true if the first child of the node is included in the list.
  1145. * hasLast: true if the last child of the node is included in the list.
  1146. * nodes: Child nodes returned by the request.
  1147. */
  1148. children: function (node, options = {}) {
  1149. if (isNodeDead(node)) {
  1150. return { hasFirst: true, hasLast: true, nodes: [] };
  1151. }
  1152. if (options.center && options.start) {
  1153. throw Error("Can't specify both 'center' and 'start' options.");
  1154. }
  1155. let maxNodes = options.maxNodes || -1;
  1156. if (maxNodes == -1) {
  1157. maxNodes = Number.MAX_VALUE;
  1158. }
  1159. // We're going to create a few document walkers with the same filter,
  1160. // make it easier.
  1161. let getFilteredWalker = documentWalkerNode => {
  1162. return this.getDocumentWalker(documentWalkerNode, options.whatToShow);
  1163. };
  1164. // Need to know the first and last child.
  1165. let rawNode = node.rawNode;
  1166. let firstChild = getFilteredWalker(rawNode).firstChild();
  1167. let lastChild = getFilteredWalker(rawNode).lastChild();
  1168. if (!firstChild) {
  1169. // No children, we're done.
  1170. return { hasFirst: true, hasLast: true, nodes: [] };
  1171. }
  1172. let start;
  1173. if (options.center) {
  1174. start = options.center.rawNode;
  1175. } else if (options.start) {
  1176. start = options.start.rawNode;
  1177. } else {
  1178. start = firstChild;
  1179. }
  1180. let nodes = [];
  1181. // Start by reading backward from the starting point if we're centering...
  1182. let backwardWalker = getFilteredWalker(start);
  1183. if (start != firstChild && options.center) {
  1184. backwardWalker.previousSibling();
  1185. let backwardCount = Math.floor(maxNodes / 2);
  1186. let backwardNodes = this._readBackward(backwardWalker, backwardCount);
  1187. nodes = backwardNodes;
  1188. }
  1189. // Then read forward by any slack left in the max children...
  1190. let forwardWalker = getFilteredWalker(start);
  1191. let forwardCount = maxNodes - nodes.length;
  1192. nodes = nodes.concat(this._readForward(forwardWalker, forwardCount));
  1193. // If there's any room left, it means we've run all the way to the end.
  1194. // If we're centering, check if there are more items to read at the front.
  1195. let remaining = maxNodes - nodes.length;
  1196. if (options.center && remaining > 0 && nodes[0].rawNode != firstChild) {
  1197. let firstNodes = this._readBackward(backwardWalker, remaining);
  1198. // Then put it all back together.
  1199. nodes = firstNodes.concat(nodes);
  1200. }
  1201. return {
  1202. hasFirst: nodes[0].rawNode == firstChild,
  1203. hasLast: nodes[nodes.length - 1].rawNode == lastChild,
  1204. nodes: nodes
  1205. };
  1206. },
  1207. /**
  1208. * Return siblings of the given node. By default this method will return
  1209. * all siblings of the node, but there are options that can restrict this
  1210. * to a more manageable subset.
  1211. *
  1212. * If `start` or `center` are not specified, this method will center on the
  1213. * node whose siblings are requested.
  1214. *
  1215. * @param NodeActor node
  1216. * The node whose children you're curious about.
  1217. * @param object options
  1218. * Named options:
  1219. * `maxNodes`: The set of nodes returned by the method will be no longer
  1220. * than maxNodes.
  1221. * `start`: If a node is specified, the list of nodes will start
  1222. * with the given child. Mutally exclusive with `center`.
  1223. * `center`: If a node is specified, the given node will be as centered
  1224. * as possible in the list, given how close to the ends of the child
  1225. * list it is. Mutually exclusive with `start`.
  1226. * `whatToShow`: A bitmask of node types that should be included. See
  1227. * https://developer.mozilla.org/en-US/docs/Web/API/NodeFilter.
  1228. *
  1229. * @returns an object with three items:
  1230. * hasFirst: true if the first child of the node is included in the list.
  1231. * hasLast: true if the last child of the node is included in the list.
  1232. * nodes: Child nodes returned by the request.
  1233. */
  1234. siblings: function (node, options = {}) {
  1235. if (isNodeDead(node)) {
  1236. return { hasFirst: true, hasLast: true, nodes: [] };
  1237. }
  1238. let parentNode = this.getDocumentWalker(node.rawNode, options.whatToShow)
  1239. .parentNode();
  1240. if (!parentNode) {
  1241. return {
  1242. hasFirst: true,
  1243. hasLast: true,
  1244. nodes: [node]
  1245. };
  1246. }
  1247. if (!(options.start || options.center)) {
  1248. options.center = node;
  1249. }
  1250. return this.children(this._ref(parentNode), options);
  1251. },
  1252. /**
  1253. * Get the next sibling of a given node. Getting nodes one at a time
  1254. * might be inefficient, be careful.
  1255. *
  1256. * @param object options
  1257. * Named options:
  1258. * `whatToShow`: A bitmask of node types that should be included. See
  1259. * https://developer.mozilla.org/en-US/docs/Web/API/NodeFilter.
  1260. */
  1261. nextSibling: function (node, options = {}) {
  1262. if (isNodeDead(node)) {
  1263. return null;
  1264. }
  1265. let walker = this.getDocumentWalker(node.rawNode, options.whatToShow);
  1266. let sibling = walker.nextSibling();
  1267. return sibling ? this._ref(sibling) : null;
  1268. },
  1269. /**
  1270. * Get the previous sibling of a given node. Getting nodes one at a time
  1271. * might be inefficient, be careful.
  1272. *
  1273. * @param object options
  1274. * Named options:
  1275. * `whatToShow`: A bitmask of node types that should be included. See
  1276. * https://developer.mozilla.org/en-US/docs/Web/API/NodeFilter.
  1277. */
  1278. previousSibling: function (node, options = {}) {
  1279. if (isNodeDead(node)) {
  1280. return null;
  1281. }
  1282. let walker = this.getDocumentWalker(node.rawNode, options.whatToShow);
  1283. let sibling = walker.previousSibling();
  1284. return sibling ? this._ref(sibling) : null;
  1285. },
  1286. /**
  1287. * Helper function for the `children` method: Read forward in the sibling
  1288. * list into an array with `count` items, including the current node.
  1289. */
  1290. _readForward: function (walker, count) {
  1291. let ret = [];
  1292. let node = walker.currentNode;
  1293. do {
  1294. ret.push(this._ref(node));
  1295. node = walker.nextSibling();
  1296. } while (node && --count);
  1297. return ret;
  1298. },
  1299. /**
  1300. * Helper function for the `children` method: Read backward in the sibling
  1301. * list into an array with `count` items, including the current node.
  1302. */
  1303. _readBackward: function (walker, count) {
  1304. let ret = [];
  1305. let node = walker.currentNode;
  1306. do {
  1307. ret.push(this._ref(node));
  1308. node = walker.previousSibling();
  1309. } while (node && --count);
  1310. ret.reverse();
  1311. return ret;
  1312. },
  1313. /**
  1314. * Return the node that the parent process has asked to
  1315. * inspect. This node is expected to be stored in gInspectingNode
  1316. * (which is set by a message manager message to the child.js frame
  1317. * script). The node is returned over the remote debugging protocol
  1318. * as a NodeFront.
  1319. */
  1320. findInspectingNode: function () {
  1321. let node = gInspectingNode;
  1322. if (!node) {
  1323. return {};
  1324. }
  1325. return this.attachElement(node);
  1326. },
  1327. /**
  1328. * Return the first node in the document that matches the given selector.
  1329. * See https://developer.mozilla.org/en-US/docs/Web/API/Element.querySelector
  1330. *
  1331. * @param NodeActor baseNode
  1332. * @param string selector
  1333. */
  1334. querySelector: function (baseNode, selector) {
  1335. if (isNodeDead(baseNode)) {
  1336. return {};
  1337. }
  1338. let node = baseNode.rawNode.querySelector(selector);
  1339. if (!node) {
  1340. return {};
  1341. }
  1342. return this.attachElement(node);
  1343. },
  1344. /**
  1345. * Return a NodeListActor with all nodes that match the given selector.
  1346. * See https://developer.mozilla.org/en-US/docs/Web/API/Element.querySelectorAll
  1347. *
  1348. * @param NodeActor baseNode
  1349. * @param string selector
  1350. */
  1351. querySelectorAll: function (baseNode, selector) {
  1352. let nodeList = null;
  1353. try {
  1354. nodeList = baseNode.rawNode.querySelectorAll(selector);
  1355. } catch (e) {
  1356. // Bad selector. Do nothing as the selector can come from a searchbox.
  1357. }
  1358. return new NodeListActor(this, nodeList);
  1359. },
  1360. /**
  1361. * Get a list of nodes that match the given selector in all known frames of
  1362. * the current content page.
  1363. * @param {String} selector.
  1364. * @return {Array}
  1365. */
  1366. _multiFrameQuerySelectorAll: function (selector) {
  1367. let nodes = [];
  1368. for (let {document} of this.tabActor.windows) {
  1369. try {
  1370. nodes = [...nodes, ...document.querySelectorAll(selector)];
  1371. } catch (e) {
  1372. // Bad selector. Do nothing as the selector can come from a searchbox.
  1373. }
  1374. }
  1375. return nodes;
  1376. },
  1377. /**
  1378. * Return a NodeListActor with all nodes that match the given selector in all
  1379. * frames of the current content page.
  1380. * @param {String} selector
  1381. */
  1382. multiFrameQuerySelectorAll: function (selector) {
  1383. return new NodeListActor(this, this._multiFrameQuerySelectorAll(selector));
  1384. },
  1385. /**
  1386. * Search the document for a given string.
  1387. * Results will be searched with the walker-search module (searches through
  1388. * tag names, attribute names and values, and text contents).
  1389. *
  1390. * @returns {searchresult}
  1391. * - {NodeList} list
  1392. * - {Array<Object>} metadata. Extra information with indices that
  1393. * match up with node list.
  1394. */
  1395. search: function (query) {
  1396. let results = this.walkerSearch.search(query);
  1397. let nodeList = new NodeListActor(this, results.map(r => r.node));
  1398. return {
  1399. list: nodeList,
  1400. metadata: []
  1401. };
  1402. },
  1403. /**
  1404. * Returns a list of matching results for CSS selector autocompletion.
  1405. *
  1406. * @param string query
  1407. * The selector query being completed
  1408. * @param string completing
  1409. * The exact token being completed out of the query
  1410. * @param string selectorState
  1411. * One of "pseudo", "id", "tag", "class", "null"
  1412. */
  1413. getSuggestionsForQuery: function (query, completing, selectorState) {
  1414. let sugs = {
  1415. classes: new Map(),
  1416. tags: new Map(),
  1417. ids: new Map()
  1418. };
  1419. let result = [];
  1420. let nodes = null;
  1421. // Filtering and sorting the results so that protocol transfer is miminal.
  1422. switch (selectorState) {
  1423. case "pseudo":
  1424. result = PSEUDO_SELECTORS.filter(item => {
  1425. return item[0].startsWith(":" + completing);
  1426. });
  1427. break;
  1428. case "class":
  1429. if (!query) {
  1430. nodes = this._multiFrameQuerySelectorAll("[class]");
  1431. } else {
  1432. nodes = this._multiFrameQuerySelectorAll(query);
  1433. }
  1434. for (let node of nodes) {
  1435. for (let className of node.classList) {
  1436. sugs.classes.set(className, (sugs.classes.get(className)|0) + 1);
  1437. }
  1438. }
  1439. sugs.classes.delete("");
  1440. sugs.classes.delete(HIDDEN_CLASS);
  1441. for (let [className, count] of sugs.classes) {
  1442. if (className.startsWith(completing)) {
  1443. result.push(["." + CSS.escape(className), count, selectorState]);
  1444. }
  1445. }
  1446. break;
  1447. case "id":
  1448. if (!query) {
  1449. nodes = this._multiFrameQuerySelectorAll("[id]");
  1450. } else {
  1451. nodes = this._multiFrameQuerySelectorAll(query);
  1452. }
  1453. for (let node of nodes) {
  1454. sugs.ids.set(node.id, (sugs.ids.get(node.id)|0) + 1);
  1455. }
  1456. for (let [id, count] of sugs.ids) {
  1457. if (id.startsWith(completing) && id !== "") {
  1458. result.push(["#" + CSS.escape(id), count, selectorState]);
  1459. }
  1460. }
  1461. break;
  1462. case "tag":
  1463. if (!query) {
  1464. nodes = this._multiFrameQuerySelectorAll("*");
  1465. } else {
  1466. nodes = this._multiFrameQuerySelectorAll(query);
  1467. }
  1468. for (let node of nodes) {
  1469. let tag = node.localName;
  1470. sugs.tags.set(tag, (sugs.tags.get(tag)|0) + 1);
  1471. }
  1472. for (let [tag, count] of sugs.tags) {
  1473. if ((new RegExp("^" + completing + ".*", "i")).test(tag)) {
  1474. result.push([tag, count, selectorState]);
  1475. }
  1476. }
  1477. // For state 'tag' (no preceding # or .) and when there's no query (i.e.
  1478. // only one word) then search for the matching classes and ids
  1479. if (!query) {
  1480. result = [
  1481. ...result,
  1482. ...this.getSuggestionsForQuery(null, completing, "class")
  1483. .suggestions,
  1484. ...this.getSuggestionsForQuery(null, completing, "id")
  1485. .suggestions
  1486. ];
  1487. }
  1488. break;
  1489. case "null":
  1490. nodes = this._multiFrameQuerySelectorAll(query);
  1491. for (let node of nodes) {
  1492. sugs.ids.set(node.id, (sugs.ids.get(node.id)|0) + 1);
  1493. let tag = node.localName;
  1494. sugs.tags.set(tag, (sugs.tags.get(tag)|0) + 1);
  1495. for (let className of node.classList) {
  1496. sugs.classes.set(className, (sugs.classes.get(className)|0) + 1);
  1497. }
  1498. }
  1499. for (let [tag, count] of sugs.tags) {
  1500. tag && result.push([tag, count]);
  1501. }
  1502. for (let [id, count] of sugs.ids) {
  1503. id && result.push(["#" + id, count]);
  1504. }
  1505. sugs.classes.delete("");
  1506. sugs.classes.delete(HIDDEN_CLASS);
  1507. for (let [className, count] of sugs.classes) {
  1508. className && result.push(["." + className, count]);
  1509. }
  1510. }
  1511. // Sort by count (desc) and name (asc)
  1512. result = result.sort((a, b) => {
  1513. // Computed a sortable string with first the inverted count, then the name
  1514. let sortA = (10000 - a[1]) + a[0];
  1515. let sortB = (10000 - b[1]) + b[0];
  1516. // Prefixing ids, classes and tags, to group results
  1517. let firstA = a[0].substring(0, 1);
  1518. let firstB = b[0].substring(0, 1);
  1519. if (firstA === "#") {
  1520. sortA = "2" + sortA;
  1521. } else if (firstA === ".") {
  1522. sortA = "1" + sortA;
  1523. } else {
  1524. sortA = "0" + sortA;
  1525. }
  1526. if (firstB === "#") {
  1527. sortB = "2" + sortB;
  1528. } else if (firstB === ".") {
  1529. sortB = "1" + sortB;
  1530. } else {
  1531. sortB = "0" + sortB;
  1532. }
  1533. // String compare
  1534. return sortA.localeCompare(sortB);
  1535. });
  1536. result.slice(0, 25);
  1537. return {
  1538. query: query,
  1539. suggestions: result
  1540. };
  1541. },
  1542. /**
  1543. * Add a pseudo-class lock to a node.
  1544. *
  1545. * @param NodeActor node
  1546. * @param string pseudo
  1547. * A pseudoclass: ':hover', ':active', ':focus'
  1548. * @param options
  1549. * Options object:
  1550. * `parents`: True if the pseudo-class should be added
  1551. * to parent nodes.
  1552. *
  1553. * @returns An empty packet. A "pseudoClassLock" mutation will
  1554. * be queued for any changed nodes.
  1555. */
  1556. addPseudoClassLock: function (node, pseudo, options = {}) {
  1557. if (isNodeDead(node)) {
  1558. return;
  1559. }
  1560. // There can be only one node locked per pseudo, so dismiss all existing
  1561. // ones
  1562. for (let locked of this._activePseudoClassLocks) {
  1563. if (DOMUtils.hasPseudoClassLock(locked.rawNode, pseudo)) {
  1564. this._removePseudoClassLock(locked, pseudo);
  1565. }
  1566. }
  1567. this._addPseudoClassLock(node, pseudo);
  1568. if (!options.parents) {
  1569. return;
  1570. }
  1571. let walker = this.getDocumentWalker(node.rawNode);
  1572. let cur;
  1573. while ((cur = walker.parentNode())) {
  1574. let curNode = this._ref(cur);
  1575. this._addPseudoClassLock(curNode, pseudo);
  1576. }
  1577. },
  1578. _queuePseudoClassMutation: function (node) {
  1579. this.queueMutation({
  1580. target: node.actorID,
  1581. type: "pseudoClassLock",
  1582. pseudoClassLocks: node.writePseudoClassLocks()
  1583. });
  1584. },
  1585. _addPseudoClassLock: function (node, pseudo) {
  1586. if (node.rawNode.nodeType !== Ci.nsIDOMNode.ELEMENT_NODE) {
  1587. return false;
  1588. }
  1589. DOMUtils.addPseudoClassLock(node.rawNode, pseudo);
  1590. this._activePseudoClassLocks.add(node);
  1591. this._queuePseudoClassMutation(node);
  1592. return true;
  1593. },
  1594. _installHelperSheet: function (node) {
  1595. if (!this.installedHelpers) {
  1596. this.installedHelpers = new WeakMap();
  1597. }
  1598. let win = node.rawNode.ownerDocument.defaultView;
  1599. if (!this.installedHelpers.has(win)) {
  1600. let { Style } = require("sdk/stylesheet/style");
  1601. let { attach } = require("sdk/content/mod");
  1602. let style = Style({source: HELPER_SHEET, type: "agent" });
  1603. attach(style, win);
  1604. this.installedHelpers.set(win, style);
  1605. }
  1606. },
  1607. hideNode: function (node) {
  1608. if (isNodeDead(node)) {
  1609. return;
  1610. }
  1611. this._installHelperSheet(node);
  1612. node.rawNode.classList.add(HIDDEN_CLASS);
  1613. },
  1614. unhideNode: function (node) {
  1615. if (isNodeDead(node)) {
  1616. return;
  1617. }
  1618. node.rawNode.classList.remove(HIDDEN_CLASS);
  1619. },
  1620. /**
  1621. * Remove a pseudo-class lock from a node.
  1622. *
  1623. * @param NodeActor node
  1624. * @param string pseudo
  1625. * A pseudoclass: ':hover', ':active', ':focus'
  1626. * @param options
  1627. * Options object:
  1628. * `parents`: True if the pseudo-class should be removed
  1629. * from parent nodes.
  1630. *
  1631. * @returns An empty response. "pseudoClassLock" mutations
  1632. * will be emitted for any changed nodes.
  1633. */
  1634. removePseudoClassLock: function (node, pseudo, options = {}) {
  1635. if (isNodeDead(node)) {
  1636. return;
  1637. }
  1638. this._removePseudoClassLock(node, pseudo);
  1639. // Remove pseudo class for children as we don't want to allow
  1640. // turning it on for some childs without setting it on some parents
  1641. for (let locked of this._activePseudoClassLocks) {
  1642. if (node.rawNode.contains(locked.rawNode) &&
  1643. DOMUtils.hasPseudoClassLock(locked.rawNode, pseudo)) {
  1644. this._removePseudoClassLock(locked, pseudo);
  1645. }
  1646. }
  1647. if (!options.parents) {
  1648. return;
  1649. }
  1650. let walker = this.getDocumentWalker(node.rawNode);
  1651. let cur;
  1652. while ((cur = walker.parentNode())) {
  1653. let curNode = this._ref(cur);
  1654. this._removePseudoClassLock(curNode, pseudo);
  1655. }
  1656. },
  1657. _removePseudoClassLock: function (node, pseudo) {
  1658. if (node.rawNode.nodeType != Ci.nsIDOMNode.ELEMENT_NODE) {
  1659. return false;
  1660. }
  1661. DOMUtils.removePseudoClassLock(node.rawNode, pseudo);
  1662. if (!node.writePseudoClassLocks()) {
  1663. this._activePseudoClassLocks.delete(node);
  1664. }
  1665. this._queuePseudoClassMutation(node);
  1666. return true;
  1667. },
  1668. /**
  1669. * Clear all the pseudo-classes on a given node or all nodes.
  1670. * @param {NodeActor} node Optional node to clear pseudo-classes on
  1671. */
  1672. clearPseudoClassLocks: function (node) {
  1673. if (node && isNodeDead(node)) {
  1674. return;
  1675. }
  1676. if (node) {
  1677. DOMUtils.clearPseudoClassLocks(node.rawNode);
  1678. this._activePseudoClassLocks.delete(node);
  1679. this._queuePseudoClassMutation(node);
  1680. } else {
  1681. for (let locked of this._activePseudoClassLocks) {
  1682. DOMUtils.clearPseudoClassLocks(locked.rawNode);
  1683. this._activePseudoClassLocks.delete(locked);
  1684. this._queuePseudoClassMutation(locked);
  1685. }
  1686. }
  1687. },
  1688. /**
  1689. * Get a node's innerHTML property.
  1690. */
  1691. innerHTML: function (node) {
  1692. let html = "";
  1693. if (!isNodeDead(node)) {
  1694. html = node.rawNode.innerHTML;
  1695. }
  1696. return LongStringActor(this.conn, html);
  1697. },
  1698. /**
  1699. * Set a node's innerHTML property.
  1700. *
  1701. * @param {NodeActor} node The node.
  1702. * @param {string} value The piece of HTML content.
  1703. */
  1704. setInnerHTML: function (node, value) {
  1705. if (isNodeDead(node)) {
  1706. return;
  1707. }
  1708. let rawNode = node.rawNode;
  1709. if (rawNode.nodeType !== rawNode.ownerDocument.ELEMENT_NODE) {
  1710. throw new Error("Can only change innerHTML to element nodes");
  1711. }
  1712. rawNode.innerHTML = value;
  1713. },
  1714. /**
  1715. * Get a node's outerHTML property.
  1716. *
  1717. * @param {NodeActor} node The node.
  1718. */
  1719. outerHTML: function (node) {
  1720. let outerHTML = "";
  1721. if (!isNodeDead(node)) {
  1722. outerHTML = node.rawNode.outerHTML;
  1723. }
  1724. return LongStringActor(this.conn, outerHTML);
  1725. },
  1726. /**
  1727. * Set a node's outerHTML property.
  1728. *
  1729. * @param {NodeActor} node The node.
  1730. * @param {string} value The piece of HTML content.
  1731. */
  1732. setOuterHTML: function (node, value) {
  1733. if (isNodeDead(node)) {
  1734. return;
  1735. }
  1736. let parsedDOM = DOMParser.parseFromString(value, "text/html");
  1737. let rawNode = node.rawNode;
  1738. let parentNode = rawNode.parentNode;
  1739. // Special case for head and body. Setting document.body.outerHTML
  1740. // creates an extra <head> tag, and document.head.outerHTML creates
  1741. // an extra <body>. So instead we will call replaceChild with the
  1742. // parsed DOM, assuming that they aren't trying to set both tags at once.
  1743. if (rawNode.tagName === "BODY") {
  1744. if (parsedDOM.head.innerHTML === "") {
  1745. parentNode.replaceChild(parsedDOM.body, rawNode);
  1746. } else {
  1747. rawNode.outerHTML = value;
  1748. }
  1749. } else if (rawNode.tagName === "HEAD") {
  1750. if (parsedDOM.body.innerHTML === "") {
  1751. parentNode.replaceChild(parsedDOM.head, rawNode);
  1752. } else {
  1753. rawNode.outerHTML = value;
  1754. }
  1755. } else if (node.isDocumentElement()) {
  1756. // Unable to set outerHTML on the document element. Fall back by
  1757. // setting attributes manually, then replace the body and head elements.
  1758. let finalAttributeModifications = [];
  1759. let attributeModifications = {};
  1760. for (let attribute of rawNode.attributes) {
  1761. attributeModifications[attribute.name] = null;
  1762. }
  1763. for (let attribute of parsedDOM.documentElement.attributes) {
  1764. attributeModifications[attribute.name] = attribute.value;
  1765. }
  1766. for (let key in attributeModifications) {
  1767. finalAttributeModifications.push({
  1768. attributeName: key,
  1769. newValue: attributeModifications[key]
  1770. });
  1771. }
  1772. node.modifyAttributes(finalAttributeModifications);
  1773. rawNode.replaceChild(parsedDOM.head, rawNode.querySelector("head"));
  1774. rawNode.replaceChild(parsedDOM.body, rawNode.querySelector("body"));
  1775. } else {
  1776. rawNode.outerHTML = value;
  1777. }
  1778. },
  1779. /**
  1780. * Insert adjacent HTML to a node.
  1781. *
  1782. * @param {Node} node
  1783. * @param {string} position One of "beforeBegin", "afterBegin", "beforeEnd",
  1784. * "afterEnd" (see Element.insertAdjacentHTML).
  1785. * @param {string} value The HTML content.
  1786. */
  1787. insertAdjacentHTML: function (node, position, value) {
  1788. if (isNodeDead(node)) {
  1789. return {node: [], newParents: []};
  1790. }
  1791. let rawNode = node.rawNode;
  1792. let isInsertAsSibling = position === "beforeBegin" ||
  1793. position === "afterEnd";
  1794. // Don't insert anything adjacent to the document element.
  1795. if (isInsertAsSibling && node.isDocumentElement()) {
  1796. throw new Error("Can't insert adjacent element to the root.");
  1797. }
  1798. let rawParentNode = rawNode.parentNode;
  1799. if (!rawParentNode && isInsertAsSibling) {
  1800. throw new Error("Can't insert as sibling without parent node.");
  1801. }
  1802. // We can't use insertAdjacentHTML, because we want to return the nodes
  1803. // being created (so the front can remove them if the user undoes
  1804. // the change). So instead, use Range.createContextualFragment().
  1805. let range = rawNode.ownerDocument.createRange();
  1806. if (position === "beforeBegin" || position === "afterEnd") {
  1807. range.selectNode(rawNode);
  1808. } else {
  1809. range.selectNodeContents(rawNode);
  1810. }
  1811. let docFrag = range.createContextualFragment(value);
  1812. let newRawNodes = Array.from(docFrag.childNodes);
  1813. switch (position) {
  1814. case "beforeBegin":
  1815. rawParentNode.insertBefore(docFrag, rawNode);
  1816. break;
  1817. case "afterEnd":
  1818. // Note: if the second argument is null, rawParentNode.insertBefore
  1819. // behaves like rawParentNode.appendChild.
  1820. rawParentNode.insertBefore(docFrag, rawNode.nextSibling);
  1821. break;
  1822. case "afterBegin":
  1823. rawNode.insertBefore(docFrag, rawNode.firstChild);
  1824. break;
  1825. case "beforeEnd":
  1826. rawNode.appendChild(docFrag);
  1827. break;
  1828. default:
  1829. throw new Error("Invalid position value. Must be either " +
  1830. "'beforeBegin', 'beforeEnd', 'afterBegin' or 'afterEnd'.");
  1831. }
  1832. return this.attachElements(newRawNodes);
  1833. },
  1834. /**
  1835. * Duplicate a specified node
  1836. *
  1837. * @param {NodeActor} node The node to duplicate.
  1838. */
  1839. duplicateNode: function ({rawNode}) {
  1840. let clonedNode = rawNode.cloneNode(true);
  1841. rawNode.parentNode.insertBefore(clonedNode, rawNode.nextSibling);
  1842. },
  1843. /**
  1844. * Test whether a node is a document or a document element.
  1845. *
  1846. * @param {NodeActor} node The node to remove.
  1847. * @return {boolean} True if the node is a document or a document element.
  1848. */
  1849. isDocumentOrDocumentElementNode: function (node) {
  1850. return ((node.rawNode.ownerDocument &&
  1851. node.rawNode.ownerDocument.documentElement === this.rawNode) ||
  1852. node.rawNode.nodeType === Ci.nsIDOMNode.DOCUMENT_NODE);
  1853. },
  1854. /**
  1855. * Removes a node from its parent node.
  1856. *
  1857. * @param {NodeActor} node The node to remove.
  1858. * @returns The node's nextSibling before it was removed.
  1859. */
  1860. removeNode: function (node) {
  1861. if (isNodeDead(node) || this.isDocumentOrDocumentElementNode(node)) {
  1862. throw Error("Cannot remove document, document elements or dead nodes.");
  1863. }
  1864. let nextSibling = this.nextSibling(node);
  1865. node.rawNode.remove();
  1866. // Mutation events will take care of the rest.
  1867. return nextSibling;
  1868. },
  1869. /**
  1870. * Removes an array of nodes from their parent node.
  1871. *
  1872. * @param {NodeActor[]} nodes The nodes to remove.
  1873. */
  1874. removeNodes: function (nodes) {
  1875. // Check that all nodes are valid before processing the removals.
  1876. for (let node of nodes) {
  1877. if (isNodeDead(node) || this.isDocumentOrDocumentElementNode(node)) {
  1878. throw Error("Cannot remove document, document elements or dead nodes");
  1879. }
  1880. }
  1881. for (let node of nodes) {
  1882. node.rawNode.remove();
  1883. // Mutation events will take care of the rest.
  1884. }
  1885. },
  1886. /**
  1887. * Insert a node into the DOM.
  1888. */
  1889. insertBefore: function (node, parent, sibling) {
  1890. if (isNodeDead(node) ||
  1891. isNodeDead(parent) ||
  1892. (sibling && isNodeDead(sibling))) {
  1893. return;
  1894. }
  1895. let rawNode = node.rawNode;
  1896. let rawParent = parent.rawNode;
  1897. let rawSibling = sibling ? sibling.rawNode : null;
  1898. // Don't bother inserting a node if the document position isn't going
  1899. // to change. This prevents needless iframes reloading and mutations.
  1900. if (rawNode.parentNode === rawParent) {
  1901. let currentNextSibling = this.nextSibling(node);
  1902. currentNextSibling = currentNextSibling ? currentNextSibling.rawNode :
  1903. null;
  1904. if (rawNode === rawSibling || currentNextSibling === rawSibling) {
  1905. return;
  1906. }
  1907. }
  1908. rawParent.insertBefore(rawNode, rawSibling);
  1909. },
  1910. /**
  1911. * Editing a node's tagname actually means creating a new node with the same
  1912. * attributes, removing the node and inserting the new one instead.
  1913. * This method does not return anything as mutation events are taking care of
  1914. * informing the consumers about changes.
  1915. */
  1916. editTagName: function (node, tagName) {
  1917. if (isNodeDead(node)) {
  1918. return null;
  1919. }
  1920. let oldNode = node.rawNode;
  1921. // Create a new element with the same attributes as the current element and
  1922. // prepare to replace the current node with it.
  1923. let newNode;
  1924. try {
  1925. newNode = nodeDocument(oldNode).createElement(tagName);
  1926. } catch (x) {
  1927. // Failed to create a new element with that tag name, ignore the change,
  1928. // and signal the error to the front.
  1929. return Promise.reject(new Error("Could not change node's tagName to " + tagName));
  1930. }
  1931. let attrs = oldNode.attributes;
  1932. for (let i = 0; i < attrs.length; i++) {
  1933. newNode.setAttribute(attrs[i].name, attrs[i].value);
  1934. }
  1935. // Insert the new node, and transfer the old node's children.
  1936. oldNode.parentNode.insertBefore(newNode, oldNode);
  1937. while (oldNode.firstChild) {
  1938. newNode.appendChild(oldNode.firstChild);
  1939. }
  1940. oldNode.remove();
  1941. return null;
  1942. },
  1943. /**
  1944. * Get any pending mutation records. Must be called by the client after
  1945. * the `new-mutations` notification is received. Returns an array of
  1946. * mutation records.
  1947. *
  1948. * Mutation records have a basic structure:
  1949. *
  1950. * {
  1951. * type: attributes|characterData|childList,
  1952. * target: <domnode actor ID>,
  1953. * }
  1954. *
  1955. * And additional attributes based on the mutation type:
  1956. *
  1957. * `attributes` type:
  1958. * attributeName: <string> - the attribute that changed
  1959. * attributeNamespace: <string> - the attribute's namespace URI, if any.
  1960. * newValue: <string> - The new value of the attribute, if any.
  1961. *
  1962. * `characterData` type:
  1963. * newValue: <string> - the new nodeValue for the node
  1964. *
  1965. * `childList` type is returned when the set of children for a node
  1966. * has changed. Includes extra data, which can be used by the client to
  1967. * maintain its ownership subtree.
  1968. *
  1969. * added: array of <domnode actor ID> - The list of actors *previously
  1970. * seen by the client* that were added to the target node.
  1971. * removed: array of <domnode actor ID> The list of actors *previously
  1972. * seen by the client* that were removed from the target node.
  1973. * inlineTextChild: If the node now has a single text child, it will
  1974. * be sent here.
  1975. *
  1976. * Actors that are included in a MutationRecord's `removed` but
  1977. * not in an `added` have been removed from the client's ownership
  1978. * tree (either by being moved under a node the client has seen yet
  1979. * or by being removed from the tree entirely), and is considered
  1980. * 'orphaned'.
  1981. *
  1982. * Keep in mind that if a node that the client hasn't seen is moved
  1983. * into or out of the target node, it will not be included in the
  1984. * removedNodes and addedNodes list, so if the client is interested
  1985. * in the new set of children it needs to issue a `children` request.
  1986. */
  1987. getMutations: function (options = {}) {
  1988. let pending = this._pendingMutations || [];
  1989. this._pendingMutations = [];
  1990. if (options.cleanup) {
  1991. for (let node of this._orphaned) {
  1992. // Release the orphaned node. Nodes or children that have been
  1993. // retained will be moved to this._retainedOrphans.
  1994. this.releaseNode(node);
  1995. }
  1996. this._orphaned = new Set();
  1997. }
  1998. return pending;
  1999. },
  2000. queueMutation: function (mutation) {
  2001. if (!this.actorID || this._destroyed) {
  2002. // We've been destroyed, don't bother queueing this mutation.
  2003. return;
  2004. }
  2005. // We only send the `new-mutations` notification once, until the client
  2006. // fetches mutations with the `getMutations` packet.
  2007. let needEvent = this._pendingMutations.length === 0;
  2008. this._pendingMutations.push(mutation);
  2009. if (needEvent) {
  2010. events.emit(this, "new-mutations");
  2011. }
  2012. },
  2013. /**
  2014. * Handles mutations from the DOM mutation observer API.
  2015. *
  2016. * @param array[MutationRecord] mutations
  2017. * See https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver#MutationRecord
  2018. */
  2019. onMutations: function (mutations) {
  2020. // Notify any observers that want *all* mutations (even on nodes that aren't
  2021. // referenced). This is not sent over the protocol so can only be used by
  2022. // scripts running in the server process.
  2023. events.emit(this, "any-mutation");
  2024. for (let change of mutations) {
  2025. let targetActor = this.getNode(change.target);
  2026. if (!targetActor) {
  2027. continue;
  2028. }
  2029. let targetNode = change.target;
  2030. let type = change.type;
  2031. let mutation = {
  2032. type: type,
  2033. target: targetActor.actorID,
  2034. };
  2035. if (type === "attributes") {
  2036. mutation.attributeName = change.attributeName;
  2037. mutation.attributeNamespace = change.attributeNamespace || undefined;
  2038. mutation.newValue = targetNode.hasAttribute(mutation.attributeName) ?
  2039. targetNode.getAttribute(mutation.attributeName)
  2040. : null;
  2041. } else if (type === "characterData") {
  2042. mutation.newValue = targetNode.nodeValue;
  2043. this._maybeQueueInlineTextChildMutation(change, targetNode);
  2044. } else if (type === "childList" || type === "nativeAnonymousChildList") {
  2045. // Get the list of removed and added actors that the client has seen
  2046. // so that it can keep its ownership tree up to date.
  2047. let removedActors = [];
  2048. let addedActors = [];
  2049. for (let removed of change.removedNodes) {
  2050. let removedActor = this.getNode(removed);
  2051. if (!removedActor) {
  2052. // If the client never encountered this actor we don't need to
  2053. // mention that it was removed.
  2054. continue;
  2055. }
  2056. // While removed from the tree, nodes are saved as orphaned.
  2057. this._orphaned.add(removedActor);
  2058. removedActors.push(removedActor.actorID);
  2059. }
  2060. for (let added of change.addedNodes) {
  2061. let addedActor = this.getNode(added);
  2062. if (!addedActor) {
  2063. // If the client never encounted this actor we don't need to tell
  2064. // it about its addition for ownership tree purposes - if the
  2065. // client wants to see the new nodes it can ask for children.
  2066. continue;
  2067. }
  2068. // The actor is reconnected to the ownership tree, unorphan
  2069. // it and let the client know so that its ownership tree is up
  2070. // to date.
  2071. this._orphaned.delete(addedActor);
  2072. addedActors.push(addedActor.actorID);
  2073. }
  2074. mutation.numChildren = targetActor.numChildren;
  2075. mutation.removed = removedActors;
  2076. mutation.added = addedActors;
  2077. let inlineTextChild = this.inlineTextChild(targetActor);
  2078. if (inlineTextChild) {
  2079. mutation.inlineTextChild = inlineTextChild.form();
  2080. }
  2081. }
  2082. this.queueMutation(mutation);
  2083. }
  2084. },
  2085. /**
  2086. * Check if the provided mutation could change the way the target element is
  2087. * inlined with its parent node. If it might, a custom mutation of type
  2088. * "inlineTextChild" will be queued.
  2089. *
  2090. * @param {MutationRecord} mutation
  2091. * A characterData type mutation
  2092. */
  2093. _maybeQueueInlineTextChildMutation: function (mutation) {
  2094. let {oldValue, target} = mutation;
  2095. let newValue = target.nodeValue;
  2096. let limit = gValueSummaryLength;
  2097. if ((oldValue.length <= limit && newValue.length <= limit) ||
  2098. (oldValue.length > limit && newValue.length > limit)) {
  2099. // Bail out if the new & old values are both below/above the size limit.
  2100. return;
  2101. }
  2102. let parentActor = this.getNode(target.parentNode);
  2103. if (!parentActor || parentActor.rawNode.children.length > 0) {
  2104. // If the parent node has other children, a character data mutation will
  2105. // not change anything regarding inlining text nodes.
  2106. return;
  2107. }
  2108. let inlineTextChild = this.inlineTextChild(parentActor);
  2109. this.queueMutation({
  2110. type: "inlineTextChild",
  2111. target: parentActor.actorID,
  2112. inlineTextChild:
  2113. inlineTextChild ? inlineTextChild.form() : undefined
  2114. });
  2115. },
  2116. onFrameLoad: function ({ window, isTopLevel }) {
  2117. if (isTopLevel) {
  2118. // If we initialize the inspector while the document is loading,
  2119. // we may already have a root document set in the constructor.
  2120. if (this.rootDoc && !Cu.isDeadWrapper(this.rootDoc) &&
  2121. this.rootDoc.defaultView) {
  2122. this.onFrameUnload({ window: this.rootDoc.defaultView });
  2123. }
  2124. this.rootDoc = window.document;
  2125. this.rootNode = this.document();
  2126. this.queueMutation({
  2127. type: "newRoot",
  2128. target: this.rootNode.form()
  2129. });
  2130. return;
  2131. }
  2132. let frame = getFrameElement(window);
  2133. let frameActor = this.getNode(frame);
  2134. if (!frameActor) {
  2135. return;
  2136. }
  2137. this.queueMutation({
  2138. type: "frameLoad",
  2139. target: frameActor.actorID,
  2140. });
  2141. // Send a childList mutation on the frame.
  2142. this.queueMutation({
  2143. type: "childList",
  2144. target: frameActor.actorID,
  2145. added: [],
  2146. removed: []
  2147. });
  2148. },
  2149. // Returns true if domNode is in window or a subframe.
  2150. _childOfWindow: function (window, domNode) {
  2151. let win = nodeDocument(domNode).defaultView;
  2152. while (win) {
  2153. if (win === window) {
  2154. return true;
  2155. }
  2156. win = getFrameElement(win);
  2157. }
  2158. return false;
  2159. },
  2160. onFrameUnload: function ({ window }) {
  2161. // Any retained orphans that belong to this document
  2162. // or its children need to be released, and a mutation sent
  2163. // to notify of that.
  2164. let releasedOrphans = [];
  2165. for (let retained of this._retainedOrphans) {
  2166. if (Cu.isDeadWrapper(retained.rawNode) ||
  2167. this._childOfWindow(window, retained.rawNode)) {
  2168. this._retainedOrphans.delete(retained);
  2169. releasedOrphans.push(retained.actorID);
  2170. this.releaseNode(retained, { force: true });
  2171. }
  2172. }
  2173. if (releasedOrphans.length > 0) {
  2174. this.queueMutation({
  2175. target: this.rootNode.actorID,
  2176. type: "unretained",
  2177. nodes: releasedOrphans
  2178. });
  2179. }
  2180. let doc = window.document;
  2181. let documentActor = this.getNode(doc);
  2182. if (!documentActor) {
  2183. return;
  2184. }
  2185. if (this.rootDoc === doc) {
  2186. this.rootDoc = null;
  2187. this.rootNode = null;
  2188. }
  2189. this.queueMutation({
  2190. type: "documentUnload",
  2191. target: documentActor.actorID
  2192. });
  2193. let walker = this.getDocumentWalker(doc);
  2194. let parentNode = walker.parentNode();
  2195. if (parentNode) {
  2196. // Send a childList mutation on the frame so that clients know
  2197. // they should reread the children list.
  2198. this.queueMutation({
  2199. type: "childList",
  2200. target: this.getNode(parentNode).actorID,
  2201. added: [],
  2202. removed: []
  2203. });
  2204. }
  2205. // Need to force a release of this node, because those nodes can't
  2206. // be accessed anymore.
  2207. this.releaseNode(documentActor, { force: true });
  2208. },
  2209. /**
  2210. * Check if a node is attached to the DOM tree of the current page.
  2211. * @param {nsIDomNode} rawNode
  2212. * @return {Boolean} false if the node is removed from the tree or within a
  2213. * document fragment
  2214. */
  2215. _isInDOMTree: function (rawNode) {
  2216. let walker = this.getDocumentWalker(rawNode);
  2217. let current = walker.currentNode;
  2218. // Reaching the top of tree
  2219. while (walker.parentNode()) {
  2220. current = walker.currentNode;
  2221. }
  2222. // The top of the tree is a fragment or is not rootDoc, hence rawNode isn't
  2223. // attached
  2224. if (current.nodeType === Ci.nsIDOMNode.DOCUMENT_FRAGMENT_NODE ||
  2225. current !== this.rootDoc) {
  2226. return false;
  2227. }
  2228. // Otherwise the top of the tree is rootDoc, hence rawNode is in rootDoc
  2229. return true;
  2230. },
  2231. /**
  2232. * @see _isInDomTree
  2233. */
  2234. isInDOMTree: function (node) {
  2235. if (isNodeDead(node)) {
  2236. return false;
  2237. }
  2238. return this._isInDOMTree(node.rawNode);
  2239. },
  2240. /**
  2241. * Given an ObjectActor (identified by its ID), commonly used in the debugger,
  2242. * webconsole and variablesView, return the corresponding inspector's
  2243. * NodeActor
  2244. */
  2245. getNodeActorFromObjectActor: function (objectActorID) {
  2246. let actor = this.conn.getActor(objectActorID);
  2247. if (!actor) {
  2248. return null;
  2249. }
  2250. let debuggerObject = this.conn.getActor(objectActorID).obj;
  2251. let rawNode = debuggerObject.unsafeDereference();
  2252. if (!this._isInDOMTree(rawNode)) {
  2253. return null;
  2254. }
  2255. // This is a special case for the document object whereby it is considered
  2256. // as document.documentElement (the <html> node)
  2257. if (rawNode.defaultView && rawNode === rawNode.defaultView.document) {
  2258. rawNode = rawNode.documentElement;
  2259. }
  2260. return this.attachElement(rawNode);
  2261. },
  2262. /**
  2263. * Given a StyleSheetActor (identified by its ID), commonly used in the
  2264. * style-editor, get its ownerNode and return the corresponding walker's
  2265. * NodeActor.
  2266. * Note that getNodeFromActor was added later and can now be used instead.
  2267. */
  2268. getStyleSheetOwnerNode: function (styleSheetActorID) {
  2269. return this.getNodeFromActor(styleSheetActorID, ["ownerNode"]);
  2270. },
  2271. /**
  2272. * This method can be used to retrieve NodeActor for DOM nodes from other
  2273. * actors in a way that they can later be highlighted in the page, or
  2274. * selected in the inspector.
  2275. * If an actor has a reference to a DOM node, and the UI needs to know about
  2276. * this DOM node (and possibly select it in the inspector), the UI should
  2277. * first retrieve a reference to the walkerFront:
  2278. *
  2279. * // Make sure the inspector/walker have been initialized first.
  2280. * toolbox.initInspector().then(() => {
  2281. * // Retrieve the walker.
  2282. * let walker = toolbox.walker;
  2283. * });
  2284. *
  2285. * And then call this method:
  2286. *
  2287. * // Get the nodeFront from my actor, passing the ID and properties path.
  2288. * walker.getNodeFromActor(myActorID, ["element"]).then(nodeFront => {
  2289. * // Use the nodeFront, e.g. select the node in the inspector.
  2290. * toolbox.getPanel("inspector").selection.setNodeFront(nodeFront);
  2291. * });
  2292. *
  2293. * @param {String} actorID The ID for the actor that has a reference to the
  2294. * DOM node.
  2295. * @param {Array} path Where, on the actor, is the DOM node stored. If in the
  2296. * scope of the actor, the node is available as `this.data.node`, then this
  2297. * should be ["data", "node"].
  2298. * @return {NodeActor} The attached NodeActor, or null if it couldn't be
  2299. * found.
  2300. */
  2301. getNodeFromActor: function (actorID, path) {
  2302. let actor = this.conn.getActor(actorID);
  2303. if (!actor) {
  2304. return null;
  2305. }
  2306. let obj = actor;
  2307. for (let name of path) {
  2308. if (!(name in obj)) {
  2309. return null;
  2310. }
  2311. obj = obj[name];
  2312. }
  2313. return this.attachElement(obj);
  2314. },
  2315. /**
  2316. * Returns an instance of the LayoutActor that is used to retrieve CSS layout-related
  2317. * information.
  2318. *
  2319. * @return {LayoutActor}
  2320. */
  2321. getLayoutInspector: function () {
  2322. if (!this.layoutActor) {
  2323. this.layoutActor = new LayoutActor(this.conn, this.tabActor, this);
  2324. }
  2325. return this.layoutActor;
  2326. },
  2327. });
  2328. /**
  2329. * Server side of the inspector actor, which is used to create
  2330. * inspector-related actors, including the walker.
  2331. */
  2332. exports.InspectorActor = protocol.ActorClassWithSpec(inspectorSpec, {
  2333. initialize: function (conn, tabActor) {
  2334. protocol.Actor.prototype.initialize.call(this, conn);
  2335. this.tabActor = tabActor;
  2336. this._onColorPicked = this._onColorPicked.bind(this);
  2337. this._onColorPickCanceled = this._onColorPickCanceled.bind(this);
  2338. this.destroyEyeDropper = this.destroyEyeDropper.bind(this);
  2339. },
  2340. destroy: function () {
  2341. protocol.Actor.prototype.destroy.call(this);
  2342. this.destroyEyeDropper();
  2343. this._highlighterPromise = null;
  2344. this._pageStylePromise = null;
  2345. this._walkerPromise = null;
  2346. this.walker = null;
  2347. this.tabActor = null;
  2348. },
  2349. // Forces destruction of the actor and all its children
  2350. // like highlighter, walker and style actors.
  2351. disconnect: function () {
  2352. this.destroy();
  2353. },
  2354. get window() {
  2355. return this.tabActor.window;
  2356. },
  2357. getWalker: function (options = {}) {
  2358. if (this._walkerPromise) {
  2359. return this._walkerPromise;
  2360. }
  2361. let deferred = promise.defer();
  2362. this._walkerPromise = deferred.promise;
  2363. let window = this.window;
  2364. let domReady = () => {
  2365. let tabActor = this.tabActor;
  2366. window.removeEventListener("DOMContentLoaded", domReady, true);
  2367. this.walker = WalkerActor(this.conn, tabActor, options);
  2368. this.manage(this.walker);
  2369. events.once(this.walker, "destroyed", () => {
  2370. this._walkerPromise = null;
  2371. this._pageStylePromise = null;
  2372. });
  2373. deferred.resolve(this.walker);
  2374. };
  2375. if (window.document.readyState === "loading") {
  2376. window.addEventListener("DOMContentLoaded", domReady, true);
  2377. } else {
  2378. domReady();
  2379. }
  2380. return this._walkerPromise;
  2381. },
  2382. getPageStyle: function () {
  2383. if (this._pageStylePromise) {
  2384. return this._pageStylePromise;
  2385. }
  2386. this._pageStylePromise = this.getWalker().then(walker => {
  2387. let pageStyle = PageStyleActor(this);
  2388. this.manage(pageStyle);
  2389. return pageStyle;
  2390. });
  2391. return this._pageStylePromise;
  2392. },
  2393. /**
  2394. * The most used highlighter actor is the HighlighterActor which can be
  2395. * conveniently retrieved by this method.
  2396. * The same instance will always be returned by this method when called
  2397. * several times.
  2398. * The highlighter actor returned here is used to highlighter elements's
  2399. * box-models from the markup-view, box model, console, debugger, ... as
  2400. * well as select elements with the pointer (pick).
  2401. *
  2402. * @param {Boolean} autohide Optionally autohide the highlighter after an
  2403. * element has been picked
  2404. * @return {HighlighterActor}
  2405. */
  2406. getHighlighter: function (autohide) {
  2407. if (this._highlighterPromise) {
  2408. return this._highlighterPromise;
  2409. }
  2410. this._highlighterPromise = this.getWalker().then(walker => {
  2411. let highlighter = HighlighterActor(this, autohide);
  2412. this.manage(highlighter);
  2413. return highlighter;
  2414. });
  2415. return this._highlighterPromise;
  2416. },
  2417. /**
  2418. * If consumers need to display several highlighters at the same time or
  2419. * different types of highlighters, then this method should be used, passing
  2420. * the type name of the highlighter needed as argument.
  2421. * A new instance will be created everytime the method is called, so it's up
  2422. * to the consumer to release it when it is not needed anymore
  2423. *
  2424. * @param {String} type The type of highlighter to create
  2425. * @return {Highlighter} The highlighter actor instance or null if the
  2426. * typeName passed doesn't match any available highlighter
  2427. */
  2428. getHighlighterByType: function (typeName) {
  2429. if (isTypeRegistered(typeName)) {
  2430. return CustomHighlighterActor(this, typeName);
  2431. }
  2432. return null;
  2433. },
  2434. /**
  2435. * Get the node's image data if any (for canvas and img nodes).
  2436. * Returns an imageData object with the actual data being a LongStringActor
  2437. * and a size json object.
  2438. * The image data is transmitted as a base64 encoded png data-uri.
  2439. * The method rejects if the node isn't an image or if the image is missing
  2440. *
  2441. * Accepts a maxDim request parameter to resize images that are larger. This
  2442. * is important as the resizing occurs server-side so that image-data being
  2443. * transfered in the longstring back to the client will be that much smaller
  2444. */
  2445. getImageDataFromURL: function (url, maxDim) {
  2446. let img = new this.window.Image();
  2447. img.src = url;
  2448. // imageToImageData waits for the image to load.
  2449. return imageToImageData(img, maxDim).then(imageData => {
  2450. return {
  2451. data: LongStringActor(this.conn, imageData.data),
  2452. size: imageData.size
  2453. };
  2454. });
  2455. },
  2456. /**
  2457. * Resolve a URL to its absolute form, in the scope of a given content window.
  2458. * @param {String} url.
  2459. * @param {NodeActor} node If provided, the owner window of this node will be
  2460. * used to resolve the URL. Otherwise, the top-level content window will be
  2461. * used instead.
  2462. * @return {String} url.
  2463. */
  2464. resolveRelativeURL: function (url, node) {
  2465. let document = isNodeDead(node)
  2466. ? this.window.document
  2467. : nodeDocument(node.rawNode);
  2468. if (!document) {
  2469. return url;
  2470. }
  2471. let baseURI = Services.io.newURI(document.location.href, null, null);
  2472. return Services.io.newURI(url, null, baseURI).spec;
  2473. },
  2474. /**
  2475. * Create an instance of the eye-dropper highlighter and store it on this._eyeDropper.
  2476. * Note that for now, a new instance is created every time to deal with page navigation.
  2477. */
  2478. createEyeDropper: function () {
  2479. this.destroyEyeDropper();
  2480. this._highlighterEnv = new HighlighterEnvironment();
  2481. this._highlighterEnv.initFromTabActor(this.tabActor);
  2482. this._eyeDropper = new EyeDropper(this._highlighterEnv);
  2483. },
  2484. /**
  2485. * Destroy the current eye-dropper highlighter instance.
  2486. */
  2487. destroyEyeDropper: function () {
  2488. if (this._eyeDropper) {
  2489. this.cancelPickColorFromPage();
  2490. this._eyeDropper.destroy();
  2491. this._eyeDropper = null;
  2492. this._highlighterEnv.destroy();
  2493. this._highlighterEnv = null;
  2494. }
  2495. },
  2496. /**
  2497. * Pick a color from the page using the eye-dropper. This method doesn't return anything
  2498. * but will cause events to be sent to the front when a color is picked or when the user
  2499. * cancels the picker.
  2500. * @param {Object} options
  2501. */
  2502. pickColorFromPage: function (options) {
  2503. this.createEyeDropper();
  2504. this._eyeDropper.show(this.window.document.documentElement, options);
  2505. this._eyeDropper.once("selected", this._onColorPicked);
  2506. this._eyeDropper.once("canceled", this._onColorPickCanceled);
  2507. events.once(this.tabActor, "will-navigate", this.destroyEyeDropper);
  2508. },
  2509. /**
  2510. * After the pickColorFromPage method is called, the only way to dismiss the eye-dropper
  2511. * highlighter is for the user to click in the page and select a color. If you need to
  2512. * dismiss the eye-dropper programatically instead, use this method.
  2513. */
  2514. cancelPickColorFromPage: function () {
  2515. if (this._eyeDropper) {
  2516. this._eyeDropper.hide();
  2517. this._eyeDropper.off("selected", this._onColorPicked);
  2518. this._eyeDropper.off("canceled", this._onColorPickCanceled);
  2519. events.off(this.tabActor, "will-navigate", this.destroyEyeDropper);
  2520. }
  2521. },
  2522. _onColorPicked: function (e, color) {
  2523. events.emit(this, "color-picked", color);
  2524. },
  2525. _onColorPickCanceled: function () {
  2526. events.emit(this, "color-pick-canceled");
  2527. }
  2528. });
  2529. // Exported for test purposes.
  2530. exports._documentWalker = DocumentWalker;
  2531. function nodeDocument(node) {
  2532. if (Cu.isDeadWrapper(node)) {
  2533. return null;
  2534. }
  2535. return node.ownerDocument ||
  2536. (node.nodeType == Ci.nsIDOMNode.DOCUMENT_NODE ? node : null);
  2537. }
  2538. function nodeDocshell(node) {
  2539. let doc = node ? nodeDocument(node) : null;
  2540. let win = doc ? doc.defaultView : null;
  2541. if (win) {
  2542. return win.QueryInterface(Ci.nsIInterfaceRequestor)
  2543. .getInterface(Ci.nsIDocShell);
  2544. }
  2545. return null;
  2546. }
  2547. function isNodeDead(node) {
  2548. return !node || !node.rawNode || Cu.isDeadWrapper(node.rawNode);
  2549. }
  2550. /**
  2551. * Wrapper for inDeepTreeWalker. Adds filtering to the traversal methods.
  2552. * See inDeepTreeWalker for more information about the methods.
  2553. *
  2554. * @param {DOMNode} node
  2555. * @param {Window} rootWin
  2556. * @param {Int} whatToShow See nodeFilterConstants / inIDeepTreeWalker for
  2557. * options.
  2558. * @param {Function} filter A custom filter function Taking in a DOMNode
  2559. * and returning an Int. See WalkerActor.nodeFilter for an example.
  2560. */
  2561. function DocumentWalker(node, rootWin,
  2562. whatToShow = nodeFilterConstants.SHOW_ALL,
  2563. filter = standardTreeWalkerFilter) {
  2564. if (!rootWin.location) {
  2565. throw new Error("Got an invalid root window in DocumentWalker");
  2566. }
  2567. this.walker = Cc["@mozilla.org/inspector/deep-tree-walker;1"]
  2568. .createInstance(Ci.inIDeepTreeWalker);
  2569. this.walker.showAnonymousContent = true;
  2570. this.walker.showSubDocuments = true;
  2571. this.walker.showDocumentsAsNodes = true;
  2572. this.walker.init(rootWin.document, whatToShow);
  2573. this.filter = filter;
  2574. // Make sure that the walker knows about the initial node (which could
  2575. // be skipped due to a filter). Note that simply calling parentNode()
  2576. // causes currentNode to be updated.
  2577. this.walker.currentNode = node;
  2578. while (node &&
  2579. this.filter(node) === nodeFilterConstants.FILTER_SKIP) {
  2580. node = this.walker.parentNode();
  2581. }
  2582. }
  2583. DocumentWalker.prototype = {
  2584. get node() {
  2585. return this.walker.node;
  2586. },
  2587. get whatToShow() {
  2588. return this.walker.whatToShow;
  2589. },
  2590. get currentNode() {
  2591. return this.walker.currentNode;
  2592. },
  2593. set currentNode(val) {
  2594. this.walker.currentNode = val;
  2595. },
  2596. parentNode: function () {
  2597. return this.walker.parentNode();
  2598. },
  2599. nextNode: function () {
  2600. let node = this.walker.currentNode;
  2601. if (!node) {
  2602. return null;
  2603. }
  2604. let nextNode = this.walker.nextNode();
  2605. while (nextNode &&
  2606. this.filter(nextNode) === nodeFilterConstants.FILTER_SKIP) {
  2607. nextNode = this.walker.nextNode();
  2608. }
  2609. return nextNode;
  2610. },
  2611. firstChild: function () {
  2612. let node = this.walker.currentNode;
  2613. if (!node) {
  2614. return null;
  2615. }
  2616. let firstChild = this.walker.firstChild();
  2617. while (firstChild &&
  2618. this.filter(firstChild) === nodeFilterConstants.FILTER_SKIP) {
  2619. firstChild = this.walker.nextSibling();
  2620. }
  2621. return firstChild;
  2622. },
  2623. lastChild: function () {
  2624. let node = this.walker.currentNode;
  2625. if (!node) {
  2626. return null;
  2627. }
  2628. let lastChild = this.walker.lastChild();
  2629. while (lastChild &&
  2630. this.filter(lastChild) === nodeFilterConstants.FILTER_SKIP) {
  2631. lastChild = this.walker.previousSibling();
  2632. }
  2633. return lastChild;
  2634. },
  2635. previousSibling: function () {
  2636. let node = this.walker.previousSibling();
  2637. while (node && this.filter(node) === nodeFilterConstants.FILTER_SKIP) {
  2638. node = this.walker.previousSibling();
  2639. }
  2640. return node;
  2641. },
  2642. nextSibling: function () {
  2643. let node = this.walker.nextSibling();
  2644. while (node && this.filter(node) === nodeFilterConstants.FILTER_SKIP) {
  2645. node = this.walker.nextSibling();
  2646. }
  2647. return node;
  2648. }
  2649. };
  2650. function isInXULDocument(el) {
  2651. let doc = nodeDocument(el);
  2652. return doc &&
  2653. doc.documentElement &&
  2654. doc.documentElement.namespaceURI === XUL_NS;
  2655. }
  2656. /**
  2657. * This DeepTreeWalker filter skips whitespace text nodes and anonymous
  2658. * content with the exception of ::before and ::after and anonymous content
  2659. * in XUL document (needed to show all elements in the browser toolbox).
  2660. */
  2661. function standardTreeWalkerFilter(node) {
  2662. // ::before and ::after are native anonymous content, but we always
  2663. // want to show them
  2664. if (node.nodeName === "_moz_generated_content_before" ||
  2665. node.nodeName === "_moz_generated_content_after") {
  2666. return nodeFilterConstants.FILTER_ACCEPT;
  2667. }
  2668. // Ignore empty whitespace text nodes that do not impact the layout.
  2669. if (isWhitespaceTextNode(node)) {
  2670. return nodeHasSize(node)
  2671. ? nodeFilterConstants.FILTER_ACCEPT
  2672. : nodeFilterConstants.FILTER_SKIP;
  2673. }
  2674. // Ignore all native and XBL anonymous content inside a non-XUL document
  2675. if (!isInXULDocument(node) && (isXBLAnonymous(node) ||
  2676. isNativeAnonymous(node))) {
  2677. // Note: this will skip inspecting the contents of feedSubscribeLine since
  2678. // that's XUL content injected in an HTML document, but we need to because
  2679. // this also skips many other elements that need to be skipped - like form
  2680. // controls, scrollbars, video controls, etc (see bug 1187482).
  2681. return nodeFilterConstants.FILTER_SKIP;
  2682. }
  2683. return nodeFilterConstants.FILTER_ACCEPT;
  2684. }
  2685. /**
  2686. * This DeepTreeWalker filter is like standardTreeWalkerFilter except that
  2687. * it also includes all anonymous content (like internal form controls).
  2688. */
  2689. function allAnonymousContentTreeWalkerFilter(node) {
  2690. // Ignore empty whitespace text nodes that do not impact the layout.
  2691. if (isWhitespaceTextNode(node)) {
  2692. return nodeHasSize(node)
  2693. ? nodeFilterConstants.FILTER_ACCEPT
  2694. : nodeFilterConstants.FILTER_SKIP;
  2695. }
  2696. return nodeFilterConstants.FILTER_ACCEPT;
  2697. }
  2698. /**
  2699. * Is the given node a text node composed of whitespace only?
  2700. * @param {DOMNode} node
  2701. * @return {Boolean}
  2702. */
  2703. function isWhitespaceTextNode(node) {
  2704. return node.nodeType == Ci.nsIDOMNode.TEXT_NODE && !/[^\s]/.exec(node.nodeValue);
  2705. }
  2706. /**
  2707. * Does the given node have non-0 width and height?
  2708. * @param {DOMNode} node
  2709. * @return {Boolean}
  2710. */
  2711. function nodeHasSize(node) {
  2712. if (!node.getBoxQuads) {
  2713. return false;
  2714. }
  2715. let quads = node.getBoxQuads();
  2716. return quads.length && quads.some(quad => quad.bounds.width && quad.bounds.height);
  2717. }
  2718. /**
  2719. * Returns a promise that is settled once the given HTMLImageElement has
  2720. * finished loading.
  2721. *
  2722. * @param {HTMLImageElement} image - The image element.
  2723. * @param {Number} timeout - Maximum amount of time the image is allowed to load
  2724. * before the waiting is aborted. Ignored if flags.testing is set.
  2725. *
  2726. * @return {Promise} that is fulfilled once the image has loaded. If the image
  2727. * fails to load or the load takes too long, the promise is rejected.
  2728. */
  2729. function ensureImageLoaded(image, timeout) {
  2730. let { HTMLImageElement } = image.ownerDocument.defaultView;
  2731. if (!(image instanceof HTMLImageElement)) {
  2732. return promise.reject("image must be an HTMLImageELement");
  2733. }
  2734. if (image.complete) {
  2735. // The image has already finished loading.
  2736. return promise.resolve();
  2737. }
  2738. // This image is still loading.
  2739. let onLoad = AsyncUtils.listenOnce(image, "load");
  2740. // Reject if loading fails.
  2741. let onError = AsyncUtils.listenOnce(image, "error").then(() => {
  2742. return promise.reject("Image '" + image.src + "' failed to load.");
  2743. });
  2744. // Don't timeout when testing. This is never settled.
  2745. let onAbort = new Promise(() => {});
  2746. if (!flags.testing) {
  2747. // Tests are not running. Reject the promise after given timeout.
  2748. onAbort = DevToolsUtils.waitForTime(timeout).then(() => {
  2749. return promise.reject("Image '" + image.src + "' took too long to load.");
  2750. });
  2751. }
  2752. // See which happens first.
  2753. return promise.race([onLoad, onError, onAbort]);
  2754. }
  2755. /**
  2756. * Given an <img> or <canvas> element, return the image data-uri. If @param node
  2757. * is an <img> element, the method waits a while for the image to load before
  2758. * the data is generated. If the image does not finish loading in a reasonable
  2759. * time (IMAGE_FETCHING_TIMEOUT milliseconds) the process aborts.
  2760. *
  2761. * @param {HTMLImageElement|HTMLCanvasElement} node - The <img> or <canvas>
  2762. * element, or Image() object. Other types cause the method to reject.
  2763. * @param {Number} maxDim - Optionally pass a maximum size you want the longest
  2764. * side of the image to be resized to before getting the image data.
  2765. * @return {Promise} A promise that is fulfilled with an object containing the
  2766. * data-uri and size-related information:
  2767. * { data: "...",
  2768. * size: {
  2769. * naturalWidth: 400,
  2770. * naturalHeight: 300,
  2771. * resized: true }
  2772. * }.
  2773. *
  2774. * If something goes wrong, the promise is rejected.
  2775. */
  2776. var imageToImageData = Task.async(function* (node, maxDim) {
  2777. let { HTMLCanvasElement, HTMLImageElement } = node.ownerDocument.defaultView;
  2778. let isImg = node instanceof HTMLImageElement;
  2779. let isCanvas = node instanceof HTMLCanvasElement;
  2780. if (!isImg && !isCanvas) {
  2781. throw new Error("node is not a <canvas> or <img> element.");
  2782. }
  2783. if (isImg) {
  2784. // Ensure that the image is ready.
  2785. yield ensureImageLoaded(node, IMAGE_FETCHING_TIMEOUT);
  2786. }
  2787. // Get the image resize ratio if a maxDim was provided
  2788. let resizeRatio = 1;
  2789. let imgWidth = node.naturalWidth || node.width;
  2790. let imgHeight = node.naturalHeight || node.height;
  2791. let imgMax = Math.max(imgWidth, imgHeight);
  2792. if (maxDim && imgMax > maxDim) {
  2793. resizeRatio = maxDim / imgMax;
  2794. }
  2795. // Extract the image data
  2796. let imageData;
  2797. // The image may already be a data-uri, in which case, save ourselves the
  2798. // trouble of converting via the canvas.drawImage.toDataURL method, but only
  2799. // if the image doesn't need resizing
  2800. if (isImg && node.src.startsWith("data:") && resizeRatio === 1) {
  2801. imageData = node.src;
  2802. } else {
  2803. // Create a canvas to copy the rawNode into and get the imageData from
  2804. let canvas = node.ownerDocument.createElementNS(XHTML_NS, "canvas");
  2805. canvas.width = imgWidth * resizeRatio;
  2806. canvas.height = imgHeight * resizeRatio;
  2807. let ctx = canvas.getContext("2d");
  2808. // Copy the rawNode image or canvas in the new canvas and extract data
  2809. ctx.drawImage(node, 0, 0, canvas.width, canvas.height);
  2810. imageData = canvas.toDataURL("image/png");
  2811. }
  2812. return {
  2813. data: imageData,
  2814. size: {
  2815. naturalWidth: imgWidth,
  2816. naturalHeight: imgHeight,
  2817. resized: resizeRatio !== 1
  2818. }
  2819. };
  2820. });
  2821. loader.lazyGetter(this, "DOMUtils", function () {
  2822. return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
  2823. });