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;
}