UNPKG

5.43 kBPlain TextView Raw
1// Copyright (c) Microsoft Corporation. All rights reserved.
2// Licensed under the MIT License. See License.txt in the project root for license information.
3
4const parser = new DOMParser();
5
6// Policy to make our code Trusted Types compliant.
7// https://github.com/w3c/webappsec-trusted-types
8// We are calling DOMParser.parseFromString() to parse XML payload from Azure services.
9// The parsed DOM object is not exposed to outside. Scripts are disabled when parsing
10// according to the spec. There are no HTML/XSS security concerns on the usage of
11// parseFromString() here.
12let ttPolicy: Pick<TrustedTypePolicy, "createHTML"> | undefined;
13try {
14 if (typeof self.trustedTypes !== "undefined") {
15 ttPolicy = self.trustedTypes.createPolicy("@azure/ms-rest-js#xml.browser", {
16 createHTML: (s) => s,
17 });
18 }
19} catch (e) {
20 console.warn('Could not create trusted types policy "@azure/ms-rest-js#xml.browser"');
21}
22
23export function parseXML(str: string): Promise<any> {
24 try {
25 const dom = parser.parseFromString((ttPolicy?.createHTML(str) ?? str) as string, "application/xml");
26 throwIfError(dom);
27
28 const obj = domToObject(dom.childNodes[0]);
29 return Promise.resolve(obj);
30 } catch (err) {
31 return Promise.reject(err);
32 }
33}
34
35let errorNS = "";
36try {
37 const invalidXML = (ttPolicy?.createHTML("INVALID") ?? "INVALID") as string;
38 errorNS =
39 parser.parseFromString(invalidXML, "text/xml").getElementsByTagName("parsererror")[0]
40 .namespaceURI! ?? "";
41} catch (ignored) {
42 // Most browsers will return a document containing <parsererror>, but IE will throw.
43}
44
45function throwIfError(dom: Document) {
46 if (errorNS) {
47 const parserErrors = dom.getElementsByTagNameNS(errorNS, "parsererror");
48 if (parserErrors.length) {
49 throw new Error(parserErrors.item(0)!.innerHTML);
50 }
51 }
52}
53
54function isElement(node: Node): node is Element {
55 return !!(node as Element).attributes;
56}
57
58/**
59 * Get the Element-typed version of the provided Node if the provided node is an element with
60 * attributes. If it isn't, then undefined is returned.
61 */
62function asElementWithAttributes(node: Node): Element | undefined {
63 return isElement(node) && node.hasAttributes() ? node : undefined;
64}
65
66function domToObject(node: Node): any {
67 let result: any = {};
68
69 const childNodeCount: number = node.childNodes.length;
70
71 const firstChildNode: Node = node.childNodes[0];
72 const onlyChildTextValue: string | undefined =
73 (firstChildNode &&
74 childNodeCount === 1 &&
75 firstChildNode.nodeType === Node.TEXT_NODE &&
76 firstChildNode.nodeValue) ||
77 undefined;
78
79 const elementWithAttributes: Element | undefined = asElementWithAttributes(node);
80 if (elementWithAttributes) {
81 result["$"] = {};
82
83 for (let i = 0; i < elementWithAttributes.attributes.length; i++) {
84 const attr = elementWithAttributes.attributes[i];
85 result["$"][attr.nodeName] = attr.nodeValue;
86 }
87
88 if (onlyChildTextValue) {
89 result["_"] = onlyChildTextValue;
90 }
91 } else if (childNodeCount === 0) {
92 result = "";
93 } else if (onlyChildTextValue) {
94 result = onlyChildTextValue;
95 }
96
97 if (!onlyChildTextValue) {
98 for (let i = 0; i < childNodeCount; i++) {
99 const child = node.childNodes[i];
100 // Ignore leading/trailing whitespace nodes
101 if (child.nodeType !== Node.TEXT_NODE) {
102 const childObject: any = domToObject(child);
103 if (!result[child.nodeName]) {
104 result[child.nodeName] = childObject;
105 } else if (Array.isArray(result[child.nodeName])) {
106 result[child.nodeName].push(childObject);
107 } else {
108 result[child.nodeName] = [result[child.nodeName], childObject];
109 }
110 }
111 }
112 }
113
114 return result;
115}
116
117// tslint:disable-next-line:no-null-keyword
118const doc = document.implementation.createDocument(null, null, null);
119const serializer = new XMLSerializer();
120
121export function stringifyXML(obj: any, opts?: { rootName?: string }) {
122 const rootName = (opts && opts.rootName) || "root";
123 const dom = buildNode(obj, rootName)[0];
124 return (
125 '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' + serializer.serializeToString(dom)
126 );
127}
128
129function buildAttributes(attrs: { [key: string]: { toString(): string } }): Attr[] {
130 const result = [];
131 for (const key of Object.keys(attrs)) {
132 const attr = doc.createAttribute(key);
133 attr.value = attrs[key].toString();
134 result.push(attr);
135 }
136 return result;
137}
138
139function buildNode(obj: any, elementName: string): Node[] {
140 if (typeof obj === "string" || typeof obj === "number" || typeof obj === "boolean") {
141 const elem = doc.createElement(elementName);
142 elem.textContent = obj.toString();
143 return [elem];
144 } else if (Array.isArray(obj)) {
145 const result = [];
146 for (const arrayElem of obj) {
147 for (const child of buildNode(arrayElem, elementName)) {
148 result.push(child);
149 }
150 }
151 return result;
152 } else if (typeof obj === "object") {
153 const elem = doc.createElement(elementName);
154 for (const key of Object.keys(obj)) {
155 if (key === "$") {
156 for (const attr of buildAttributes(obj[key])) {
157 elem.attributes.setNamedItem(attr);
158 }
159 } else {
160 for (const child of buildNode(obj[key], key)) {
161 elem.appendChild(child);
162 }
163 }
164 }
165 return [elem];
166 } else {
167 throw new Error(`Illegal value passed to buildObject: ${obj}`);
168 }
169}