Wed, 31 Dec 2014 06:09:35 +0100
Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.
1 /* -*- js2-basic-offset: 2; indent-tabs-mode: nil; -*- */
2 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
5 * You can obtain one at http://mozilla.org/MPL/2.0/. */
7 "use strict";
9 const {Cc, Ci, Cu} = require("chrome");
11 loader.lazyGetter(this, "NetworkHelper", () => require("devtools/toolkit/webconsole/network-helper"));
12 loader.lazyImporter(this, "NetUtil", "resource://gre/modules/NetUtil.jsm");
13 loader.lazyServiceGetter(this, "mimeService", "@mozilla.org/mime;1", "nsIMIMEService");
15 let WebConsoleUtils = require("devtools/toolkit/webconsole/utils").Utils;
17 const STRINGS_URI = "chrome://browser/locale/devtools/webconsole.properties";
18 let l10n = new WebConsoleUtils.l10n(STRINGS_URI);
21 /**
22 * Creates a new NetworkPanel.
23 *
24 * @constructor
25 * @param nsIDOMNode aParent
26 * Parent node to append the created panel to.
27 * @param object aHttpActivity
28 * HttpActivity to display in the panel.
29 * @param object aWebConsoleFrame
30 * The parent WebConsoleFrame object that owns this network panel
31 * instance.
32 */
33 function NetworkPanel(aParent, aHttpActivity, aWebConsoleFrame)
34 {
35 let doc = aParent.ownerDocument;
36 this.httpActivity = aHttpActivity;
37 this.webconsole = aWebConsoleFrame;
38 this._longStringClick = this._longStringClick.bind(this);
39 this._responseBodyFetch = this._responseBodyFetch.bind(this);
40 this._requestBodyFetch = this._requestBodyFetch.bind(this);
42 // Create the underlaying panel
43 this.panel = createElement(doc, "panel", {
44 label: l10n.getStr("NetworkPanel.label"),
45 titlebar: "normal",
46 noautofocus: "true",
47 noautohide: "true",
48 close: "true"
49 });
51 // Create the iframe that displays the NetworkPanel XHTML.
52 this.iframe = createAndAppendElement(this.panel, "iframe", {
53 src: "chrome://browser/content/devtools/NetworkPanel.xhtml",
54 type: "content",
55 flex: "1"
56 });
58 let self = this;
60 // Destroy the panel when it's closed.
61 this.panel.addEventListener("popuphidden", function onPopupHide() {
62 self.panel.removeEventListener("popuphidden", onPopupHide, false);
63 self.panel.parentNode.removeChild(self.panel);
64 self.panel = null;
65 self.iframe = null;
66 self.httpActivity = null;
67 self.webconsole = null;
69 if (self.linkNode) {
70 self.linkNode._panelOpen = false;
71 self.linkNode = null;
72 }
73 }, false);
75 // Set the document object and update the content once the panel is loaded.
76 this.iframe.addEventListener("load", function onLoad() {
77 if (!self.iframe) {
78 return;
79 }
81 self.iframe.removeEventListener("load", onLoad, true);
82 self.update();
83 }, true);
85 this.panel.addEventListener("popupshown", function onPopupShown() {
86 self.panel.removeEventListener("popupshown", onPopupShown, true);
87 self.update();
88 }, true);
90 // Create the footer.
91 let footer = createElement(doc, "hbox", { align: "end" });
92 createAndAppendElement(footer, "spacer", { flex: 1 });
94 createAndAppendElement(footer, "resizer", { dir: "bottomend" });
95 this.panel.appendChild(footer);
97 aParent.appendChild(this.panel);
98 }
99 exports.NetworkPanel = NetworkPanel;
101 NetworkPanel.prototype =
102 {
103 /**
104 * The current state of the output.
105 */
106 _state: 0,
108 /**
109 * State variables.
110 */
111 _INIT: 0,
112 _DISPLAYED_REQUEST_HEADER: 1,
113 _DISPLAYED_REQUEST_BODY: 2,
114 _DISPLAYED_RESPONSE_HEADER: 3,
115 _TRANSITION_CLOSED: 4,
117 _fromDataRegExp: /Content-Type\:\s*application\/x-www-form-urlencoded/,
119 _contentType: null,
121 /**
122 * Function callback invoked whenever the panel content is updated. This is
123 * used only by tests.
124 *
125 * @private
126 * @type function
127 */
128 _onUpdate: null,
130 get document() {
131 return this.iframe && this.iframe.contentWindow ?
132 this.iframe.contentWindow.document : null;
133 },
135 /**
136 * Small helper function that is nearly equal to l10n.getFormatStr
137 * except that it prefixes aName with "NetworkPanel.".
138 *
139 * @param string aName
140 * The name of an i10n string to format. This string is prefixed with
141 * "NetworkPanel." before calling the HUDService.getFormatStr function.
142 * @param array aArray
143 * Values used as placeholder for the i10n string.
144 * @returns string
145 * The i10n formated string.
146 */
147 _format: function NP_format(aName, aArray)
148 {
149 return l10n.getFormatStr("NetworkPanel." + aName, aArray);
150 },
152 /**
153 * Returns the content type of the response body. This is based on the
154 * response.content.mimeType property. If this value is not available, then
155 * the content type is guessed by the file extension of the request URL.
156 *
157 * @return string
158 * Content type or empty string if no content type could be figured
159 * out.
160 */
161 get contentType()
162 {
163 if (this._contentType) {
164 return this._contentType;
165 }
167 let request = this.httpActivity.request;
168 let response = this.httpActivity.response;
170 let contentType = "";
171 let types = response.content ?
172 (response.content.mimeType || "").split(/,|;/) : [];
173 for (let i = 0; i < types.length; i++) {
174 if (types[i] in NetworkHelper.mimeCategoryMap) {
175 contentType = types[i];
176 break;
177 }
178 }
180 if (contentType) {
181 this._contentType = contentType;
182 return contentType;
183 }
185 // Try to get the content type from the request file extension.
186 let uri = NetUtil.newURI(request.url);
187 if ((uri instanceof Ci.nsIURL) && uri.fileExtension) {
188 try {
189 contentType = mimeService.getTypeFromExtension(uri.fileExtension);
190 }
191 catch(ex) {
192 // Added to prevent failures on OS X 64. No Flash?
193 Cu.reportError(ex);
194 }
195 }
197 this._contentType = contentType;
198 return contentType;
199 },
201 /**
202 *
203 * @returns boolean
204 * True if the response is an image, false otherwise.
205 */
206 get _responseIsImage()
207 {
208 return this.contentType &&
209 NetworkHelper.mimeCategoryMap[this.contentType] == "image";
210 },
212 /**
213 *
214 * @returns boolean
215 * True if the response body contains text, false otherwise.
216 */
217 get _isResponseBodyTextData()
218 {
219 return this.contentType ?
220 NetworkHelper.isTextMimeType(this.contentType) : false;
221 },
223 /**
224 * Tells if the server response is cached.
225 *
226 * @returns boolean
227 * Returns true if the server responded that the request is already
228 * in the browser's cache, false otherwise.
229 */
230 get _isResponseCached()
231 {
232 return this.httpActivity.response.status == 304;
233 },
235 /**
236 * Tells if the request body includes form data.
237 *
238 * @returns boolean
239 * Returns true if the posted body contains form data.
240 */
241 get _isRequestBodyFormData()
242 {
243 let requestBody = this.httpActivity.request.postData.text;
244 if (typeof requestBody == "object" && requestBody.type == "longString") {
245 requestBody = requestBody.initial;
246 }
247 return this._fromDataRegExp.test(requestBody);
248 },
250 /**
251 * Appends the node with id=aId by the text aValue.
252 *
253 * @private
254 * @param string aId
255 * @param string aValue
256 * @return nsIDOMElement
257 * The DOM element with id=aId.
258 */
259 _appendTextNode: function NP__appendTextNode(aId, aValue)
260 {
261 let textNode = this.document.createTextNode(aValue);
262 let elem = this.document.getElementById(aId);
263 elem.appendChild(textNode);
264 return elem;
265 },
267 /**
268 * Generates some HTML to display the key-value pair of the aList data. The
269 * generated HTML is added to node with id=aParentId.
270 *
271 * @param string aParentId
272 * Id of the parent node to append the list to.
273 * @oaram array aList
274 * Array that holds the objects you want to display. Each object must
275 * have two properties: name and value.
276 * @param boolean aIgnoreCookie
277 * If true, the key-value named "Cookie" is not added to the list.
278 * @returns void
279 */
280 _appendList: function NP_appendList(aParentId, aList, aIgnoreCookie)
281 {
282 let parent = this.document.getElementById(aParentId);
283 let doc = this.document;
285 aList.sort(function(a, b) {
286 return a.name.toLowerCase() < b.name.toLowerCase();
287 });
289 aList.forEach(function(aItem) {
290 let name = aItem.name;
291 if (aIgnoreCookie && (name == "Cookie" || name == "Set-Cookie")) {
292 return;
293 }
295 let value = aItem.value;
296 let longString = null;
297 if (typeof value == "object" && value.type == "longString") {
298 value = value.initial;
299 longString = true;
300 }
302 /**
303 * The following code creates the HTML:
304 * <tr>
305 * <th scope="row" class="property-name">${line}:</th>
306 * <td class="property-value">${aList[line]}</td>
307 * </tr>
308 * and adds it to parent.
309 */
310 let row = doc.createElement("tr");
311 let textNode = doc.createTextNode(name + ":");
312 let th = doc.createElement("th");
313 th.setAttribute("scope", "row");
314 th.setAttribute("class", "property-name");
315 th.appendChild(textNode);
316 row.appendChild(th);
318 textNode = doc.createTextNode(value);
319 let td = doc.createElement("td");
320 td.setAttribute("class", "property-value");
321 td.appendChild(textNode);
323 if (longString) {
324 let a = doc.createElement("a");
325 a.href = "#";
326 a.className = "longStringEllipsis";
327 a.addEventListener("mousedown", this._longStringClick.bind(this, aItem));
328 a.textContent = l10n.getStr("longStringEllipsis");
329 td.appendChild(a);
330 }
332 row.appendChild(td);
334 parent.appendChild(row);
335 }.bind(this));
336 },
338 /**
339 * The click event handler for the ellipsis which allows the user to retrieve
340 * the full header value.
341 *
342 * @private
343 * @param object aHeader
344 * The header object with the |name| and |value| properties.
345 * @param nsIDOMEvent aEvent
346 * The DOM click event object.
347 */
348 _longStringClick: function NP__longStringClick(aHeader, aEvent)
349 {
350 aEvent.preventDefault();
352 let longString = this.webconsole.webConsoleClient.longString(aHeader.value);
354 longString.substring(longString.initial.length, longString.length,
355 function NP__onLongStringSubstring(aResponse)
356 {
357 if (aResponse.error) {
358 Cu.reportError("NP__onLongStringSubstring error: " + aResponse.error);
359 return;
360 }
362 aHeader.value = aHeader.value.initial + aResponse.substring;
364 let textNode = aEvent.target.previousSibling;
365 textNode.textContent += aResponse.substring;
366 textNode.parentNode.removeChild(aEvent.target);
367 });
368 },
370 /**
371 * Displays the node with id=aId.
372 *
373 * @private
374 * @param string aId
375 * @return nsIDOMElement
376 * The element with id=aId.
377 */
378 _displayNode: function NP__displayNode(aId)
379 {
380 let elem = this.document.getElementById(aId);
381 elem.style.display = "block";
382 },
384 /**
385 * Sets the request URL, request method, the timing information when the
386 * request started and the request header content on the NetworkPanel.
387 * If the request header contains cookie data, a list of sent cookies is
388 * generated and a special sent cookie section is displayed + the cookie list
389 * added to it.
390 *
391 * @returns void
392 */
393 _displayRequestHeader: function NP__displayRequestHeader()
394 {
395 let request = this.httpActivity.request;
396 let requestTime = new Date(this.httpActivity.startedDateTime);
398 this._appendTextNode("headUrl", request.url);
399 this._appendTextNode("headMethod", request.method);
400 this._appendTextNode("requestHeadersInfo",
401 l10n.timestampString(requestTime));
403 this._appendList("requestHeadersContent", request.headers, true);
405 if (request.cookies.length > 0) {
406 this._displayNode("requestCookie");
407 this._appendList("requestCookieContent", request.cookies);
408 }
409 },
411 /**
412 * Displays the request body section of the NetworkPanel and set the request
413 * body content on the NetworkPanel.
414 *
415 * @returns void
416 */
417 _displayRequestBody: function NP__displayRequestBody()
418 {
419 let postData = this.httpActivity.request.postData;
420 this._displayNode("requestBody");
421 this._appendTextNode("requestBodyContent", postData.text);
422 },
424 /*
425 * Displays the `sent form data` section. Parses the request header for the
426 * submitted form data displays it inside of the `sent form data` section.
427 *
428 * @returns void
429 */
430 _displayRequestForm: function NP__processRequestForm()
431 {
432 let postData = this.httpActivity.request.postData.text;
433 let requestBodyLines = postData.split("\n");
434 let formData = requestBodyLines[requestBodyLines.length - 1].
435 replace(/\+/g, " ").split("&");
437 function unescapeText(aText)
438 {
439 try {
440 return decodeURIComponent(aText);
441 }
442 catch (ex) {
443 return decodeURIComponent(unescape(aText));
444 }
445 }
447 let formDataArray = [];
448 for (let i = 0; i < formData.length; i++) {
449 let data = formData[i];
450 let idx = data.indexOf("=");
451 let key = data.substring(0, idx);
452 let value = data.substring(idx + 1);
453 formDataArray.push({
454 name: unescapeText(key),
455 value: unescapeText(value)
456 });
457 }
459 this._appendList("requestFormDataContent", formDataArray);
460 this._displayNode("requestFormData");
461 },
463 /**
464 * Displays the response section of the NetworkPanel, sets the response status,
465 * the duration between the start of the request and the receiving of the
466 * response header as well as the response header content on the the NetworkPanel.
467 *
468 * @returns void
469 */
470 _displayResponseHeader: function NP__displayResponseHeader()
471 {
472 let timing = this.httpActivity.timings;
473 let response = this.httpActivity.response;
475 this._appendTextNode("headStatus",
476 [response.httpVersion, response.status,
477 response.statusText].join(" "));
479 // Calculate how much time it took from the request start, until the
480 // response started to be received.
481 let deltaDuration = 0;
482 ["dns", "connect", "send", "wait"].forEach(function (aValue) {
483 let ms = timing[aValue];
484 if (ms > -1) {
485 deltaDuration += ms;
486 }
487 });
489 this._appendTextNode("responseHeadersInfo",
490 this._format("durationMS", [deltaDuration]));
492 this._displayNode("responseContainer");
493 this._appendList("responseHeadersContent", response.headers, true);
495 if (response.cookies.length > 0) {
496 this._displayNode("responseCookie");
497 this._appendList("responseCookieContent", response.cookies);
498 }
499 },
501 /**
502 * Displays the respones image section, sets the source of the image displayed
503 * in the image response section to the request URL and the duration between
504 * the receiving of the response header and the end of the request. Once the
505 * image is loaded, the size of the requested image is set.
506 *
507 * @returns void
508 */
509 _displayResponseImage: function NP__displayResponseImage()
510 {
511 let self = this;
512 let timing = this.httpActivity.timings;
513 let request = this.httpActivity.request;
514 let response = this.httpActivity.response;
515 let cached = "";
517 if (this._isResponseCached) {
518 cached = "Cached";
519 }
521 let imageNode = this.document.getElementById("responseImage" +
522 cached + "Node");
524 let text = response.content.text;
525 if (typeof text == "object" && text.type == "longString") {
526 this._showResponseBodyFetchLink();
527 }
528 else {
529 imageNode.setAttribute("src",
530 "data:" + this.contentType + ";base64," + text);
531 }
533 // This function is called to set the imageInfo.
534 function setImageInfo() {
535 self._appendTextNode("responseImage" + cached + "Info",
536 self._format("imageSizeDeltaDurationMS",
537 [ imageNode.width, imageNode.height, timing.receive ]
538 )
539 );
540 }
542 // Check if the image is already loaded.
543 if (imageNode.width != 0) {
544 setImageInfo();
545 }
546 else {
547 // Image is not loaded yet therefore add a load event.
548 imageNode.addEventListener("load", function imageNodeLoad() {
549 imageNode.removeEventListener("load", imageNodeLoad, false);
550 setImageInfo();
551 }, false);
552 }
554 this._displayNode("responseImage" + cached);
555 },
557 /**
558 * Displays the response body section, sets the the duration between
559 * the receiving of the response header and the end of the request as well as
560 * the content of the response body on the NetworkPanel.
561 *
562 * @returns void
563 */
564 _displayResponseBody: function NP__displayResponseBody()
565 {
566 let timing = this.httpActivity.timings;
567 let response = this.httpActivity.response;
568 let cached = this._isResponseCached ? "Cached" : "";
570 this._appendTextNode("responseBody" + cached + "Info",
571 this._format("durationMS", [timing.receive]));
573 this._displayNode("responseBody" + cached);
575 let text = response.content.text;
576 if (typeof text == "object") {
577 text = text.initial;
578 this._showResponseBodyFetchLink();
579 }
581 this._appendTextNode("responseBody" + cached + "Content", text);
582 },
584 /**
585 * Show the "fetch response body" link.
586 * @private
587 */
588 _showResponseBodyFetchLink: function NP__showResponseBodyFetchLink()
589 {
590 let content = this.httpActivity.response.content;
592 let elem = this._appendTextNode("responseBodyFetchLink",
593 this._format("fetchRemainingResponseContentLink",
594 [content.text.length - content.text.initial.length]));
596 elem.style.display = "block";
597 elem.addEventListener("mousedown", this._responseBodyFetch);
598 },
600 /**
601 * Click event handler for the link that allows users to fetch the remaining
602 * response body.
603 *
604 * @private
605 * @param nsIDOMEvent aEvent
606 */
607 _responseBodyFetch: function NP__responseBodyFetch(aEvent)
608 {
609 aEvent.target.style.display = "none";
610 aEvent.target.removeEventListener("mousedown", this._responseBodyFetch);
612 let content = this.httpActivity.response.content;
613 let longString = this.webconsole.webConsoleClient.longString(content.text);
614 longString.substring(longString.initial.length, longString.length,
615 function NP__onLongStringSubstring(aResponse)
616 {
617 if (aResponse.error) {
618 Cu.reportError("NP__onLongStringSubstring error: " + aResponse.error);
619 return;
620 }
622 content.text = content.text.initial + aResponse.substring;
623 let cached = this._isResponseCached ? "Cached" : "";
625 if (this._responseIsImage) {
626 let imageNode = this.document.getElementById("responseImage" +
627 cached + "Node");
628 imageNode.src =
629 "data:" + this.contentType + ";base64," + content.text;
630 }
631 else {
632 this._appendTextNode("responseBody" + cached + "Content",
633 aResponse.substring);
634 }
635 }.bind(this));
636 },
638 /**
639 * Displays the `Unknown Content-Type hint` and sets the duration between the
640 * receiving of the response header on the NetworkPanel.
641 *
642 * @returns void
643 */
644 _displayResponseBodyUnknownType: function NP__displayResponseBodyUnknownType()
645 {
646 let timing = this.httpActivity.timings;
648 this._displayNode("responseBodyUnknownType");
649 this._appendTextNode("responseBodyUnknownTypeInfo",
650 this._format("durationMS", [timing.receive]));
652 this._appendTextNode("responseBodyUnknownTypeContent",
653 this._format("responseBodyUnableToDisplay.content", [this.contentType]));
654 },
656 /**
657 * Displays the `no response body` section and sets the the duration between
658 * the receiving of the response header and the end of the request.
659 *
660 * @returns void
661 */
662 _displayNoResponseBody: function NP_displayNoResponseBody()
663 {
664 let timing = this.httpActivity.timings;
666 this._displayNode("responseNoBody");
667 this._appendTextNode("responseNoBodyInfo",
668 this._format("durationMS", [timing.receive]));
669 },
671 /**
672 * Updates the content of the NetworkPanel's iframe.
673 *
674 * @returns void
675 */
676 update: function NP_update()
677 {
678 if (!this.document || this.document.readyState != "complete") {
679 return;
680 }
682 let updates = this.httpActivity.updates;
683 let timing = this.httpActivity.timings;
684 let request = this.httpActivity.request;
685 let response = this.httpActivity.response;
687 switch (this._state) {
688 case this._INIT:
689 this._displayRequestHeader();
690 this._state = this._DISPLAYED_REQUEST_HEADER;
691 // FALL THROUGH
693 case this._DISPLAYED_REQUEST_HEADER:
694 // Process the request body if there is one.
695 if (!this.httpActivity.discardRequestBody && request.postData.text) {
696 this._updateRequestBody();
697 this._state = this._DISPLAYED_REQUEST_BODY;
698 }
699 // FALL THROUGH
701 case this._DISPLAYED_REQUEST_BODY:
702 if (!response.headers.length || !Object.keys(timing).length) {
703 break;
704 }
705 this._displayResponseHeader();
706 this._state = this._DISPLAYED_RESPONSE_HEADER;
707 // FALL THROUGH
709 case this._DISPLAYED_RESPONSE_HEADER:
710 if (updates.indexOf("responseContent") == -1 ||
711 updates.indexOf("eventTimings") == -1) {
712 break;
713 }
715 this._state = this._TRANSITION_CLOSED;
716 if (this.httpActivity.discardResponseBody) {
717 break;
718 }
720 if (!response.content || !response.content.text) {
721 this._displayNoResponseBody();
722 }
723 else if (this._responseIsImage) {
724 this._displayResponseImage();
725 }
726 else if (!this._isResponseBodyTextData) {
727 this._displayResponseBodyUnknownType();
728 }
729 else if (response.content.text) {
730 this._displayResponseBody();
731 }
732 break;
733 }
735 if (this._onUpdate) {
736 this._onUpdate();
737 }
738 },
740 /**
741 * Update the panel to hold the current information we have about the request
742 * body.
743 * @private
744 */
745 _updateRequestBody: function NP__updateRequestBody()
746 {
747 let postData = this.httpActivity.request.postData;
748 if (typeof postData.text == "object" && postData.text.type == "longString") {
749 let elem = this._appendTextNode("requestBodyFetchLink",
750 this._format("fetchRemainingRequestContentLink",
751 [postData.text.length - postData.text.initial.length]));
753 elem.style.display = "block";
754 elem.addEventListener("mousedown", this._requestBodyFetch);
755 return;
756 }
758 // Check if we send some form data. If so, display the form data special.
759 if (this._isRequestBodyFormData) {
760 this._displayRequestForm();
761 }
762 else {
763 this._displayRequestBody();
764 }
765 },
767 /**
768 * Click event handler for the link that allows users to fetch the remaining
769 * request body.
770 *
771 * @private
772 * @param nsIDOMEvent aEvent
773 */
774 _requestBodyFetch: function NP__requestBodyFetch(aEvent)
775 {
776 aEvent.target.style.display = "none";
777 aEvent.target.removeEventListener("mousedown", this._responseBodyFetch);
779 let postData = this.httpActivity.request.postData;
780 let longString = this.webconsole.webConsoleClient.longString(postData.text);
781 longString.substring(longString.initial.length, longString.length,
782 function NP__onLongStringSubstring(aResponse)
783 {
784 if (aResponse.error) {
785 Cu.reportError("NP__onLongStringSubstring error: " + aResponse.error);
786 return;
787 }
789 postData.text = postData.text.initial + aResponse.substring;
790 this._updateRequestBody();
791 }.bind(this));
792 },
793 };
795 /**
796 * Creates a DOMNode and sets all the attributes of aAttributes on the created
797 * element.
798 *
799 * @param nsIDOMDocument aDocument
800 * Document to create the new DOMNode.
801 * @param string aTag
802 * Name of the tag for the DOMNode.
803 * @param object aAttributes
804 * Attributes set on the created DOMNode.
805 *
806 * @returns nsIDOMNode
807 */
808 function createElement(aDocument, aTag, aAttributes)
809 {
810 let node = aDocument.createElement(aTag);
811 if (aAttributes) {
812 for (let attr in aAttributes) {
813 node.setAttribute(attr, aAttributes[attr]);
814 }
815 }
816 return node;
817 }
819 /**
820 * Creates a new DOMNode and appends it to aParent.
821 *
822 * @param nsIDOMNode aParent
823 * A parent node to append the created element.
824 * @param string aTag
825 * Name of the tag for the DOMNode.
826 * @param object aAttributes
827 * Attributes set on the created DOMNode.
828 *
829 * @returns nsIDOMNode
830 */
831 function createAndAppendElement(aParent, aTag, aAttributes)
832 {
833 let node = createElement(aParent.ownerDocument, aTag, aAttributes);
834 aParent.appendChild(node);
835 return node;
836 }