diff --git a/cli/src/cordova.ts b/cli/src/cordova.ts index 289208462d..9cccb27d1c 100644 --- a/cli/src/cordova.ts +++ b/cli/src/cordova.ts @@ -369,6 +369,7 @@ async function logiOSPlist(configElement: any, config: Config, plugin: Plugin) { if (await pathExists(plistPath)) { const xmlMeta = await readXML(plistPath); const data = await readFile(plistPath, { encoding: 'utf-8' }); + const trimmedPlistData = data.replace(/(\t|\r|\n)/g, ''); const plistData = plist.parse(data) as PlistObject; const dict = xmlMeta.plist.dict.pop(); if (!dict.key.includes(configElement.$.parent)) { @@ -404,7 +405,140 @@ async function logiOSPlist(configElement: any, config: Config, plugin: Plugin) { ); } } else { - logPossibleMissingItem(configElement, plugin); + let xml = buildConfigFileXml(configElement); + xml = `${configElement.$.parent}${getConfigFileTagContent( + xml, + )}`; + xml = `${xml}`; + + const parseXmlToSearchable = ( + childElementsObj: any[], + arrayToAddTo: any[], + ) => { + for (const childElement of childElementsObj) { + const childElementName = childElement['#name']; + const toAdd: { + name: string; + attrs?: { [key: string]: any } | undefined; + children?: any[] | undefined; + value?: any | undefined; + } = { name: childElementName }; + if (childElementName === 'key' || childElementName === 'string') { + toAdd.value = childElement['_']; + } else { + if (childElement['$']) { + toAdd.attrs = { ...childElement['$'] }; + } + if (childElement['$$']) { + toAdd.children = []; + parseXmlToSearchable(childElement['$$'], toAdd['children']); + } + } + arrayToAddTo.push(toAdd); + } + }; + + const existingElements = parseXML(trimmedPlistData, { + explicitChildren: true, + trim: true, + preserveChildrenOrder: true, + }); + const parsedExistingElements: any[] = []; + const rootKeyOfExistingElements = Object.keys(existingElements)[0]; + const rootOfExistingElementsToAdd: { + name: string; + attrs?: { [key: string]: any } | undefined; + children: any[]; + } = { name: rootKeyOfExistingElements, children: [] }; + if (existingElements[rootKeyOfExistingElements]['$']) { + rootOfExistingElementsToAdd.attrs = { + ...existingElements[rootKeyOfExistingElements]['$'], + }; + } + parseXmlToSearchable( + existingElements[rootKeyOfExistingElements]['$$'], + rootOfExistingElementsToAdd['children'], + ); + parsedExistingElements.push(rootOfExistingElementsToAdd); + + const requiredElements = parseXML(xml, { + explicitChildren: true, + trim: true, + preserveChildrenOrder: true, + }); + const parsedRequiredElements: any[] = []; + const rootKeyOfRequiredElements = Object.keys(requiredElements)[0]; + const rootOfRequiredElementsToAdd: { + name: string; + attrs?: { [key: string]: any } | undefined; + children: any[]; + } = { name: rootKeyOfRequiredElements, children: [] }; + if (requiredElements[rootKeyOfRequiredElements]['$']) { + rootOfRequiredElementsToAdd.attrs = { + ...requiredElements[rootKeyOfRequiredElements]['$'], + }; + } + parseXmlToSearchable( + requiredElements[rootKeyOfRequiredElements]['$$'], + rootOfRequiredElementsToAdd['children'], + ); + parsedRequiredElements.push(rootOfRequiredElementsToAdd); + + const doesContainElements = ( + requiredElementsArray: any[], + existingElementsArray: any[], + ) => { + for (const requiredElement of requiredElementsArray) { + if ( + requiredElement.name === 'key' || + requiredElement.name === 'string' + ) { + let foundMatch = false; + for (const existingElement of existingElementsArray) { + if ( + existingElement.name === requiredElement.name && + existingElement.value === requiredElement.value + ) { + foundMatch = true; + break; + } + } + if (!foundMatch) { + return false; + } + } else { + let foundMatch = false; + for (const existingElement of existingElementsArray) { + if (existingElement.name === requiredElement.name) { + if ( + (requiredElement.children !== undefined) === + (existingElement.children !== undefined) + ) { + if ( + doesContainElements( + requiredElement.children, + existingElement.children, + ) + ) { + foundMatch = true; + break; + } + } + } + } + if (!foundMatch) { + return false; + } + } + } + return true; + }; + + if ( + !doesContainElements(parsedRequiredElements, parsedExistingElements) + ) { + logPossibleMissingItem(configElement, plugin); + } } } } else { @@ -648,7 +782,7 @@ export async function writeCordovaAndroidManifest( ) { const keys = Object.keys(configElement).filter(k => k !== '$'); keys.map(k => { - configElement[k].map((e: any) => { + configElement[k].map(async (e: any) => { const xmlElement = buildXmlElement(e, k); const pathParts = getPathParts( configElement.$.parent || configElement.$.target, @@ -670,11 +804,226 @@ export async function writeCordovaAndroidManifest( applicationXMLEntries.push(xmlElement); } } else { - logger.warn( - `Configuration required for ${c.strong(p.id)}.\n` + - `Add the following to AndroidManifest.xml:\n` + - xmlElement, + const manifestPathOfCapApp = join( + config.android.appDirAbs, + 'src', + 'main', + 'AndroidManifest.xml', + ); + const manifestContentTrimmed = ( + await readFile(manifestPathOfCapApp) + ) + .toString() + .trim() + .replace(/\n|\t|\r/g, '') + .replace(/[\s]{1,}[\s]{1,}/g, '>') + .replace(/[\s]{2,}/g, ' '); + const requiredManifestContentTrimmed = xmlElement + .trim() + .replace(/\n|\t|\r/g, '') + .replace(/[\s]{1,}[\s]{1,}/g, '>') + .replace(/[\s]{2,}/g, ' '); + const pathPartList = getPathParts( + configElement.$.parent || configElement.$.target, ); + + const doesXmlManifestContainRequiredInfo = ( + requiredElements: any, + existingElements: any, + pathTarget: string[], + ): boolean => { + const findElementsToSearchIn = ( + existingElements: any[], + pathTarget: string[], + ): any[] => { + const parts = [...pathTarget]; + const elementsToSearchNextIn = []; + for (const existingElement of existingElements) { + if (existingElement.name === pathTarget[0]) { + for (const el of existingElement.children) { + elementsToSearchNextIn.push(el); + } + } + } + if (elementsToSearchNextIn.length === 0) { + return []; + } else { + parts.splice(0, 1); + if (parts.length <= 0) { + return elementsToSearchNextIn; + } else { + return findElementsToSearchIn( + elementsToSearchNextIn, + parts, + ); + } + } + }; + const parseXmlToSearchable = ( + childElementsObj: any, + arrayToAddTo: any[], + ) => { + for (const childElementKey of Object.keys( + childElementsObj, + )) { + for (const occurannceOfElement of childElementsObj[ + childElementKey + ]) { + const toAdd: { + name: string; + attrs?: { [key: string]: any } | undefined; + children?: any[] | undefined; + } = { name: childElementKey }; + if (occurannceOfElement['$']) { + toAdd.attrs = { ...occurannceOfElement['$'] }; + } + if (occurannceOfElement['$$']) { + toAdd.children = []; + parseXmlToSearchable( + occurannceOfElement['$$'], + toAdd['children'], + ); + } + arrayToAddTo.push(toAdd); + } + } + }; + const doesElementMatch = ( + requiredElement: any, + existingElement: any, + ): boolean => { + if (requiredElement.name !== existingElement.name) { + return false; + } + if ( + (requiredElement.attrs !== undefined) !== + (existingElement.attrs !== undefined) + ) { + return false; + } else { + if (requiredElement.attrs !== undefined) { + const requiredELementAttrKeys = Object.keys( + requiredElement.attrs, + ); + for (const key of requiredELementAttrKeys) { + if ( + requiredElement.attrs[key] !== + existingElement.attrs[key] + ) { + return false; + } + } + } + } + if ( + (requiredElement.children !== undefined) !== + (existingElement.children !== undefined) + ) { + return false; + } else { + if (requiredElement.children !== undefined) { + // each req element is in existing element + for (const requiredElementItem of requiredElement.children) { + let foundRequiredElement = false; + for (const existingElementItem of existingElement.children) { + const foundRequiredElementIn = doesElementMatch( + requiredElementItem, + existingElementItem, + ); + if (foundRequiredElementIn) { + foundRequiredElement = true; + break; + } + } + if (!foundRequiredElement) { + return false; + } + } + } + } + return true; + }; + const parsedExistingElements: any[] = []; + const rootKeyOfExistingElements = + Object.keys(existingElements)[0]; + const rootOfExistingElementsToAdd: { + name: string; + attrs?: { [key: string]: any } | undefined; + children: any[]; + } = { name: rootKeyOfExistingElements, children: [] }; + if (existingElements[rootKeyOfExistingElements]['$']) { + rootOfExistingElementsToAdd.attrs = { + ...existingElements[rootKeyOfExistingElements]['$'], + }; + } + parseXmlToSearchable( + existingElements[rootKeyOfExistingElements]['$$'], + rootOfExistingElementsToAdd['children'], + ); + parsedExistingElements.push(rootOfExistingElementsToAdd); + const parsedRequiredElements: any[] = []; + const rootKeyOfRequiredElements = + Object.keys(requiredElements)[0]; + const rootOfRequiredElementsToAdd: { + name: string; + attrs?: { [key: string]: any } | undefined; + children: any[]; + } = { name: rootKeyOfRequiredElements, children: [] }; + if (requiredElements[rootKeyOfRequiredElements]['$']) { + rootOfRequiredElementsToAdd.attrs = { + ...requiredElements[rootKeyOfRequiredElements]['$'], + }; + } + parseXmlToSearchable( + requiredElements[rootKeyOfRequiredElements]['$$'], + rootOfRequiredElementsToAdd['children'], + ); + parsedRequiredElements.push(rootOfRequiredElementsToAdd); + const elementsToSearch = findElementsToSearchIn( + parsedExistingElements, + pathTarget, + ); + + for (const requiredElement of parsedRequiredElements) { + let foundMatch = false; + for (const existingElement of elementsToSearch) { + const doesContain = doesElementMatch( + requiredElement, + existingElement, + ); + if (doesContain) { + foundMatch = true; + break; + } + } + if (!foundMatch) { + return false; + } + } + return true; + }; + + if ( + !doesXmlManifestContainRequiredInfo( + parseXML(requiredManifestContentTrimmed, { + explicitChildren: true, + trim: true, + }), + parseXML(manifestContentTrimmed, { + explicitChildren: true, + trim: true, + }), + pathPartList, + ) + ) { + logger.warn( + `Android Configuration required for ${c.strong(p.id)}.\n` + + `Add the following to AndroidManifest.xml:\n` + + xmlElement, + ); + } } } else { if ( diff --git a/cli/src/util/xml.ts b/cli/src/util/xml.ts index 1a3d5897cc..b291325b55 100644 --- a/cli/src/util/xml.ts +++ b/cli/src/util/xml.ts @@ -14,9 +14,13 @@ export async function readXML(path: string): Promise { } } -export function parseXML(xmlStr: string): any { +export function parseXML(xmlStr: string, options?: xml2js.OptionsV2): any { + const parser = + options !== undefined + ? new xml2js.Parser({ ...options }) + : new xml2js.Parser(); let xmlObj; - xml2js.parseString(xmlStr, (err: any, result: any) => { + parser.parseString(xmlStr, (err: any, result: any) => { if (!err) { xmlObj = result; }