From 7536b603fac647a14ef9dd80ea03e973489a971d Mon Sep 17 00:00:00 2001 From: Rikki Gibson Date: Mon, 13 Aug 2018 11:27:11 -0700 Subject: [PATCH 1/4] Update package.json --- package-lock.json | 12 ++---------- package.json | 6 ++++-- 2 files changed, 6 insertions(+), 12 deletions(-) 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", From fd6d3fad397d8bd7c10f474d1b581c456bc2b14e Mon Sep 17 00:00:00 2001 From: Rikki Gibson Date: Mon, 13 Aug 2018 11:26:29 -0700 Subject: [PATCH 2/4] Pull in subset of isomorphic-xm2js --- lib/msRest.ts | 2 +- lib/policies/deserializationPolicy.ts | 23 ++--- lib/serviceClient.ts | 5 +- lib/util/utils.ts | 13 --- lib/util/xml.browser.ts | 124 ++++++++++++++++++++++++++ lib/util/xml.ts | 33 +++++++ 6 files changed, 168 insertions(+), 32 deletions(-) create mode 100644 lib/util/xml.browser.ts create mode 100644 lib/util/xml.ts 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; +} + + +export function stringifyXML(obj: any, opts?: { rootName?: string }) { + const rootName = (opts || {}).rootName || "root"; + const dom = buildNode(obj, rootName)[0]; + return serializer.serializeToString(dom); +} + +// tslint:disable-next-line:no-null-keyword +const doc = document.implementation.createDocument(null, null, null); +const serializer = new XMLSerializer(); + +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); + } + }); + }); +} From 1f84a2ee442024151a5390c34a4588f785997eaa Mon Sep 17 00:00:00 2001 From: Rikki Gibson Date: Mon, 13 Aug 2018 11:27:27 -0700 Subject: [PATCH 3/4] Update webpack configs --- webpack.config.ts | 1 + webpack.testconfig.ts | 4 +--- 2 files changed, 2 insertions(+), 3 deletions(-) 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: { From f8b528b5e10c4eb043dc8be608077d09fc58a422 Mon Sep 17 00:00:00 2001 From: Rikki Gibson Date: Thu, 16 Aug 2018 16:09:32 -0700 Subject: [PATCH 4/4] Add XML declaration when stringifying --- lib/util/xml.browser.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/lib/util/xml.browser.ts b/lib/util/xml.browser.ts index 17beacf8..710e4f4f 100644 --- a/lib/util/xml.browser.ts +++ b/lib/util/xml.browser.ts @@ -68,17 +68,16 @@ function domToObject(node: Node): any { 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); + return '' + serializer.serializeToString(dom); } -// tslint:disable-next-line:no-null-keyword -const doc = document.implementation.createDocument(null, null, null); -const serializer = new XMLSerializer(); - function buildAttributes(attrs: { [key: string]: { toString(): string; } }): Attr[] { const result = []; for (const key of Object.keys(attrs)) {