diff --git a/lib/msRest.ts b/lib/msRest.ts index f897e45d..e1529aa6 100644 --- a/lib/msRest.ts +++ b/lib/msRest.ts @@ -33,7 +33,7 @@ export { stripRequest, stripResponse, delay, executePromisesSequentially, generateUuid, encodeUri, ServiceCallback, promiseToCallback, responseToBody, promiseToServiceCallback, isValidUuid, - applyMixins, isNode, stringifyXML, prepareXMLRootList, isDuration + applyMixins, isNode, isDuration } from "./util/utils"; export { URLBuilder, URLQuery } from "./url"; diff --git a/lib/policies/deserializationPolicy.ts b/lib/policies/deserializationPolicy.ts index c2522446..9c1e708d 100644 --- a/lib/policies/deserializationPolicy.ts +++ b/lib/policies/deserializationPolicy.ts @@ -1,13 +1,13 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. See License.txt in the project root for license information. -import * as xml2js from "isomorphic-xml2js"; import { HttpOperationResponse } from "../httpOperationResponse"; import { OperationResponse } from "../operationResponse"; import { OperationSpec } from "../operationSpec"; import { RestError } from "../restError"; import { Mapper, MapperType } from "../serializer"; import * as utils from "../util/utils"; +import { parseXML } from "../util/xml"; import { WebResource } from "../webResource"; import { BaseRequestPolicy, RequestPolicy, RequestPolicyFactory, RequestPolicyOptions } from "./requestPolicy"; @@ -153,21 +153,12 @@ function parse(operationResponse: HttpOperationResponse): Promise component.toLowerCase()); if (contentComponents.some(component => component === "application/xml" || component === "text/xml")) { - const xmlParser = new xml2js.Parser({ - explicitArray: false, - explicitCharkey: false, - explicitRoot: false - }); - return new Promise(function (resolve, reject) { - xmlParser.parseString(text, function (err: any, result: any) { - if (err) { - reject(err); - } else { - operationResponse.parsedBody = result; - resolve(operationResponse); - } - }); - }).catch(errorHandler); + return parseXML(text) + .then(body => { + operationResponse.parsedBody = body; + return operationResponse; + }) + .catch(errorHandler); } else if (contentComponents.some(component => component === "application/json" || component === "text/json") || !contentType) { return new Promise(resolve => { operationResponse.parsedBody = JSON.parse(text); diff --git a/lib/serviceClient.ts b/lib/serviceClient.ts index f1ca5275..ab3be52f 100644 --- a/lib/serviceClient.ts +++ b/lib/serviceClient.ts @@ -24,6 +24,7 @@ import { CompositeMapper, DictionaryMapper, Mapper, MapperType, Serializer } fro import { URLBuilder } from "./url"; import { Constants } from "./util/constants"; import * as utils from "./util/utils"; +import { stringifyXML } from "./util/xml"; import { RequestPrepareOptions, WebResource, RequestOptionsBase } from "./webResource"; /** @@ -326,10 +327,10 @@ export function serializeRequestBody(serviceClient: ServiceClient, httpRequest: const isStream = typeName === MapperType.Stream; if (operationSpec.isXML) { if (typeName === MapperType.Sequence) { - httpRequest.body = utils.stringifyXML(utils.prepareXMLRootList(httpRequest.body, xmlElementName || xmlName || serializedName!), { rootName: xmlName || serializedName }); + httpRequest.body = stringifyXML(utils.prepareXMLRootList(httpRequest.body, xmlElementName || xmlName || serializedName!), { rootName: xmlName || serializedName }); } else if (!isStream) { - httpRequest.body = utils.stringifyXML(httpRequest.body, { rootName: xmlName || serializedName }); + httpRequest.body = stringifyXML(httpRequest.body, { rootName: xmlName || serializedName }); } } else if (!isStream) { httpRequest.body = JSON.stringify(httpRequest.body); diff --git a/lib/util/utils.ts b/lib/util/utils.ts index 37e768f4..c5d8ba75 100644 --- a/lib/util/utils.ts +++ b/lib/util/utils.ts @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. See License.txt in the project root for license information. -import * as xml2js from "isomorphic-xml2js"; import * as uuidv4 from "uuid/v4"; import { HttpOperationResponse } from "../httpOperationResponse"; import { RestError } from "../restError"; @@ -240,18 +239,6 @@ export function promiseToServiceCallback(promise: Promise { + try { + const dom = parser.parseFromString(str, "application/xml"); + const errorMessage = getErrorMessage(dom); + if (errorMessage) { + throw new Error(errorMessage); + } + + const obj = domToObject(dom.childNodes[0]); + return Promise.resolve(obj); + } catch (err) { + return Promise.reject(err); + } +} + +const errorNS = parser.parseFromString("INVALID", "text/xml").getElementsByTagName("parsererror")[0].namespaceURI!; +function getErrorMessage(dom: Document): string | undefined { + const parserErrors = dom.getElementsByTagNameNS(errorNS, "parsererror"); + if (parserErrors.length) { + return parserErrors.item(0).innerHTML; + } else { + return undefined; + } +} + +function isElement(node: Node): node is Element { + return !!(node as Element).attributes; +} + +function domToObject(node: Node): any { + // empty node + if (node.childNodes.length === 0 && !(isElement(node) && node.hasAttributes())) { + return ""; + } + + if (node.childNodes.length === 1 && node.childNodes[0].nodeType === Node.TEXT_NODE) { + return node.childNodes[0].nodeValue; + } + + const result: { [key: string]: any } = {}; + for (let i = 0; i < node.childNodes.length; i++) { + const child = node.childNodes[i]; + // Ignore leading/trailing whitespace nodes + if (child.nodeType !== Node.TEXT_NODE) { + if (!result[child.nodeName]) { + result[child.nodeName] = domToObject(child); + } else if (Array.isArray(result[child.nodeName])) { + result[child.nodeName].push(domToObject(child)); + } else { + result[child.nodeName] = [result[child.nodeName], domToObject(child)]; + } + } + } + + if (isElement(node) && node.hasAttributes()) { + result["$"] = {}; + + for (let i = 0; i < node.attributes.length; i++) { + const attr = node.attributes[i]; + result["$"][attr.nodeName] = attr.nodeValue; + } + } + + return result; +} + +// tslint:disable-next-line:no-null-keyword +const doc = document.implementation.createDocument(null, null, null); +const serializer = new XMLSerializer(); + +export function stringifyXML(obj: any, opts?: { rootName?: string }) { + const rootName = (opts || {}).rootName || "root"; + const dom = buildNode(obj, rootName)[0]; + return '' + serializer.serializeToString(dom); +} + +function buildAttributes(attrs: { [key: string]: { toString(): string; } }): Attr[] { + const result = []; + for (const key of Object.keys(attrs)) { + const attr = doc.createAttribute(key); + attr.value = attrs[key].toString(); + result.push(attr); + } + return result; +} + +function buildNode(obj: any, elementName: string): Node[] { + if (typeof obj === "string" || typeof obj === "number" || typeof obj === "boolean") { + const elem = doc.createElement(elementName); + elem.textContent = obj.toString(); + return [elem]; + } + else if (Array.isArray(obj)) { + const result = []; + for (const arrayElem of obj) { + for (const child of buildNode(arrayElem, elementName)) { + result.push(child); + } + } + return result; + } else if (typeof obj === "object") { + const elem = doc.createElement(elementName); + for (const key of Object.keys(obj)) { + if (key === "$") { + for (const attr of buildAttributes(obj[key])) { + elem.attributes.setNamedItem(attr); + } + } else { + for (const child of buildNode(obj[key], key)) { + elem.appendChild(child); + } + } + } + return [elem]; + } + else { + throw new Error(`Illegal value passed to buildObject: ${obj}`); + } +} diff --git a/lib/util/xml.ts b/lib/util/xml.ts new file mode 100644 index 00000000..6e192728 --- /dev/null +++ b/lib/util/xml.ts @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +import * as xml2js from "xml2js"; + +export function stringifyXML(obj: any, opts?: { rootName?: string }) { + const builder = new xml2js.Builder({ + explicitArray: false, + explicitCharkey: false, + rootName: (opts || {}).rootName, + renderOpts: { + pretty: false + } + }); + return builder.buildObject(obj); +} + +export function parseXML(str: string): Promise { + const xmlParser = new xml2js.Parser({ + explicitArray: false, + explicitCharkey: false, + explicitRoot: false + }); + return new Promise((resolve, reject) => { + xmlParser.parseString(str, (err?: Error, res?: any) => { + if (err) { + reject(err); + } else { + resolve(res); + } + }); + }); +} diff --git a/package-lock.json b/package-lock.json index e4a82688..3d15af67 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "ms-rest-js", - "version": "0.16.0", + "version": "0.19.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -228,6 +228,7 @@ "version": "0.4.3", "resolved": "https://registry.npmjs.org/@types/xml2js/-/xml2js-0.4.3.tgz", "integrity": "sha512-Pv2HGRE4gWLs31In7nsyXEH4uVVsd0HNV9i2dyASvtDIlOtSTr1eczPLDpdEuyv5LWH5LT20GIXwPjkshKWI1g==", + "dev": true, "requires": { "@types/events": "*", "@types/node": "*" @@ -4982,15 +4983,6 @@ "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", "dev": true }, - "isomorphic-xml2js": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/isomorphic-xml2js/-/isomorphic-xml2js-0.1.3.tgz", - "integrity": "sha512-dIkT2U9ritKVWF/HfHfGwm5tTnlMnknYsv7l12oJlQQgOV2CNV65pX+FHy6HFL9YP8q0JcrlNQAFRJIN2agUmQ==", - "requires": { - "@types/xml2js": "^0.4.2", - "xml2js": "^0.4.19" - } - }, "istextorbinary": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/istextorbinary/-/istextorbinary-2.2.1.tgz", diff --git a/package.json b/package.json index ae9765e1..055da964 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "./dist/lib/msRest.js": "./es/lib/msRest.js", "./es/lib/policies/msRestUserAgentPolicy.js": "./es/lib/policies/msRestUserAgentPolicy.stub.js", "./es/lib/util/base64.js": "./es/lib/util/base64.browser.js", + "./es/lib/util/xml.js": "./es/lib/util/xml.browser.js", "./es/lib/defaultHttpClient.js": "./es/lib/defaultHttpClient.browser.js" }, "license": "MIT", @@ -42,9 +43,9 @@ "axios": "^0.18.0", "form-data": "^2.3.2", "tough-cookie": "^2.4.3", - "isomorphic-xml2js": "^0.1.3", "tslib": "^1.9.2", - "uuid": "^3.2.1" + "uuid": "^3.2.1", + "xml2js": "^0.4.19" }, "devDependencies": { "@types/glob": "^5.0.35", @@ -53,6 +54,7 @@ "@types/tough-cookie": "^2.3.3", "@types/webpack": "^4.1.3", "@types/webpack-dev-middleware": "^2.0.1", + "@types/xml2js": "^0.4.3", "abortcontroller-polyfill": "^1.1.9", "express": "^4.16.3", "glob": "^7.1.2", diff --git a/webpack.config.ts b/webpack.config.ts index 76aa5bdb..f980b1a4 100644 --- a/webpack.config.ts +++ b/webpack.config.ts @@ -12,6 +12,7 @@ const config: webpack.Configuration = { }, plugins: [ new webpack.NormalModuleReplacementPlugin(/(\.).+util\/base64/, path.resolve(__dirname, "./lib/util/base64.browser.ts")), + new webpack.NormalModuleReplacementPlugin(/(\.).+util\/xml/, path.resolve(__dirname, "./lib/util/xml.browser.ts")), new webpack.NormalModuleReplacementPlugin(/(\.).+defaultHttpClient/, path.resolve(__dirname, "./lib/defaultHttpClient.browser.ts")) ], module: { diff --git a/webpack.testconfig.ts b/webpack.testconfig.ts index 8e817093..d2bb27fc 100644 --- a/webpack.testconfig.ts +++ b/webpack.testconfig.ts @@ -6,15 +6,13 @@ const config: webpack.Configuration = { entry: [...glob.sync(path.join(__dirname, 'test/shared/**/*.ts')), ...glob.sync(path.join(__dirname, 'test/browser/**/*.ts'))], mode: 'development', devtool: 'source-map', - devServer: { - contentBase: __dirname - }, output: { filename: 'testBundle.js', path: __dirname }, plugins: [ new webpack.NormalModuleReplacementPlugin(/(\.).+util\/base64/, path.resolve(__dirname, "./lib/util/base64.browser.ts")), + new webpack.NormalModuleReplacementPlugin(/(\.).+util\/xml/, path.resolve(__dirname, "./lib/util/xml.browser.ts")), new webpack.NormalModuleReplacementPlugin(/(\.).+defaultHttpClient/, path.resolve(__dirname, "./lib/defaultHttpClient.browser.ts")) ], module: {