summaryrefslogtreecommitdiff
path: root/StoneIsland/platforms/ios/cordova/lib/prepare.js
diff options
context:
space:
mode:
authorJules Laplace <jules@okfoc.us>2016-11-08 11:46:59 -0500
committerJules Laplace <jules@okfoc.us>2016-11-08 11:46:59 -0500
commit5fa81da81260d65113f57a293b6256d334fe8e2d (patch)
tree01d3dd7ab7a1febccd20de1756d0801a64ae64e9 /StoneIsland/platforms/ios/cordova/lib/prepare.js
parente5652e9cd560ccda249819857c207643820b075f (diff)
parent7773d1d0686de69504e9b820efdb3e94d72eff04 (diff)
le build
Diffstat (limited to 'StoneIsland/platforms/ios/cordova/lib/prepare.js')
-rwxr-xr-xStoneIsland/platforms/ios/cordova/lib/prepare.js1003
1 files changed, 1003 insertions, 0 deletions
diff --git a/StoneIsland/platforms/ios/cordova/lib/prepare.js b/StoneIsland/platforms/ios/cordova/lib/prepare.js
new file mode 100755
index 00000000..8d1cda94
--- /dev/null
+++ b/StoneIsland/platforms/ios/cordova/lib/prepare.js
@@ -0,0 +1,1003 @@
+/**
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you 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 Q = require('q');
+var fs = require('fs');
+var path = require('path');
+var shell = require('shelljs');
+var xcode = require('xcode');
+var unorm = require('unorm');
+var plist = require('plist');
+var URL = require('url');
+var events = require('cordova-common').events;
+var xmlHelpers = require('cordova-common').xmlHelpers;
+var ConfigParser = require('cordova-common').ConfigParser;
+var CordovaError = require('cordova-common').CordovaError;
+var PlatformJson = require('cordova-common').PlatformJson;
+var PlatformMunger = require('cordova-common').ConfigChanges.PlatformMunger;
+var PluginInfoProvider = require('cordova-common').PluginInfoProvider;
+var FileUpdater = require('cordova-common').FileUpdater;
+
+/*jshint sub:true*/
+
+module.exports.prepare = function (cordovaProject, options) {
+ var self = this;
+
+ var platformJson = PlatformJson.load(this.locations.root, 'ios');
+ var munger = new PlatformMunger('ios', this.locations.root, platformJson, new PluginInfoProvider());
+
+ this._config = updateConfigFile(cordovaProject.projectConfig, munger, this.locations);
+
+ // Update own www dir with project's www assets and plugins' assets and js-files
+ return Q.when(updateWww(cordovaProject, this.locations))
+ .then(function () {
+ // update project according to config.xml changes.
+ return updateProject(self._config, self.locations);
+ })
+ .then(function () {
+ updateIcons(cordovaProject, self.locations);
+ updateSplashScreens(cordovaProject, self.locations);
+ updateLaunchStoryboardImages(cordovaProject, self.locations);
+ })
+ .then(function () {
+ events.emit('verbose', 'Prepared iOS project successfully');
+ });
+};
+
+module.exports.clean = function (options) {
+ // A cordovaProject isn't passed into the clean() function, because it might have
+ // been called from the platform shell script rather than the CLI. Check for the
+ // noPrepare option passed in by the non-CLI clean script. If that's present, or if
+ // there's no config.xml found at the project root, then don't clean prepared files.
+ var projectRoot = path.resolve(this.root, '../..');
+ var projectConfigFile = path.join(projectRoot, 'config.xml');
+ if ((options && options.noPrepare) || !fs.existsSync(projectConfigFile) ||
+ !fs.existsSync(this.locations.configXml)) {
+ return Q();
+ }
+
+ var projectConfig = new ConfigParser(this.locations.configXml);
+
+ var self = this;
+ return Q().then(function () {
+ cleanWww(projectRoot, self.locations);
+ cleanIcons(projectRoot, projectConfig, self.locations);
+ cleanSplashScreens(projectRoot, projectConfig, self.locations);
+ cleanLaunchStoryboardImages(projectRoot, projectConfig, self.locations);
+ });
+};
+
+/**
+ * Updates config files in project based on app's config.xml and config munge,
+ * generated by plugins.
+ *
+ * @param {ConfigParser} sourceConfig A project's configuration that will
+ * be merged into platform's config.xml
+ * @param {ConfigChanges} configMunger An initialized ConfigChanges instance
+ * for this platform.
+ * @param {Object} locations A map of locations for this platform
+ *
+ * @return {ConfigParser} An instance of ConfigParser, that
+ * represents current project's configuration. When returned, the
+ * configuration is already dumped to appropriate config.xml file.
+ */
+function updateConfigFile(sourceConfig, configMunger, locations) {
+ events.emit('verbose', 'Generating platform-specific config.xml from defaults for iOS at ' + locations.configXml);
+
+ // First cleanup current config and merge project's one into own
+ // Overwrite platform config.xml with defaults.xml.
+ shell.cp('-f', locations.defaultConfigXml, locations.configXml);
+
+ // Then apply config changes from global munge to all config files
+ // in project (including project's config)
+ configMunger.reapply_global_munge().save_all();
+
+ events.emit('verbose', 'Merging project\'s config.xml into platform-specific iOS config.xml');
+ // Merge changes from app's config.xml into platform's one
+ var config = new ConfigParser(locations.configXml);
+ xmlHelpers.mergeXml(sourceConfig.doc.getroot(),
+ config.doc.getroot(), 'ios', /*clobber=*/true);
+
+ config.write();
+ return config;
+}
+
+/**
+ * Logs all file operations via the verbose event stream, indented.
+ */
+function logFileOp(message) {
+ events.emit('verbose', ' ' + message);
+}
+
+/**
+ * Updates platform 'www' directory by replacing it with contents of
+ * 'platform_www' and app www. Also copies project's overrides' folder into
+ * the platform 'www' folder
+ *
+ * @param {Object} cordovaProject An object which describes cordova project.
+ * @param {boolean} destinations An object that contains destinations
+ * paths for www files.
+ */
+function updateWww(cordovaProject, destinations) {
+ var sourceDirs = [
+ path.relative(cordovaProject.root, cordovaProject.locations.www),
+ path.relative(cordovaProject.root, destinations.platformWww)
+ ];
+
+ // If project contains 'merges' for our platform, use them as another overrides
+ var merges_path = path.join(cordovaProject.root, 'merges', 'ios');
+ if (fs.existsSync(merges_path)) {
+ events.emit('verbose', 'Found "merges/ios" folder. Copying its contents into the iOS project.');
+ sourceDirs.push(path.join('merges', 'ios'));
+ }
+
+ var targetDir = path.relative(cordovaProject.root, destinations.www);
+ events.emit(
+ 'verbose', 'Merging and updating files from [' + sourceDirs.join(', ') + '] to ' + targetDir);
+ FileUpdater.mergeAndUpdateDir(
+ sourceDirs, targetDir, { rootDir: cordovaProject.root }, logFileOp);
+}
+
+/**
+ * Cleans all files from the platform 'www' directory.
+ */
+function cleanWww(projectRoot, locations) {
+ var targetDir = path.relative(projectRoot, locations.www);
+ events.emit('verbose', 'Cleaning ' + targetDir);
+
+ // No source paths are specified, so mergeAndUpdateDir() will clear the target directory.
+ FileUpdater.mergeAndUpdateDir(
+ [], targetDir, { rootDir: projectRoot, all: true }, logFileOp);
+}
+
+/**
+ * Updates project structure and AndroidManifest according to project's configuration.
+ *
+ * @param {ConfigParser} platformConfig A project's configuration that will
+ * be used to update project
+ * @param {Object} locations A map of locations for this platform (In/Out)
+ */
+function updateProject(platformConfig, locations) {
+
+ // CB-6992 it is necessary to normalize characters
+ // because node and shell scripts handles unicode symbols differently
+ // We need to normalize the name to NFD form since iOS uses NFD unicode form
+ var name = unorm.nfd(platformConfig.name());
+ var pkg = platformConfig.getAttribute('ios-CFBundleIdentifier') || platformConfig.packageName();
+ var version = platformConfig.version();
+
+ var originalName = path.basename(locations.xcodeCordovaProj);
+
+ // Update package id (bundle id)
+ var plistFile = path.join(locations.xcodeCordovaProj, originalName + '-Info.plist');
+ var infoPlist = plist.parse(fs.readFileSync(plistFile, 'utf8'));
+ infoPlist['CFBundleIdentifier'] = pkg;
+
+ // Update version (bundle version)
+ infoPlist['CFBundleShortVersionString'] = version;
+ var CFBundleVersion = platformConfig.getAttribute('ios-CFBundleVersion') || default_CFBundleVersion(version);
+ infoPlist['CFBundleVersion'] = CFBundleVersion;
+
+ if (platformConfig.getAttribute('defaultlocale')) {
+ infoPlist['CFBundleDevelopmentRegion'] = platformConfig.getAttribute('defaultlocale');
+ }
+
+ // replace Info.plist ATS entries according to <access> and <allow-navigation> config.xml entries
+ var ats = writeATSEntries(platformConfig);
+ if (Object.keys(ats).length > 0) {
+ infoPlist['NSAppTransportSecurity'] = ats;
+ } else {
+ delete infoPlist['NSAppTransportSecurity'];
+ }
+
+ handleOrientationSettings(platformConfig, infoPlist);
+ updateProjectPlistForLaunchStoryboard(platformConfig, infoPlist);
+
+ var info_contents = plist.build(infoPlist);
+ info_contents = info_contents.replace(/<string>[\s\r\n]*<\/string>/g,'<string></string>');
+ fs.writeFileSync(plistFile, info_contents, 'utf-8');
+ events.emit('verbose', 'Wrote out iOS Bundle Identifier "' + pkg + '" and iOS Bundle Version "' + version + '" to ' + plistFile);
+
+ return handleBuildSettings(platformConfig, locations).then(function() {
+ if (name == originalName) {
+ events.emit('verbose', 'iOS Product Name has not changed (still "' + originalName + '")');
+ return Q();
+ } else { // CB-11712 <name> was changed, we don't support it'
+ var errorString =
+ 'The product name change (<name> tag) in config.xml is not supported dynamically.\n' +
+ 'To change your product name, you have to remove, then add your ios platform again.\n' +
+ 'Make sure you save your plugins beforehand using `cordova plugin save`.\n' +
+ '\tcordova plugin save\n' +
+ '\tcordova platform rm ios\n' +
+ '\tcordova platform add ios\n'
+ ;
+
+ return Q.reject(new CordovaError(errorString));
+ }
+ });
+}
+
+function handleOrientationSettings(platformConfig, infoPlist) {
+
+ switch (getOrientationValue(platformConfig)) {
+ case 'portrait':
+ infoPlist['UIInterfaceOrientation'] = [ 'UIInterfaceOrientationPortrait' ];
+ infoPlist['UISupportedInterfaceOrientations'] = [ 'UIInterfaceOrientationPortrait', 'UIInterfaceOrientationPortraitUpsideDown' ];
+ infoPlist['UISupportedInterfaceOrientations~ipad'] = [ 'UIInterfaceOrientationPortrait', 'UIInterfaceOrientationPortraitUpsideDown' ];
+ break;
+ case 'landscape':
+ infoPlist['UIInterfaceOrientation'] = [ 'UIInterfaceOrientationLandscapeLeft' ];
+ infoPlist['UISupportedInterfaceOrientations'] = [ 'UIInterfaceOrientationLandscapeLeft', 'UIInterfaceOrientationLandscapeRight' ];
+ infoPlist['UISupportedInterfaceOrientations~ipad'] = [ 'UIInterfaceOrientationLandscapeLeft', 'UIInterfaceOrientationLandscapeRight' ];
+ break;
+ case 'all':
+ infoPlist['UIInterfaceOrientation'] = [ 'UIInterfaceOrientationPortrait' ];
+ infoPlist['UISupportedInterfaceOrientations'] = [ 'UIInterfaceOrientationPortrait', 'UIInterfaceOrientationPortraitUpsideDown', 'UIInterfaceOrientationLandscapeLeft', 'UIInterfaceOrientationLandscapeRight' ];
+ infoPlist['UISupportedInterfaceOrientations~ipad'] = [ 'UIInterfaceOrientationPortrait', 'UIInterfaceOrientationPortraitUpsideDown', 'UIInterfaceOrientationLandscapeLeft', 'UIInterfaceOrientationLandscapeRight' ];
+ break;
+ case 'default':
+ infoPlist['UISupportedInterfaceOrientations'] = [ 'UIInterfaceOrientationPortrait', 'UIInterfaceOrientationLandscapeLeft', 'UIInterfaceOrientationLandscapeRight' ];
+ infoPlist['UISupportedInterfaceOrientations~ipad'] = [ 'UIInterfaceOrientationPortrait', 'UIInterfaceOrientationPortraitUpsideDown', 'UIInterfaceOrientationLandscapeLeft', 'UIInterfaceOrientationLandscapeRight' ];
+ delete infoPlist['UIInterfaceOrientation'];
+ }
+}
+
+function handleBuildSettings(platformConfig, locations) {
+ var targetDevice = parseTargetDevicePreference(platformConfig.getPreference('target-device', 'ios'));
+ var deploymentTarget = platformConfig.getPreference('deployment-target', 'ios');
+
+ // no build settings provided, we don't need to parse and update .pbxproj file
+ if (!targetDevice && !deploymentTarget) {
+ return Q();
+ }
+
+ var proj = new xcode.project(locations.pbxproj);
+
+ try {
+ proj.parseSync();
+ } catch (err) {
+ return Q.reject(new CordovaError('Could not parse project.pbxproj: ' + err));
+ }
+
+ if (targetDevice) {
+ events.emit('verbose', 'Set TARGETED_DEVICE_FAMILY to ' + targetDevice + '.');
+ proj.updateBuildProperty('TARGETED_DEVICE_FAMILY', targetDevice);
+ }
+
+ if (deploymentTarget) {
+ events.emit('verbose', 'Set IPHONEOS_DEPLOYMENT_TARGET to "' + deploymentTarget + '".');
+ proj.updateBuildProperty('IPHONEOS_DEPLOYMENT_TARGET', deploymentTarget);
+ }
+
+ fs.writeFileSync(locations.pbxproj, proj.writeSync(), 'utf-8');
+
+ return Q();
+}
+
+function mapIconResources(icons, iconsDir) {
+ // See https://developer.apple.com/library/ios/documentation/UserExperience/Conceptual/MobileHIG/IconMatrix.html
+ // for launch images sizes reference.
+ var platformIcons = [
+ {dest: 'icon-60@2x.png', width: 120, height: 120},
+ {dest: 'icon-60@3x.png', width: 180, height: 180},
+ {dest: 'icon-76.png', width: 76, height: 76},
+ {dest: 'icon-76@2x.png', width: 152, height: 152},
+ {dest: 'icon-small.png', width: 29, height: 29},
+ {dest: 'icon-small@2x.png', width: 58, height: 58},
+ {dest: 'icon-40.png', width: 40, height: 40},
+ {dest: 'icon-40@2x.png', width: 80, height: 80},
+ {dest: 'icon-small@3x.png', width: 87, height: 87},
+ {dest: 'icon.png', width: 57, height: 57},
+ {dest: 'icon@2x.png', width: 114, height: 114},
+ {dest: 'icon-72.png', width: 72, height: 72},
+ {dest: 'icon-72@2x.png', width: 144, height: 144},
+ {dest: 'icon-50.png', width: 50, height: 50},
+ {dest: 'icon-50@2x.png', width: 100, height: 100},
+ {dest: 'icon-83.5@2x.png', width: 167, height: 167}
+ ];
+
+ var pathMap = {};
+ platformIcons.forEach(function (item) {
+ var icon = icons.getBySize(item.width, item.height) || icons.getDefault();
+ if (icon) {
+ var target = path.join(iconsDir, item.dest);
+ pathMap[target] = icon.src;
+ }
+ });
+ return pathMap;
+}
+
+function getIconsDir(projectRoot, platformProjDir) {
+ var iconsDir;
+ var xcassetsExists = folderExists(path.join(projectRoot, platformProjDir, 'Images.xcassets/'));
+
+ if (xcassetsExists) {
+ iconsDir = path.join(platformProjDir, 'Images.xcassets/AppIcon.appiconset/');
+ } else {
+ iconsDir = path.join(platformProjDir, 'Resources/icons/');
+ }
+
+ return iconsDir;
+}
+
+function updateIcons(cordovaProject, locations) {
+ var icons = cordovaProject.projectConfig.getIcons('ios');
+
+ if (icons.length === 0) {
+ events.emit('verbose', 'This app does not have icons defined');
+ return;
+ }
+
+ var platformProjDir = path.relative(cordovaProject.root, locations.xcodeCordovaProj);
+ var iconsDir = getIconsDir(cordovaProject.root, platformProjDir);
+ var resourceMap = mapIconResources(icons, iconsDir);
+ events.emit('verbose', 'Updating icons at ' + iconsDir);
+ FileUpdater.updatePaths(
+ resourceMap, { rootDir: cordovaProject.root }, logFileOp);
+}
+
+function cleanIcons(projectRoot, projectConfig, locations) {
+ var icons = projectConfig.getIcons('ios');
+ if (icons.length > 0) {
+ var platformProjDir = path.relative(projectRoot, locations.xcodeCordovaProj);
+ var iconsDir = getIconsDir(projectRoot, platformProjDir);
+ var resourceMap = mapIconResources(icons, iconsDir);
+ Object.keys(resourceMap).forEach(function (targetIconPath) {
+ resourceMap[targetIconPath] = null;
+ });
+ events.emit('verbose', 'Cleaning icons at ' + iconsDir);
+
+ // Source paths are removed from the map, so updatePaths() will delete the target files.
+ FileUpdater.updatePaths(
+ resourceMap, { rootDir: projectRoot, all: true }, logFileOp);
+ }
+}
+
+function mapSplashScreenResources(splashScreens, splashScreensDir) {
+ var platformSplashScreens = [
+ {dest: 'Default~iphone.png', width: 320, height: 480},
+ {dest: 'Default@2x~iphone.png', width: 640, height: 960},
+ {dest: 'Default-Portrait~ipad.png', width: 768, height: 1024},
+ {dest: 'Default-Portrait@2x~ipad.png', width: 1536, height: 2048},
+ {dest: 'Default-Landscape~ipad.png', width: 1024, height: 768},
+ {dest: 'Default-Landscape@2x~ipad.png', width: 2048, height: 1536},
+ {dest: 'Default-568h@2x~iphone.png', width: 640, height: 1136},
+ {dest: 'Default-667h.png', width: 750, height: 1334},
+ {dest: 'Default-736h.png', width: 1242, height: 2208},
+ {dest: 'Default-Landscape-736h.png', width: 2208, height: 1242}
+ ];
+
+ var pathMap = {};
+ platformSplashScreens.forEach(function (item) {
+ var splash = splashScreens.getBySize(item.width, item.height);
+ if (splash) {
+ var target = path.join(splashScreensDir, item.dest);
+ pathMap[target] = splash.src;
+ }
+ });
+ return pathMap;
+}
+
+function getSplashScreensDir(projectRoot, platformProjDir) {
+ var splashScreensDir;
+ var xcassetsExists = folderExists(path.join(projectRoot, platformProjDir, 'Images.xcassets/'));
+
+ if (xcassetsExists) {
+ splashScreensDir = path.join(platformProjDir, 'Images.xcassets/LaunchImage.launchimage/');
+ } else {
+ splashScreensDir = path.join(platformProjDir, 'Resources/splash/');
+ }
+
+ return splashScreensDir;
+}
+
+function updateSplashScreens(cordovaProject, locations) {
+ var splashScreens = cordovaProject.projectConfig.getSplashScreens('ios');
+
+ if (splashScreens.length === 0) {
+ events.emit('verbose', 'This app does not have splash screens defined');
+ return;
+ }
+
+ var platformProjDir = path.relative(cordovaProject.root, locations.xcodeCordovaProj);
+ var splashScreensDir = getSplashScreensDir(cordovaProject.root, platformProjDir);
+ var resourceMap = mapSplashScreenResources(splashScreens, splashScreensDir);
+ events.emit('verbose', 'Updating splash screens at ' + splashScreensDir);
+ FileUpdater.updatePaths(
+ resourceMap, { rootDir: cordovaProject.root }, logFileOp);
+}
+
+function cleanSplashScreens(projectRoot, projectConfig, locations) {
+ var splashScreens = projectConfig.getSplashScreens('ios');
+ if (splashScreens.length > 0) {
+ var platformProjDir = path.relative(projectRoot, locations.xcodeCordovaProj);
+ var splashScreensDir = getSplashScreensDir(projectRoot, platformProjDir);
+ var resourceMap = mapIconResources(splashScreens, splashScreensDir);
+ Object.keys(resourceMap).forEach(function (targetSplashPath) {
+ resourceMap[targetSplashPath] = null;
+ });
+ events.emit('verbose', 'Cleaning splash screens at ' + splashScreensDir);
+
+ // Source paths are removed from the map, so updatePaths() will delete the target files.
+ FileUpdater.updatePaths(
+ resourceMap, { rootDir: projectRoot, all: true }, logFileOp);
+ }
+}
+
+/**
+ * Returns an array of images for each possible idiom, scale, and size class. The images themselves are
+ * located in the platform's splash images by their pattern (@scale~idiom~sizesize). All possible
+ * combinations are returned, but not all will have a `filename` property. If the latter isn't present,
+ * the device won't attempt to load an image matching the same traits. If the filename is present,
+ * the device will try to load the image if it corresponds to the traits.
+ *
+ * The resulting return looks like this:
+ *
+ * [
+ * {
+ * idiom: 'universal|ipad|iphone',
+ * scale: '1x|2x|3x',
+ * width: 'any|com',
+ * height: 'any|com',
+ * filename: undefined|'Default@scale~idiom~widthheight.png',
+ * src: undefined|'path/to/original/matched/image/from/splash/screens.png',
+ * target: undefined|'path/to/asset/library/Default@scale~idiom~widthheight.png'
+ * }, ...
+ * ]
+ *
+ * @param {Array<Object>} splashScreens splash screens as defined in config.xml for this platform
+ * @param {string} launchStoryboardImagesDir project-root/Images.xcassets/LaunchStoryboard.imageset/
+ * @return {Array<Object>}
+ */
+function mapLaunchStoryboardContents(splashScreens, launchStoryboardImagesDir) {
+ var platformLaunchStoryboardImages = [];
+ var idioms = ['universal', 'ipad', 'iphone'];
+ var scalesForIdiom = {
+ universal: ['1x', '2x', '3x'],
+ ipad: ['1x', '2x'],
+ iphone: ['1x', '2x', '3x']
+ };
+ var sizes = ['com', 'any'];
+
+ idioms.forEach(function (idiom) {
+ scalesForIdiom[idiom].forEach(function (scale) {
+ sizes.forEach(function(width) {
+ sizes.forEach(function(height) {
+ var item = {
+ idiom: idiom,
+ scale: scale,
+ width: width,
+ height: height
+ };
+
+ /* examples of the search pattern:
+ * scale ~ idiom ~ width height
+ * @2x ~ universal ~ any any
+ * @3x ~ iphone ~ com any
+ * @2x ~ ipad ~ com any
+ */
+ var searchPattern = '@' + scale + '~' + idiom + '~' + width + height;
+
+ /* because old node versions don't have Array.find, the below is
+ * functionally equivalent to this:
+ * var launchStoryboardImage = splashScreens.find(function(item) {
+ * return item.src.indexOf(searchPattern) >= 0;
+ * });
+ */
+ var launchStoryboardImage = splashScreens.reduce(function (p, c) {
+ return (c.src.indexOf(searchPattern) >= 0) ? c : p;
+ }, undefined);
+
+ if (launchStoryboardImage) {
+ item.filename = 'Default' + searchPattern + '.png';
+ item.src = launchStoryboardImage.src;
+ item.target = path.join(launchStoryboardImagesDir, item.filename);
+ }
+
+ platformLaunchStoryboardImages.push(item);
+ });
+ });
+ });
+ });
+ return platformLaunchStoryboardImages;
+}
+
+/**
+ * Returns a dictionary representing the source and destination paths for the launch storyboard images
+ * that need to be copied.
+ *
+ * The resulting return looks like this:
+ *
+ * {
+ * 'target-path': 'source-path',
+ * ...
+ * }
+ *
+ * @param {Array<Object>} splashScreens splash screens as defined in config.xml for this platform
+ * @param {string} launchStoryboardImagesDir project-root/Images.xcassets/LaunchStoryboard.imageset/
+ * @return {Object}
+ */
+function mapLaunchStoryboardResources(splashScreens, launchStoryboardImagesDir) {
+ var platformLaunchStoryboardImages = mapLaunchStoryboardContents(splashScreens, launchStoryboardImagesDir);
+ var pathMap = {};
+ platformLaunchStoryboardImages.forEach(function (item) {
+ if (item.target) {
+ pathMap[item.target] = item.src;
+ }
+ });
+ return pathMap;
+}
+
+/**
+ * Builds the object that represents the contents.json file for the LaunchStoryboard image set.
+ *
+ * The resulting return looks like this:
+ *
+ * {
+ * images: [
+ * {
+ * idiom: 'universal|ipad|iphone',
+ * scale: '1x|2x|3x',
+ * width-class: undefined|'compact',
+ * height-class: undefined|'compact'
+ * }, ...
+ * ],
+ * info: {
+ * author: 'Xcode',
+ * version: 1
+ * }
+ * }
+ *
+ * A bit of minor logic is used to map from the array of images returned from mapLaunchStoryboardContents
+ * to the format requried by Xcode.
+ *
+ * @param {Array<Object>} splashScreens splash screens as defined in config.xml for this platform
+ * @param {string} launchStoryboardImagesDir project-root/Images.xcassets/LaunchStoryboard.imageset/
+ * @return {Object}
+ */
+function getLaunchStoryboardContentsJSON(splashScreens, launchStoryboardImagesDir) {
+ var IMAGESET_COMPACT_SIZE_CLASS = 'compact';
+ var CDV_ANY_SIZE_CLASS = 'any';
+
+ var platformLaunchStoryboardImages = mapLaunchStoryboardContents(splashScreens, launchStoryboardImagesDir);
+ var contentsJSON = {
+ images: [],
+ info: {
+ author: 'Xcode',
+ version: 1
+ }
+ };
+ contentsJSON.images = platformLaunchStoryboardImages.map(function(item) {
+ var newItem = {
+ idiom: item.idiom,
+ scale: item.scale
+ };
+
+ // Xcode doesn't want any size class property if the class is "any"
+ // If our size class is "com", Xcode wants "compact".
+ if (item.width !== CDV_ANY_SIZE_CLASS) {
+ newItem['width-class'] = IMAGESET_COMPACT_SIZE_CLASS;
+ }
+ if (item.height !== CDV_ANY_SIZE_CLASS) {
+ newItem['height-class'] = IMAGESET_COMPACT_SIZE_CLASS;
+ }
+
+ // Xcode doesn't want a filename property if there's no image for these traits
+ if (item.filename) {
+ newItem.filename = item.filename;
+ }
+ return newItem;
+ });
+ return contentsJSON;
+}
+
+/**
+ * Updates the project's plist based upon our launch storyboard images. If there are no images, then we should
+ * fall back to the regular launch images that might be supplied (that is, our app will be scaled on an iPad Pro),
+ * and if there are some images, we need to alter the UILaunchStoryboardName property to point to
+ * CDVLaunchScreen.
+ *
+ * There's some logic here to avoid overwriting changes the user might have made to their plist if they are using
+ * their own launch storyboard.
+ */
+function updateProjectPlistForLaunchStoryboard(platformConfig, infoPlist) {
+ var UI_LAUNCH_STORYBOARD_NAME = 'UILaunchStoryboardName';
+ var CDV_LAUNCH_STORYBOARD_NAME = 'CDVLaunchScreen';
+
+ var splashScreens = platformConfig.getSplashScreens('ios');
+ var contentsJSON = getLaunchStoryboardContentsJSON(splashScreens, ''); // note: we don't need a file path here; we're just counting
+ var currentLaunchStoryboard = infoPlist[UI_LAUNCH_STORYBOARD_NAME];
+
+ events.emit('verbose', 'Current launch storyboard ' + currentLaunchStoryboard);
+
+
+ /* do we have any launch images do we have for our launch storyboard?
+ * Again, for old Node versions, the below code is equivalent to this:
+ * var hasLaunchStoryboardImages = !!contentsJSON.images.find(function (item) {
+ * return item.filename !== undefined;
+ * });
+ */
+ var hasLaunchStoryboardImages = !!contentsJSON.images.reduce(function (p, c) {
+ return (c.filename !== undefined) ? c : p;
+ }, undefined);
+
+ if (hasLaunchStoryboardImages && !currentLaunchStoryboard) {
+ // only change the launch storyboard if we have images to use AND the current value is blank
+ // if it's not blank, we've either done this before, or the user has their own launch storyboard
+ events.emit('verbose', 'Changing project to use our launch storyboard');
+ infoPlist[UI_LAUNCH_STORYBOARD_NAME] = CDV_LAUNCH_STORYBOARD_NAME;
+ return;
+ }
+
+ if (!hasLaunchStoryboardImages && currentLaunchStoryboard === CDV_LAUNCH_STORYBOARD_NAME) {
+ // only revert to using the launch images if we have don't have any images for the launch storyboard
+ // but only clear it if current launch storyboard is our storyboard; the user might be using their
+ // own storyboard instead.
+ events.emit('verbose', 'Changing project to use launch images');
+ infoPlist[UI_LAUNCH_STORYBOARD_NAME] = undefined;
+ return;
+ }
+ events.emit('verbose', 'Not changing launch storyboard setting.');
+}
+
+/**
+ * Returns the directory for the Launch Storyboard image set, if image sets are being used. If they aren't
+ * being used, returns null.
+ *
+ * @param {string} projectRoot The project's root directory
+ * @param {string} platformProjDir The platform's project directory
+ */
+function getLaunchStoryboardImagesDir(projectRoot, platformProjDir) {
+ var launchStoryboardImagesDir;
+ var xcassetsExists = folderExists(path.join(projectRoot, platformProjDir, 'Images.xcassets/'));
+
+ if (xcassetsExists) {
+ launchStoryboardImagesDir = path.join(platformProjDir, 'Images.xcassets/LaunchStoryboard.imageset/');
+ } else {
+ // if we don't have a asset library for images, we can't do the storyboard.
+ launchStoryboardImagesDir = null;
+ }
+
+ return launchStoryboardImagesDir;
+}
+
+/**
+ * Update the images for the Launch Storyboard and updates the image set's contents.json file appropriately.
+ *
+ * @param {Object} cordovaProject The cordova project
+ * @param {Object} locations A dictionary containing useful location paths
+ */
+function updateLaunchStoryboardImages(cordovaProject, locations) {
+ var splashScreens = cordovaProject.projectConfig.getSplashScreens('ios');
+ var platformProjDir = locations.xcodeCordovaProj;
+ var launchStoryboardImagesDir = getLaunchStoryboardImagesDir(cordovaProject.root, platformProjDir);
+
+ if (launchStoryboardImagesDir) {
+ var resourceMap = mapLaunchStoryboardResources(splashScreens, launchStoryboardImagesDir);
+ var contentsJSON = getLaunchStoryboardContentsJSON(splashScreens, launchStoryboardImagesDir);
+
+ events.emit('verbose', 'Updating launch storyboard images at ' + launchStoryboardImagesDir);
+ FileUpdater.updatePaths(
+ resourceMap, { rootDir: cordovaProject.root }, logFileOp);
+
+ events.emit('verbose', 'Updating Storyboard image set contents.json');
+ fs.writeFileSync(path.join(launchStoryboardImagesDir, 'contents.json'),
+ JSON.stringify(contentsJSON, null, 2));
+ }
+}
+
+/**
+ * Removes the images from the launch storyboard's image set and updates the image set's contents.json
+ * file appropriately.
+ *
+ * @param {string} projectRoot Path to the project root
+ * @param {Object} projectConfig The project's config.xml
+ * @param {Object} locations A dictionary containing useful location paths
+ */
+function cleanLaunchStoryboardImages(projectRoot, projectConfig, locations) {
+ var splashScreens = projectConfig.getSplashScreens('ios');
+ var platformProjDir = locations.xcodeCordovaProj;
+ var launchStoryboardImagesDir = getLaunchStoryboardImagesDir(projectRoot, platformProjDir);
+ if (launchStoryboardImagesDir) {
+ var resourceMap = mapLaunchStoryboardResources(splashScreens, launchStoryboardImagesDir);
+ var contentsJSON = getLaunchStoryboardContentsJSON(splashScreens, launchStoryboardImagesDir);
+
+ Object.keys(resourceMap).forEach(function (targetPath) {
+ resourceMap[targetPath] = null;
+ });
+ events.emit('verbose', 'Cleaning storyboard image set at ' + launchStoryboardImagesDir);
+
+ // Source paths are removed from the map, so updatePaths() will delete the target files.
+ FileUpdater.updatePaths(
+ resourceMap, { rootDir: projectRoot, all: true }, logFileOp);
+
+ // delete filename from contents.json
+ contentsJSON.images.forEach(function(image) {
+ image.filename = undefined;
+ });
+
+ events.emit('verbose', 'Updating Storyboard image set contents.json');
+ fs.writeFileSync(path.join(launchStoryboardImagesDir, 'contents.json'),
+ JSON.stringify(contentsJSON, null, 2));
+ }
+}
+
+/**
+ * Queries ConfigParser object for the orientation <preference> value. Warns if
+ * global preference value is not supported by platform.
+ *
+ * @param {Object} platformConfig ConfigParser object
+ *
+ * @return {String} Global/platform-specific orientation in lower-case
+ * (or empty string if both are undefined).
+ */
+function getOrientationValue(platformConfig) {
+
+ var ORIENTATION_DEFAULT = 'default';
+
+ var orientation = platformConfig.getPreference('orientation');
+ if (!orientation) {
+ return '';
+ }
+
+ orientation = orientation.toLowerCase();
+
+ // Check if the given global orientation is supported
+ if (['default', 'portrait','landscape', 'all'].indexOf(orientation) >= 0) {
+ return orientation;
+ }
+
+ events.emit('warn', 'Unrecognized value for Orientation preference: ' + orientation +
+ '. Defaulting to value: ' + ORIENTATION_DEFAULT + '.');
+
+ return ORIENTATION_DEFAULT;
+}
+
+/*
+ Parses all <access> and <allow-navigation> entries and consolidates duplicates (for ATS).
+ Returns an object with a Hostname as the key, and the value an object with properties:
+ {
+ Hostname, // String
+ NSExceptionAllowsInsecureHTTPLoads, // boolean
+ NSIncludesSubdomains, // boolean
+ NSExceptionMinimumTLSVersion, // String
+ NSExceptionRequiresForwardSecrecy, // boolean
+ NSRequiresCertificateTransparency, // boolean
+
+ // the three below _only_ show when the Hostname is '*'
+ // if any of the three are set, it disables setting NSAllowsArbitraryLoads
+ // (Apple already enforces this in ATS)
+ NSAllowsArbitraryLoadsInWebContent, // boolean (default: false)
+ NSAllowsLocalNetworking, // boolean (default: false)
+ NSAllowsArbitraryLoadsInMedia, // boolean (default:false)
+ }
+*/
+function processAccessAndAllowNavigationEntries(config) {
+ var accesses = config.getAccesses();
+ var allow_navigations = config.getAllowNavigations();
+
+ return allow_navigations
+ // we concat allow_navigations and accesses, after processing accesses
+ .concat(accesses.map(function(obj) {
+ // map accesses to a common key interface using 'href', not origin
+ obj.href = obj.origin;
+ delete obj.origin;
+ return obj;
+ }))
+ // we reduce the array to an object with all the entries processed (key is Hostname)
+ .reduce(function(previousReturn, currentElement) {
+ var options = {
+ minimum_tls_version : currentElement.minimum_tls_version,
+ requires_forward_secrecy : currentElement.requires_forward_secrecy,
+ requires_certificate_transparency : currentElement.requires_certificate_transparency,
+ allows_arbitrary_loads_in_media : currentElement.allows_arbitrary_loads_in_media,
+ allows_arbitrary_loads_in_web_content : currentElement.allows_arbitrary_loads_in_web_content,
+ allows_local_networking : currentElement.allows_local_networking
+ };
+ var obj = parseWhitelistUrlForATS(currentElement.href, options);
+
+ if (obj) {
+ // we 'union' duplicate entries
+ var item = previousReturn[obj.Hostname];
+ if (!item) {
+ item = {};
+ }
+ for(var o in obj) {
+ if (obj.hasOwnProperty(o)) {
+ item[o] = obj[o];
+ }
+ }
+ previousReturn[obj.Hostname] = item;
+ }
+ return previousReturn;
+ }, {});
+}
+
+/*
+ Parses a URL and returns an object with these keys:
+ {
+ Hostname, // String
+ NSExceptionAllowsInsecureHTTPLoads, // boolean (default: false)
+ NSIncludesSubdomains, // boolean (default: false)
+ NSExceptionMinimumTLSVersion, // String (default: 'TLSv1.2')
+ NSExceptionRequiresForwardSecrecy, // boolean (default: true)
+ NSRequiresCertificateTransparency, // boolean (default: false)
+
+ // the three below _only_ apply when the Hostname is '*'
+ // if any of the three are set, it disables setting NSAllowsArbitraryLoads
+ // (Apple already enforces this in ATS)
+ NSAllowsArbitraryLoadsInWebContent, // boolean (default: false)
+ NSAllowsLocalNetworking, // boolean (default: false)
+ NSAllowsArbitraryLoadsInMedia, // boolean (default:false)
+ }
+
+ null is returned if the URL cannot be parsed, or is to be skipped for ATS.
+*/
+function parseWhitelistUrlForATS(url, options) {
+ var href = URL.parse(url);
+ var retObj = {};
+ retObj.Hostname = href.hostname;
+
+ // Guiding principle: we only set values in retObj if they are NOT the default
+
+ if (url === '*') {
+ retObj.Hostname = '*';
+ var val;
+
+ val = (options.allows_arbitrary_loads_in_web_content === 'true');
+ if (options.allows_arbitrary_loads_in_web_content && val) { // default is false
+ retObj.NSAllowsArbitraryLoadsInWebContent = true;
+ }
+
+ val = (options.allows_arbitrary_loads_in_media === 'true');
+ if (options.allows_arbitrary_loads_in_media && val) { // default is false
+ retObj.NSAllowsArbitraryLoadsInMedia = true;
+ }
+
+ val = (options.allows_local_networking === 'true');
+ if (options.allows_local_networking && val) { // default is false
+ retObj.NSAllowsLocalNetworking = true;
+ }
+
+ return retObj;
+ }
+
+ if (!retObj.Hostname) {
+ // check origin, if it allows subdomains (wildcard in hostname), we set NSIncludesSubdomains to YES. Default is NO
+ var subdomain1 = '/*.'; // wildcard in hostname
+ var subdomain2 = '*://*.'; // wildcard in hostname and protocol
+ var subdomain3 = '*://'; // wildcard in protocol only
+ if (href.pathname.indexOf(subdomain1) === 0) {
+ retObj.NSIncludesSubdomains = true;
+ retObj.Hostname = href.pathname.substring(subdomain1.length);
+ } else if (href.pathname.indexOf(subdomain2) === 0) {
+ retObj.NSIncludesSubdomains = true;
+ retObj.Hostname = href.pathname.substring(subdomain2.length);
+ } else if (href.pathname.indexOf(subdomain3) === 0) {
+ retObj.Hostname = href.pathname.substring(subdomain3.length);
+ } else {
+ // Handling "scheme:*" case to avoid creating of a blank key in NSExceptionDomains.
+ return null;
+ }
+ }
+
+ if (options.minimum_tls_version && options.minimum_tls_version !== 'TLSv1.2') { // default is TLSv1.2
+ retObj.NSExceptionMinimumTLSVersion = options.minimum_tls_version;
+ }
+
+ var rfs = (options.requires_forward_secrecy === 'true');
+ if (options.requires_forward_secrecy && !rfs) { // default is true
+ retObj.NSExceptionRequiresForwardSecrecy = false;
+ }
+
+ var rct = (options.requires_certificate_transparency === 'true');
+ if (options.requires_certificate_transparency && rct) { // default is false
+ retObj.NSRequiresCertificateTransparency = true;
+ }
+
+ // if the scheme is HTTP, we set NSExceptionAllowsInsecureHTTPLoads to YES. Default is NO
+ if (href.protocol === 'http:') {
+ retObj.NSExceptionAllowsInsecureHTTPLoads = true;
+ }
+ else if (!href.protocol && href.pathname.indexOf('*:/') === 0) { // wilcard in protocol
+ retObj.NSExceptionAllowsInsecureHTTPLoads = true;
+ }
+
+ return retObj;
+}
+
+
+/*
+ App Transport Security (ATS) writer from <access> and <allow-navigation> tags
+ in config.xml
+*/
+function writeATSEntries(config) {
+ var pObj = processAccessAndAllowNavigationEntries(config);
+
+ var ats = {};
+
+ for(var hostname in pObj) {
+ if (pObj.hasOwnProperty(hostname)) {
+ var entry = pObj[hostname];
+
+ // Guiding principle: we only set values if they are available
+
+ if (hostname === '*') {
+ // always write this, for iOS 9, since in iOS 10 it will be overriden if
+ // any of the other three keys are written
+ ats['NSAllowsArbitraryLoads'] = true;
+
+ // at least one of the overriding keys is present
+ if (entry.NSAllowsArbitraryLoadsInWebContent) {
+ ats['NSAllowsArbitraryLoadsInWebContent'] = true;
+ }
+ if (entry.NSAllowsArbitraryLoadsInMedia) {
+ ats['NSAllowsArbitraryLoadsInMedia'] = true;
+ }
+ if (entry.NSAllowsLocalNetworking) {
+ ats['NSAllowsLocalNetworking'] = true;
+ }
+
+ continue;
+ }
+
+ var exceptionDomain = {};
+
+ for(var key in entry) {
+ if (entry.hasOwnProperty(key) && key !== 'Hostname') {
+ exceptionDomain[key] = entry[key];
+ }
+ }
+
+ if (!ats['NSExceptionDomains']) {
+ ats['NSExceptionDomains'] = {};
+ }
+
+ ats['NSExceptionDomains'][hostname] = exceptionDomain;
+ }
+ }
+
+ return ats;
+}
+
+function folderExists(folderPath) {
+ try {
+ var stat = fs.statSync(folderPath);
+ return stat && stat.isDirectory();
+ } catch (e) {
+ return false;
+ }
+}
+
+// Construct a default value for CFBundleVersion as the version with any
+// -rclabel stripped=.
+function default_CFBundleVersion(version) {
+ return version.split('-')[0];
+}
+
+// Converts cordova specific representation of target device to XCode value
+function parseTargetDevicePreference(value) {
+ if (!value) return null;
+ var map = { 'universal': '"1,2"', 'handset': '"1"', 'tablet': '"2"'};
+ if (map[value.toLowerCase()]) {
+ return map[value.toLowerCase()];
+ }
+ events.emit('warn', 'Unrecognized value for target-device preference: ' + value + '.');
+ return null;
+}