blob: 4a4d7ac251f3b62b7a8f1145c703674518ab22b9 [file] [log] [blame]
/*
*
* Copyright 2013 Anis Kadri
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*
*/
var fs = require('fs'),
path = require('path'),
glob = require('glob'),
plist = require('plist'),
bplist = require('bplist-parser'),
et = require('elementtree'),
xml_helpers = require('./../util/xml-helpers'),
plist_helpers = require('./../util/plist-helpers');
function checkPlatform(platform) {
if (!(platform in require('./../platforms'))) throw new Error('platform "' + platform + '" not recognized.');
}
module.exports = {
add_installed_plugin_to_prepare_queue:function(plugins_dir, plugin, platform, vars, is_top_level) {
checkPlatform(platform);
var config = module.exports.get_platform_json(plugins_dir, platform);
config.prepare_queue.installed.push({'plugin':plugin, 'vars':vars, 'topLevel':is_top_level});
module.exports.save_platform_json(config, plugins_dir, platform);
},
add_uninstalled_plugin_to_prepare_queue:function(plugins_dir, plugin, platform, is_top_level) {
checkPlatform(platform);
var plugin_xml = xml_helpers.parseElementtreeSync(path.join(plugins_dir, plugin, 'plugin.xml'));
var config = module.exports.get_platform_json(plugins_dir, platform);
config.prepare_queue.uninstalled.push({'plugin':plugin, 'id':plugin_xml._root.attrib['id'], 'topLevel':is_top_level});
module.exports.save_platform_json(config, plugins_dir, platform);
},
get_platform_json:function(plugins_dir, platform) {
checkPlatform(platform);
var filepath = path.join(plugins_dir, platform + '.json');
if (fs.existsSync(filepath)) {
return JSON.parse(fs.readFileSync(filepath, 'utf-8'));
} else {
var config = {
prepare_queue:{installed:[], uninstalled:[]},
config_munge:{},
installed_plugins:{},
dependent_plugins:{}
};
fs.writeFileSync(filepath, JSON.stringify(config), 'utf-8');
return config;
}
},
save_platform_json:function(config, plugins_dir, platform) {
checkPlatform(platform);
var filepath = path.join(plugins_dir, platform + '.json');
fs.writeFileSync(filepath, JSON.stringify(config), 'utf-8');
},
generate_plugin_config_munge:function(plugin_dir, platform, project_dir, vars) {
checkPlatform(platform);
vars = vars || {};
var platform_handler = require('./../platforms')[platform];
// Add PACKAGE_NAME variable into vars
if (!vars['PACKAGE_NAME']) {
vars['PACKAGE_NAME'] = platform_handler.package_name(project_dir);
}
var munge = {};
var plugin_xml = xml_helpers.parseElementtreeSync(path.join(plugin_dir, 'plugin.xml'));
var platformTag = plugin_xml.find('platform[@name="' + platform + '"]');
var changes = [];
// add platform-agnostic config changes
changes = changes.concat(plugin_xml.findall('config-file'));
if (platformTag) {
// add platform-specific config changes if they exist
changes = changes.concat(platformTag.findall('config-file'));
// note down plugins-plist munges in special section of munge obj
var plugins_plist = platformTag.findall('plugins-plist');
plugins_plist.forEach(function(pl) {
if (!munge['plugins-plist']) {
munge['plugins-plist'] = {};
}
var key = pl.attrib['key'];
var value = pl.attrib['string'];
if (!munge['plugins-plist'][key]) {
munge['plugins-plist'][key] = value;
}
});
}
changes.forEach(function(change) {
var target = change.attrib['target'];
var parent = change.attrib['parent'];
if (!munge[target]) {
munge[target] = {};
}
if (!munge[target][parent]) {
munge[target][parent] = {};
}
var xmls = change.getchildren();
xmls.forEach(function(xml) {
// 1. stringify each xml
var stringified = (new et.ElementTree(xml)).write({xml_declaration:false});
// interp vars
vars && Object.keys(vars).forEach(function(key) {
var regExp = new RegExp("\\$" + key, "g");
stringified = stringified.replace(regExp, vars[key]);
});
// 2. add into munge
if (!munge[target][parent][stringified]) {
munge[target][parent][stringified] = 0;
}
munge[target][parent][stringified] += 1;
});
});
return munge;
},
remove_plugin_changes:function(platform, project_dir, plugins_dir, plugin_name, plugin_id, is_top_level, should_decrement) {
var platform_config = module.exports.get_platform_json(plugins_dir, platform);
var plugin_dir = path.join(plugins_dir, plugin_name);
var plugin_vars = (is_top_level ? platform_config.installed_plugins[plugin_id] : platform_config.dependent_plugins[plugin_id]);
// get config munge, aka how did this plugin change various config files
var config_munge = module.exports.generate_plugin_config_munge(plugin_dir, platform, project_dir, plugin_vars);
// global munge looks at all plugins' changes to config files
var global_munge = platform_config.config_munge;
// Traverse config munge and decrement global munge
Object.keys(config_munge).forEach(function(file) {
if (file == 'plugins-plist' && platform == 'ios') {
if (global_munge[file]) {
Object.keys(config_munge[file]).forEach(function(key) {
if (global_munge[file][key]) {
// prune from old plist if exists
var plistfile = glob.sync(path.join(project_dir, '**', '{PhoneGap,Cordova}.plist'));
if (plistfile.length > 0) {
plistfile = plistfile[0];
// determine if this is a binary or ascii plist and choose the parser
// this is temporary until binary support is added to node-plist
var pl = (isBinaryPlist(plistfile) ? bplist : plist);
var plistObj = pl.parseFileSync(plistfile);
delete plistObj.Plugins[key];
fs.writeFileSync(plistfile, plist.build(plistObj));
}
delete global_munge[file][key];
}
});
}
} else if (global_munge[file]) {
Object.keys(config_munge[file]).forEach(function(selector) {
if (global_munge[file][selector]) {
Object.keys(config_munge[file][selector]).forEach(function(xml_child) {
if (global_munge[file][selector][xml_child]) {
if (should_decrement) {
global_munge[file][selector][xml_child] -= 1;
}
if (global_munge[file][selector][xml_child] === 0) {
// this xml child is no longer necessary, prune it
// config.xml referenced in ios config changes refer to the project's config.xml, which we need to glob for.
var filepath = resolveConfigFilePath(project_dir, platform, file);
if (fs.existsSync(filepath)) {
if (path.extname(filepath) == '.xml') {
var xml_to_prune = [et.XML(xml_child)];
var doc = xml_helpers.parseElementtreeSync(filepath);
if (xml_helpers.pruneXML(doc, xml_to_prune, selector)) {
// were good, write out the file!
fs.writeFileSync(filepath, doc.write({indent: 4}), 'utf-8');
} else {
// uh oh
throw new Error('pruning xml at selector "' + selector + '" from "' + filepath + '" during config uninstall went bad :(');
}
} else {
// plist file
var pl = (isBinaryPlist(filepath) ? bplist : plist);
var plistObj = pl.parseFileSync(filepath);
if (plist_helpers.prunePLIST(plistObj, xml_child, selector)) {
fs.writeFileSync(filepath, plist.build(plistObj));
} else {
throw new Error('grafting to plist "' + filepath + '" during config install went bad :(');
}
}
}
delete global_munge[file][selector][xml_child];
}
}
});
}
});
}
});
platform_config.config_munge = global_munge;
// Remove from installed_plugins
if (is_top_level) {
delete platform_config.installed_plugins[plugin_id]
} else {
delete platform_config.dependent_plugins[plugin_id]
}
// save
module.exports.save_platform_json(platform_config, plugins_dir, platform);
},
add_plugin_changes:function(platform, project_dir, plugins_dir, plugin_id, plugin_vars, is_top_level, should_increment) {
var platform_config = module.exports.get_platform_json(plugins_dir, platform);
var plugin_dir = path.join(plugins_dir, plugin_id);
plugin_id = xml_helpers.parseElementtreeSync(path.join(plugin_dir, 'plugin.xml'), 'utf-8')._root.attrib['id'];
// get config munge, aka how should this plugin change various config files
var config_munge = module.exports.generate_plugin_config_munge(plugin_dir, platform, project_dir, plugin_vars);
// global munge looks at all plugins' changes to config files
var global_munge = platform_config.config_munge;
// Traverse config munge and decrement global munge
Object.keys(config_munge).forEach(function(file) {
if (!global_munge[file]) {
global_munge[file] = {};
}
Object.keys(config_munge[file]).forEach(function(selector) {
if (file == 'plugins-plist' && platform == 'ios') {
var key = selector;
if (!global_munge[file][key]) {
// this key does not exist, so add it to plist
global_munge[file][key] = config_munge[file][key];
var plistfile = glob.sync(path.join(project_dir, '**', '{PhoneGap,Cordova}.plist'));
if (plistfile.length > 0) {
plistfile = plistfile[0];
// determine if this is a binary or ascii plist and choose the parser
// this is temporary until binary support is added to node-plist
var pl = (isBinaryPlist(plistfile) ? bplist : plist);
var plistObj = pl.parseFileSync(plistfile);
plistObj.Plugins[key] = config_munge[file][key];
fs.writeFileSync(plistfile, plist.build(plistObj));
}
}
} else {
if (!global_munge[file][selector]) {
global_munge[file][selector] = {};
}
Object.keys(config_munge[file][selector]).forEach(function(xml_child) {
if (!global_munge[file][selector][xml_child]) {
global_munge[file][selector][xml_child] = 0;
}
if (should_increment) {
global_munge[file][selector][xml_child] += 1;
}
if (global_munge[file][selector][xml_child] == 1) {
// this xml child is new, graft it (only if config file exists)
// config file may be in a place not exactly specified in the target
var filepath = resolveConfigFilePath(project_dir, platform, file);
if (fs.existsSync(filepath)) {
// look at ext and do proper config change based on file type
if (path.extname(filepath) == '.xml') {
var xml_to_graft = [et.XML(xml_child)];
var doc = xml_helpers.parseElementtreeSync(filepath);
if (xml_helpers.graftXML(doc, xml_to_graft, selector)) {
// were good, write out the file!
fs.writeFileSync(filepath, doc.write({indent: 4}), 'utf-8');
} else {
// uh oh
throw new Error('grafting xml at selector "' + selector + '" from "' + filepath + '" during config install went bad :(');
}
} else {
// plist file
var pl = (isBinaryPlist(filepath) ? bplist : plist);
var plistObj = pl.parseFileSync(filepath);
if (plist_helpers.graftPLIST(plistObj, xml_child, selector)) {
fs.writeFileSync(filepath, plist.build(plistObj));
} else {
throw new Error('grafting to plist "' + filepath + '" during config install went bad :(');
}
}
} else {
// ignore if file doesnt exist
}
}
});
}
});
});
platform_config.config_munge = global_munge;
// Move to installed_plugins if it is a top-level plugin
if (is_top_level) {
platform_config.installed_plugins[plugin_id] = plugin_vars || {};
} else {
platform_config.dependent_plugins[plugin_id] = plugin_vars || {};
}
// save
module.exports.save_platform_json(platform_config, plugins_dir, platform);
},
process:function(plugins_dir, project_dir, platform) {
checkPlatform(platform);
var platform_config = module.exports.get_platform_json(plugins_dir, platform);
// Uninstallation first
platform_config.prepare_queue.uninstalled.forEach(function(u) {
module.exports.remove_plugin_changes(platform, project_dir, plugins_dir, u.plugin, u.id, u.topLevel, true);
});
// Now handle installation
platform_config.prepare_queue.installed.forEach(function(u) {
module.exports.add_plugin_changes(platform, project_dir, plugins_dir, u.plugin, u.vars, u.topLevel, true);
});
platform_config = module.exports.get_platform_json(plugins_dir, platform);
// Empty out uninstalled queue.
platform_config.prepare_queue.uninstalled = [];
// Empty out installed queue.
platform_config.prepare_queue.installed = [];
// save
module.exports.save_platform_json(platform_config, plugins_dir, platform);
}
};
// determine if a plist file is binary
function isBinaryPlist(filename) {
// I wish there was a synchronous way to read only the first 6 bytes of a
// file. This is wasteful :/
var buf = '' + fs.readFileSync(filename, 'utf8');
// binary plists start with a magic header, "bplist"
return buf.substring(0, 6) === 'bplist';
}
// Some config-file target attributes are not qualified with a full leading directory, or contain wildcards. resolve to a real path in this function
function resolveConfigFilePath(project_dir, platform, file) {
var filepath = path.join(project_dir, file);
if (file.indexOf('*') > -1) {
// handle wildcards in targets using glob.
var matches = glob.sync(path.join(project_dir, '**', file));
if (matches.length) filepath = matches[0];
} else {
// special-case config.xml target that is just "config.xml". this should be resolved to the real location of the file.
if (file == 'config.xml') {
if (platform == 'android') {
filepath = path.join(project_dir, 'res', 'xml', 'config.xml');
} else {
var matches = glob.sync(path.join(project_dir, '**', 'config.xml'));
if (matches.length) filepath = matches[0];
}
}
}
return filepath;
}