UNPKG

22.4 kBJavaScriptView Raw
1/**
2 Licensed to the Apache Software Foundation (ASF) under one
3 or more contributor license agreements. See the NOTICE file
4 distributed with this work for additional information
5 regarding copyright ownership. The ASF licenses this file
6 to you under the Apache License, Version 2.0 (the
7 "License"); you may not use this file except in compliance
8 with the License. You may obtain a copy of the License at
9
10 http://www.apache.org/licenses/LICENSE-2.0
11
12 Unless required by applicable law or agreed to in writing,
13 software distributed under the License is distributed on an
14 "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 KIND, either express or implied. See the License for the
16 specific language governing permissions and limitations
17 under the License.
18*/
19
20var et = require('elementtree');
21var xml = require('../util/xml-helpers');
22var CordovaError = require('../CordovaError/CordovaError');
23var fs = require('fs');
24var events = require('../events');
25
26/** Wraps a config.xml file */
27function ConfigParser (path) {
28 this.path = path;
29 try {
30 this.doc = xml.parseElementtreeSync(path);
31 this.cdvNamespacePrefix = getCordovaNamespacePrefix(this.doc);
32 et.register_namespace(this.cdvNamespacePrefix, 'http://cordova.apache.org/ns/1.0');
33 } catch (e) {
34 events.emit('error', 'Parsing ' + path + ' failed');
35 throw e;
36 }
37 var r = this.doc.getroot();
38 if (r.tag !== 'widget') {
39 throw new CordovaError(path + ' has incorrect root node name (expected "widget", was "' + r.tag + '")');
40 }
41}
42
43function getNodeTextSafe (el) {
44 return el && el.text && el.text.trim();
45}
46
47function findOrCreate (doc, name) {
48 var ret = doc.find(name);
49 if (!ret) {
50 ret = new et.Element(name);
51 doc.getroot().append(ret);
52 }
53 return ret;
54}
55
56function getCordovaNamespacePrefix (doc) {
57 var rootAtribs = Object.getOwnPropertyNames(doc.getroot().attrib);
58 var prefix = 'cdv';
59 for (var j = 0; j < rootAtribs.length; j++) {
60 if (rootAtribs[j].indexOf('xmlns:') === 0 &&
61 doc.getroot().attrib[rootAtribs[j]] === 'http://cordova.apache.org/ns/1.0') {
62 var strings = rootAtribs[j].split(':');
63 prefix = strings[1];
64 break;
65 }
66 }
67 return prefix;
68}
69
70/**
71 * Finds the value of an element's attribute
72 * @param {String} attributeName Name of the attribute to search for
73 * @param {Array} elems An array of ElementTree nodes
74 * @return {String}
75 */
76function findElementAttributeValue (attributeName, elems) {
77
78 elems = Array.isArray(elems) ? elems : [ elems ];
79
80 var value = elems.filter(function (elem) {
81 return elem.attrib.name.toLowerCase() === attributeName.toLowerCase();
82 }).map(function (filteredElems) {
83 return filteredElems.attrib.value;
84 }).pop();
85
86 return value || '';
87}
88
89ConfigParser.prototype = {
90 getAttribute: function (attr) {
91 return this.doc.getroot().attrib[attr];
92 },
93
94 packageName: function (id) {
95 return this.getAttribute('id');
96 },
97 setPackageName: function (id) {
98 this.doc.getroot().attrib['id'] = id;
99 },
100 android_packageName: function () {
101 return this.getAttribute('android-packageName');
102 },
103 android_activityName: function () {
104 return this.getAttribute('android-activityName');
105 },
106 ios_CFBundleIdentifier: function () {
107 return this.getAttribute('ios-CFBundleIdentifier');
108 },
109 name: function () {
110 return getNodeTextSafe(this.doc.find('name'));
111 },
112 setName: function (name) {
113 var el = findOrCreate(this.doc, 'name');
114 el.text = name;
115 },
116 shortName: function () {
117 return this.doc.find('name').attrib['short'] || this.name();
118 },
119 setShortName: function (shortname) {
120 var el = findOrCreate(this.doc, 'name');
121 if (!el.text) {
122 el.text = shortname;
123 }
124 el.attrib['short'] = shortname;
125 },
126 description: function () {
127 return getNodeTextSafe(this.doc.find('description'));
128 },
129 setDescription: function (text) {
130 var el = findOrCreate(this.doc, 'description');
131 el.text = text;
132 },
133 version: function () {
134 return this.getAttribute('version');
135 },
136 windows_packageVersion: function () {
137 return this.getAttribute('windows-packageVersion');
138 },
139 android_versionCode: function () {
140 return this.getAttribute('android-versionCode');
141 },
142 ios_CFBundleVersion: function () {
143 return this.getAttribute('ios-CFBundleVersion');
144 },
145 setVersion: function (value) {
146 this.doc.getroot().attrib['version'] = value;
147 },
148 author: function () {
149 return getNodeTextSafe(this.doc.find('author'));
150 },
151 getGlobalPreference: function (name) {
152 return findElementAttributeValue(name, this.doc.findall('preference'));
153 },
154 setGlobalPreference: function (name, value) {
155 var pref = this.doc.find('preference[@name="' + name + '"]');
156 if (!pref) {
157 pref = new et.Element('preference');
158 pref.attrib.name = name;
159 this.doc.getroot().append(pref);
160 }
161 pref.attrib.value = value;
162 },
163 getPlatformPreference: function (name, platform) {
164 return findElementAttributeValue(name, this.doc.findall('platform[@name=\'' + platform + '\']/preference'));
165 },
166 getPreference: function (name, platform) {
167
168 var platformPreference = '';
169
170 if (platform) {
171 platformPreference = this.getPlatformPreference(name, platform);
172 }
173
174 return platformPreference || this.getGlobalPreference(name);
175
176 },
177 /**
178 * Returns all resources for the platform specified.
179 * @param {String} platform The platform.
180 * @param {string} resourceName Type of static resources to return.
181 * "icon" and "splash" currently supported.
182 * @return {Array} Resources for the platform specified.
183 */
184 getStaticResources: function (platform, resourceName) {
185 var ret = [];
186 var staticResources = [];
187 if (platform) { // platform specific icons
188 this.doc.findall('platform[@name=\'' + platform + '\']/' + resourceName).forEach(function (elt) {
189 elt.platform = platform; // mark as platform specific resource
190 staticResources.push(elt);
191 });
192 }
193 // root level resources
194 staticResources = staticResources.concat(this.doc.findall(resourceName));
195 // parse resource elements
196 var that = this;
197 staticResources.forEach(function (elt) {
198 var res = {};
199 res.src = elt.attrib.src;
200 res.target = elt.attrib.target || undefined;
201 res.density = elt.attrib['density'] || elt.attrib[that.cdvNamespacePrefix + ':density'] || elt.attrib['gap:density'];
202 res.platform = elt.platform || null; // null means icon represents default icon (shared between platforms)
203 res.width = +elt.attrib.width || undefined;
204 res.height = +elt.attrib.height || undefined;
205
206 // default icon
207 if (!res.width && !res.height && !res.density) {
208 ret.defaultResource = res;
209 }
210 ret.push(res);
211 });
212
213 /**
214 * Returns resource with specified width and/or height.
215 * @param {number} width Width of resource.
216 * @param {number} height Height of resource.
217 * @return {Resource} Resource object or null if not found.
218 */
219 ret.getBySize = function (width, height) {
220 return ret.filter(function (res) {
221 if (!res.width && !res.height) {
222 return false;
223 }
224 return ((!res.width || (width === res.width)) &&
225 (!res.height || (height === res.height)));
226 })[0] || null;
227 };
228
229 /**
230 * Returns resource with specified density.
231 * @param {string} density Density of resource.
232 * @return {Resource} Resource object or null if not found.
233 */
234 ret.getByDensity = function (density) {
235 return ret.filter(function (res) {
236 return res.density === density;
237 })[0] || null;
238 };
239
240 /** Returns default icons */
241 ret.getDefault = function () {
242 return ret.defaultResource;
243 };
244
245 return ret;
246 },
247
248 /**
249 * Returns all icons for specific platform.
250 * @param {string} platform Platform name
251 * @return {Resource[]} Array of icon objects.
252 */
253 getIcons: function (platform) {
254 return this.getStaticResources(platform, 'icon');
255 },
256
257 /**
258 * Returns all splash images for specific platform.
259 * @param {string} platform Platform name
260 * @return {Resource[]} Array of Splash objects.
261 */
262 getSplashScreens: function (platform) {
263 return this.getStaticResources(platform, 'splash');
264 },
265
266 /**
267 * Returns all resource-files for a specific platform.
268 * @param {string} platform Platform name
269 * @param {boolean} includeGlobal Whether to return resource-files at the
270 * root level.
271 * @return {Resource[]} Array of resource file objects.
272 */
273 getFileResources: function (platform, includeGlobal) {
274 var fileResources = [];
275
276 if (platform) { // platform specific resources
277 fileResources = this.doc.findall('platform[@name=\'' + platform + '\']/resource-file').map(function (tag) {
278 return {
279 platform: platform,
280 src: tag.attrib.src,
281 target: tag.attrib.target,
282 versions: tag.attrib.versions,
283 deviceTarget: tag.attrib['device-target'],
284 arch: tag.attrib.arch
285 };
286 });
287 }
288
289 if (includeGlobal) {
290 this.doc.findall('resource-file').forEach(function (tag) {
291 fileResources.push({
292 platform: platform || null,
293 src: tag.attrib.src,
294 target: tag.attrib.target,
295 versions: tag.attrib.versions,
296 deviceTarget: tag.attrib['device-target'],
297 arch: tag.attrib.arch
298 });
299 });
300 }
301
302 return fileResources;
303 },
304
305 /**
306 * Returns all hook scripts for the hook type specified.
307 * @param {String} hook The hook type.
308 * @param {Array} platforms Platforms to look for scripts into (root scripts will be included as well).
309 * @return {Array} Script elements.
310 */
311 getHookScripts: function (hook, platforms) {
312 var self = this;
313 var scriptElements = self.doc.findall('./hook');
314
315 if (platforms) {
316 platforms.forEach(function (platform) {
317 scriptElements = scriptElements.concat(self.doc.findall('./platform[@name="' + platform + '"]/hook'));
318 });
319 }
320
321 function filterScriptByHookType (el) {
322 return el.attrib.src && el.attrib.type && el.attrib.type.toLowerCase() === hook;
323 }
324
325 return scriptElements.filter(filterScriptByHookType);
326 },
327 /**
328 * Returns a list of plugin (IDs).
329 *
330 * This function also returns any plugin's that
331 * were defined using the legacy <feature> tags.
332 * @return {string[]} Array of plugin IDs
333 */
334 getPluginIdList: function () {
335 var plugins = this.doc.findall('plugin');
336 var result = plugins.map(function (plugin) {
337 return plugin.attrib.name;
338 });
339 var features = this.doc.findall('feature');
340 features.forEach(function (element) {
341 var idTag = element.find('./param[@name="id"]');
342 if (idTag) {
343 result.push(idTag.attrib.value);
344 }
345 });
346 return result;
347 },
348 getPlugins: function () {
349 return this.getPluginIdList().map(function (pluginId) {
350 return this.getPlugin(pluginId);
351 }, this);
352 },
353 /**
354 * Adds a plugin element. Does not check for duplicates.
355 * @name addPlugin
356 * @function
357 * @param {object} attributes name and spec are supported
358 * @param {Array|object} variables name, value or arbitary object
359 */
360 addPlugin: function (attributes, variables) {
361 if (!attributes && !attributes.name) return;
362 var el = new et.Element('plugin');
363 el.attrib.name = attributes.name;
364 if (attributes.spec) {
365 el.attrib.spec = attributes.spec;
366 }
367
368 // support arbitrary object as variables source
369 if (variables && typeof variables === 'object' && !Array.isArray(variables)) {
370 variables = Object.keys(variables)
371 .map(function (variableName) {
372 return {name: variableName, value: variables[variableName]};
373 });
374 }
375
376 if (variables) {
377 variables.forEach(function (variable) {
378 el.append(new et.Element('variable', { name: variable.name, value: variable.value }));
379 });
380 }
381 this.doc.getroot().append(el);
382 },
383 /**
384 * Retrives the plugin with the given id or null if not found.
385 *
386 * This function also returns any plugin's that
387 * were defined using the legacy <feature> tags.
388 * @name getPlugin
389 * @function
390 * @param {String} id
391 * @returns {object} plugin including any variables
392 */
393 getPlugin: function (id) {
394 if (!id) {
395 return undefined;
396 }
397 var pluginElement = this.doc.find('./plugin/[@name="' + id + '"]');
398 if (pluginElement === null) {
399 var legacyFeature = this.doc.find('./feature/param[@name="id"][@value="' + id + '"]/..');
400 if (legacyFeature) {
401 events.emit('log', 'Found deprecated feature entry for ' + id + ' in config.xml.');
402 return featureToPlugin(legacyFeature);
403 }
404 return undefined;
405 }
406 var plugin = {};
407
408 plugin.name = pluginElement.attrib.name;
409 plugin.spec = pluginElement.attrib.spec || pluginElement.attrib.src || pluginElement.attrib.version;
410 plugin.variables = {};
411 var variableElements = pluginElement.findall('variable');
412 variableElements.forEach(function (varElement) {
413 var name = varElement.attrib.name;
414 var value = varElement.attrib.value;
415 if (name) {
416 plugin.variables[name] = value;
417 }
418 });
419 return plugin;
420 },
421 /**
422 * Remove the plugin entry with give name (id).
423 *
424 * This function also operates on any plugin's that
425 * were defined using the legacy <feature> tags.
426 * @name removePlugin
427 * @function
428 * @param id name of the plugin
429 */
430 removePlugin: function (id) {
431 if (id) {
432 var plugins = this.doc.findall('./plugin/[@name="' + id + '"]')
433 .concat(this.doc.findall('./feature/param[@name="id"][@value="' + id + '"]/..'));
434 var children = this.doc.getroot().getchildren();
435 plugins.forEach(function (plugin) {
436 var idx = children.indexOf(plugin);
437 if (idx > -1) {
438 children.splice(idx, 1);
439 }
440 });
441 }
442 },
443
444 // Add any element to the root
445 addElement: function (name, attributes) {
446 var el = et.Element(name);
447 for (var a in attributes) {
448 el.attrib[a] = attributes[a];
449 }
450 this.doc.getroot().append(el);
451 },
452
453 /**
454 * Adds an engine. Does not check for duplicates.
455 * @param {String} name the engine name
456 * @param {String} spec engine source location or version (optional)
457 */
458 addEngine: function (name, spec) {
459 if (!name) return;
460 var el = et.Element('engine');
461 el.attrib.name = name;
462 if (spec) {
463 el.attrib.spec = spec;
464 }
465 this.doc.getroot().append(el);
466 },
467 /**
468 * Removes all the engines with given name
469 * @param {String} name the engine name.
470 */
471 removeEngine: function (name) {
472 var engines = this.doc.findall('./engine/[@name="' + name + '"]');
473 for (var i = 0; i < engines.length; i++) {
474 var children = this.doc.getroot().getchildren();
475 var idx = children.indexOf(engines[i]);
476 if (idx > -1) {
477 children.splice(idx, 1);
478 }
479 }
480 },
481 getEngines: function () {
482 var engines = this.doc.findall('./engine');
483 return engines.map(function (engine) {
484 var spec = engine.attrib.spec || engine.attrib.version;
485 return {
486 'name': engine.attrib.name,
487 'spec': spec || null
488 };
489 });
490 },
491 /* Get all the access tags */
492 getAccesses: function () {
493 var accesses = this.doc.findall('./access');
494 return accesses.map(function (access) {
495 var minimum_tls_version = access.attrib['minimum-tls-version']; /* String */
496 var requires_forward_secrecy = access.attrib['requires-forward-secrecy']; /* Boolean */
497 var requires_certificate_transparency = access.attrib['requires-certificate-transparency']; /* Boolean */
498 var allows_arbitrary_loads_in_web_content = access.attrib['allows-arbitrary-loads-in-web-content']; /* Boolean */
499 var allows_arbitrary_loads_in_media = access.attrib['allows-arbitrary-loads-in-media']; /* Boolean (DEPRECATED) */
500 var allows_arbitrary_loads_for_media = access.attrib['allows-arbitrary-loads-for-media']; /* Boolean */
501 var allows_local_networking = access.attrib['allows-local-networking']; /* Boolean */
502
503 return {
504 'origin': access.attrib.origin,
505 'minimum_tls_version': minimum_tls_version,
506 'requires_forward_secrecy': requires_forward_secrecy,
507 'requires_certificate_transparency': requires_certificate_transparency,
508 'allows_arbitrary_loads_in_web_content': allows_arbitrary_loads_in_web_content,
509 'allows_arbitrary_loads_in_media': allows_arbitrary_loads_in_media,
510 'allows_arbitrary_loads_for_media': allows_arbitrary_loads_for_media,
511 'allows_local_networking': allows_local_networking
512 };
513 });
514 },
515 /* Get all the allow-navigation tags */
516 getAllowNavigations: function () {
517 var allow_navigations = this.doc.findall('./allow-navigation');
518 return allow_navigations.map(function (allow_navigation) {
519 var minimum_tls_version = allow_navigation.attrib['minimum-tls-version']; /* String */
520 var requires_forward_secrecy = allow_navigation.attrib['requires-forward-secrecy']; /* Boolean */
521 var requires_certificate_transparency = allow_navigation.attrib['requires-certificate-transparency']; /* Boolean */
522
523 return {
524 'href': allow_navigation.attrib.href,
525 'minimum_tls_version': minimum_tls_version,
526 'requires_forward_secrecy': requires_forward_secrecy,
527 'requires_certificate_transparency': requires_certificate_transparency
528 };
529 });
530 },
531 /* Get all the allow-intent tags */
532 getAllowIntents: function () {
533 var allow_intents = this.doc.findall('./allow-intent');
534 return allow_intents.map(function (allow_intent) {
535 return {
536 'href': allow_intent.attrib.href
537 };
538 });
539 },
540 /* Get all edit-config tags */
541 getEditConfigs: function (platform) {
542 var platform_tag = this.doc.find('./platform[@name="' + platform + '"]');
543 var platform_edit_configs = platform_tag ? platform_tag.findall('edit-config') : [];
544
545 var edit_configs = this.doc.findall('edit-config').concat(platform_edit_configs);
546
547 return edit_configs.map(function (tag) {
548 var editConfig =
549 {
550 file: tag.attrib['file'],
551 target: tag.attrib['target'],
552 mode: tag.attrib['mode'],
553 id: 'config.xml',
554 xmls: tag.getchildren()
555 };
556 return editConfig;
557 });
558 },
559
560 /* Get all config-file tags */
561 getConfigFiles: function (platform) {
562 var platform_tag = this.doc.find('./platform[@name="' + platform + '"]');
563 var platform_config_files = platform_tag ? platform_tag.findall('config-file') : [];
564
565 var config_files = this.doc.findall('config-file').concat(platform_config_files);
566
567 return config_files.map(function (tag) {
568 var configFile =
569 {
570 target: tag.attrib['target'],
571 parent: tag.attrib['parent'],
572 after: tag.attrib['after'],
573 xmls: tag.getchildren(),
574 // To support demuxing via versions
575 versions: tag.attrib['versions'],
576 deviceTarget: tag.attrib['device-target']
577 };
578 return configFile;
579 });
580 },
581
582 write: function () {
583 fs.writeFileSync(this.path, this.doc.write({indent: 4}), 'utf-8');
584 }
585};
586
587function featureToPlugin (featureElement) {
588 var plugin = {};
589 plugin.variables = [];
590 var pluginVersion,
591 pluginSrc;
592
593 var nodes = featureElement.findall('param');
594 nodes.forEach(function (element) {
595 var n = element.attrib.name;
596 var v = element.attrib.value;
597 if (n === 'id') {
598 plugin.name = v;
599 } else if (n === 'version') {
600 pluginVersion = v;
601 } else if (n === 'url' || n === 'installPath') {
602 pluginSrc = v;
603 } else {
604 plugin.variables[n] = v;
605 }
606 });
607
608 var spec = pluginSrc || pluginVersion;
609 if (spec) {
610 plugin.spec = spec;
611 }
612
613 return plugin;
614}
615module.exports = ConfigParser;