diff --git a/packages/insomnia/src/common/import.ts b/packages/insomnia/src/common/import.ts index 78f449a9413..c8b2b7f88e1 100644 --- a/packages/insomnia/src/common/import.ts +++ b/packages/insomnia/src/common/import.ts @@ -19,6 +19,7 @@ import { isWorkspace, type Workspace } from '../models/workspace'; import type { CurrentPlan } from '../ui/routes/organization'; import { convert, type InsomniaImporter } from '../utils/importers/convert'; import { id as postmanEnvImporterId } from '../utils/importers/importers/postman-env'; +import { flattenWsdl } from '../utils/importers/importers/wsdl'; import { invariant } from '../utils/invariant'; import { database as db } from './database'; import { generateId } from './misc'; @@ -71,6 +72,7 @@ export async function fetchImportContentFromURI({ uri }: { uri: string }) { export interface ImportFileDetail { contentStr: string; oriFileName: string; + oriFilePath?: string; } export interface PostmanDataDumpRawData { @@ -119,12 +121,24 @@ let resourceCacheList: ResourceCacheType[] = []; export async function scanResources(contentList: string[] | ImportFileDetail[]): Promise { resourceCacheList = []; const results = await Promise.allSettled(contentList.map(async content => { - const contentStr = typeof content === 'string' ? content : content.contentStr; + let contentStr = typeof content === 'string' ? content : content.contentStr; const oriFileName = typeof content === 'string' ? '' : content.oriFileName; let result: ConvertResult | null = null; try { + if (oriFileName.toLowerCase().endsWith('.wsdl')) { + let oriFilePath = ''; + if (typeof content === 'object' && content.oriFilePath) { + oriFilePath = content.oriFilePath; + } + if (oriFilePath) { + // Try to find referenced files in the WSDL file and merge them into the main file + try { + contentStr = await flattenWsdl(contentStr, oriFilePath); + } catch (err) { } + } + } result = (await convert(contentStr)) as unknown as ConvertResult; } catch (err: unknown) { if (err instanceof Error) { diff --git a/packages/insomnia/src/ui/routes/import.tsx b/packages/insomnia/src/ui/routes/import.tsx index c747b407751..ae7ca9f6f75 100644 --- a/packages/insomnia/src/ui/routes/import.tsx +++ b/packages/insomnia/src/ui/routes/import.tsx @@ -80,6 +80,7 @@ export const scanForResourcesAction: ActionFunction = async ({ request }): Promi contentList.push({ contentStr: await fetchImportContentFromURI({ uri }), oriFileName: path.basename(filePath), + oriFilePath: filePath, }); } } else { diff --git a/packages/insomnia/src/utils/importers/importers/wsdl.ts b/packages/insomnia/src/utils/importers/importers/wsdl.ts index aed66126b6e..f62f3aa7115 100644 --- a/packages/insomnia/src/utils/importers/importers/wsdl.ts +++ b/packages/insomnia/src/utils/importers/importers/wsdl.ts @@ -1,3 +1,7 @@ +import { readFile } from 'node:fs/promises'; +import path from 'node:path'; + +import { DOMParser, XMLSerializer } from '@xmldom/xmldom'; import { findWSDLForServiceName, getJsonForWSDL, @@ -95,14 +99,15 @@ const convertWsdlToPostman = async (input: string) => { export const convert: Converter = async rawData => { try { - if (rawData.indexOf('wsdl:definition') !== -1) { - const postmanData = await convertWsdlToPostman( - `${rawData}`, - ); - postmanData.info.schema += 'collection.json'; - const postmanJson = JSON.stringify(postmanData); - return postman.convert(postmanJson); + if (!verifyWsdl(rawData)) { + return null; } + const postmanData = await convertWsdlToPostman( + `${rawData}`, + ); + postmanData.info.schema += 'collection.json'; + const postmanJson = JSON.stringify(postmanData); + return postman.convert(postmanJson); } catch (error) { console.error(error); // Nothing @@ -110,3 +115,97 @@ export const convert: Converter = async rawData => { return null; }; + +const xmlSchemaNamespaceUri = 'http://www.w3.org/2001/XMLSchema'; +const wsdlNamespaceUri = 'http://schemas.xmlsoap.org/wsdl/'; + +function verifyWsdl(fileContent: string) { + try { + const mainWsdlDocument = new DOMParser().parseFromString(fileContent, 'text/xml'); + return mainWsdlDocument.documentElement.namespaceURI === wsdlNamespaceUri && + mainWsdlDocument.documentElement.localName === 'definitions'; + } catch (error) { + return false; + } +} + +function isXmlSchemaElement(element: Element) { + return element.namespaceURI === xmlSchemaNamespaceUri && element.localName === 'schema'; +} + +async function recurseXmlSchema(xsdFilePath: string, onTrackSet: Set, needToVerifyXmlSchema = true) { + if (onTrackSet.has(xsdFilePath)) { + return null; + } + const fileContent = await readFile(xsdFilePath, 'utf-8'); + const xsdDocument = new DOMParser().parseFromString(fileContent, 'text/xml'); + if (needToVerifyXmlSchema) { + if ( + !isXmlSchemaElement(xsdDocument.documentElement) + ) { + return null; + } + } + + onTrackSet.add(xsdFilePath); + + try { + // find all import and include tags + const referenceElements = [ + ...Array.from(xsdDocument.getElementsByTagNameNS(xmlSchemaNamespaceUri, 'import')), + ...Array.from(xsdDocument.getElementsByTagNameNS(xmlSchemaNamespaceUri, 'include')), + ]; + if (referenceElements.length === 0) { + onTrackSet.delete(xsdFilePath); + return xsdDocument.documentElement; + } else { + for (const referenceElement of referenceElements) { + const schemaLocation = referenceElement.getAttribute('schemaLocation'); + if (!schemaLocation) { + continue; + } + // only handle relative paths that exist + const absolutePath = path.resolve(path.dirname(xsdFilePath), schemaLocation); + try { + // assure that the file exists + await readFile(absolutePath, 'utf-8'); + const childElement = await recurseXmlSchema(absolutePath, onTrackSet); + if (!childElement) { + continue; + } + const parentElementOfReferenceElement = referenceElement.parentNode; + parentElementOfReferenceElement?.replaceChild(childElement, referenceElement); + // remove nested schema element + if (parentElementOfReferenceElement && isXmlSchemaElement(parentElementOfReferenceElement as Element)) { + parentElementOfReferenceElement.parentNode?.replaceChild(childElement, parentElementOfReferenceElement); + } + } catch (error) { + continue; + } + } + onTrackSet.delete(xsdFilePath); + return xsdDocument.documentElement; + } + } catch (error) { + onTrackSet.delete(xsdFilePath); + return null; + } +} + +// Merge all referenced xml schema files into the main wsdl file +export async function flattenWsdl(mainFileContent: string, mainFilePath: string) { + if (!verifyWsdl(mainFileContent)) { + throw new Error('Invalid WSDL file'); + } + + // keep track of all on track filepaths to avoid circular references + const onTrackSet = new Set(); + + const documentElement = await recurseXmlSchema(mainFilePath, onTrackSet, false); + + if (documentElement && documentElement.ownerDocument) { + return new XMLSerializer().serializeToString(documentElement.ownerDocument); + } else { + throw new Error('Cannot flatten WSDL'); + } +}