1173 lines
49 KiB
JavaScript
Executable File
1173 lines
49 KiB
JavaScript
Executable File
/**
|
|
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.
|
|
*/
|
|
|
|
'use strict';
|
|
var Q = require('q');
|
|
var fs = require('fs');
|
|
var path = require('path');
|
|
var shell = require('shelljs');
|
|
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;
|
|
var projectFile = require('./projectFile');
|
|
|
|
// launch storyboard and related constants
|
|
var LAUNCHIMAGE_BUILD_SETTING = 'ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME';
|
|
var LAUNCHIMAGE_BUILD_SETTING_VALUE = 'LaunchImage';
|
|
var UI_LAUNCH_STORYBOARD_NAME = 'UILaunchStoryboardName';
|
|
var CDV_LAUNCH_STORYBOARD_NAME = 'CDVLaunchScreen';
|
|
var IMAGESET_COMPACT_SIZE_CLASS = 'compact';
|
|
var CDV_ANY_SIZE_CLASS = 'any';
|
|
|
|
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);
|
|
updateFileResources(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);
|
|
cleanFileResources(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 version = platformConfig.version();
|
|
var displayName = platformConfig.shortName && platformConfig.shortName();
|
|
|
|
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'));
|
|
|
|
// 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');
|
|
}
|
|
|
|
if (displayName) {
|
|
infoPlist['CFBundleDisplayName'] = displayName;
|
|
}
|
|
|
|
// 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);
|
|
|
|
/* eslint-disable no-tabs */
|
|
// Write out the plist file with the same formatting as Xcode does
|
|
var info_contents = plist.build(infoPlist, { indent: ' ', offset: -1 });
|
|
/* eslint-enable no-tabs */
|
|
|
|
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 Version "' + version + '" to ' + plistFile);
|
|
|
|
return handleBuildSettings(platformConfig, locations, infoPlist).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, infoPlist) {
|
|
var pkg = platformConfig.getAttribute('ios-CFBundleIdentifier') || platformConfig.packageName();
|
|
var targetDevice = parseTargetDevicePreference(platformConfig.getPreference('target-device', 'ios'));
|
|
var deploymentTarget = platformConfig.getPreference('deployment-target', 'ios');
|
|
var needUpdatedBuildSettingsForLaunchStoryboard = checkIfBuildSettingsNeedUpdatedForLaunchStoryboard(platformConfig, infoPlist);
|
|
var swiftVersion = platformConfig.getPreference('SwiftVersion', 'ios');
|
|
|
|
var project;
|
|
|
|
try {
|
|
project = projectFile.parse(locations);
|
|
} catch (err) {
|
|
return Q.reject(new CordovaError('Could not parse ' + locations.pbxproj + ': ' + err));
|
|
}
|
|
|
|
var origPkg = project.xcode.getBuildProperty('PRODUCT_BUNDLE_IDENTIFIER');
|
|
|
|
// no build settings provided and we don't need to update build settings for launch storyboards,
|
|
// then we don't need to parse and update .pbxproj file
|
|
if (origPkg === pkg && !targetDevice && !deploymentTarget && !needUpdatedBuildSettingsForLaunchStoryboard && !swiftVersion) {
|
|
return Q();
|
|
}
|
|
|
|
if (origPkg !== pkg) {
|
|
events.emit('verbose', 'Set PRODUCT_BUNDLE_IDENTIFIER to ' + pkg + '.');
|
|
project.xcode.updateBuildProperty('PRODUCT_BUNDLE_IDENTIFIER', pkg);
|
|
}
|
|
|
|
if (targetDevice) {
|
|
events.emit('verbose', 'Set TARGETED_DEVICE_FAMILY to ' + targetDevice + '.');
|
|
project.xcode.updateBuildProperty('TARGETED_DEVICE_FAMILY', targetDevice);
|
|
}
|
|
|
|
if (deploymentTarget) {
|
|
events.emit('verbose', 'Set IPHONEOS_DEPLOYMENT_TARGET to "' + deploymentTarget + '".');
|
|
project.xcode.updateBuildProperty('IPHONEOS_DEPLOYMENT_TARGET', deploymentTarget);
|
|
}
|
|
|
|
if (swiftVersion) {
|
|
events.emit('verbose', 'Set SwiftVersion to "' + swiftVersion + '".');
|
|
project.xcode.updateBuildProperty('SWIFT_VERSION', swiftVersion);
|
|
}
|
|
|
|
updateBuildSettingsForLaunchStoryboard(project.xcode, platformConfig, infoPlist);
|
|
|
|
project.write();
|
|
|
|
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-20.png', width: 20, height: 20 },
|
|
{ dest: 'icon-20@2x.png', width: 40, height: 40 },
|
|
{ dest: 'icon-20@3x.png', width: 60, height: 60 },
|
|
{ dest: 'icon-40.png', width: 40, height: 40 },
|
|
{ dest: 'icon-40@2x.png', width: 80, height: 80 },
|
|
{ dest: 'icon-50.png', width: 50, height: 50 },
|
|
{ dest: 'icon-50@2x.png', width: 100, height: 100 },
|
|
{ dest: 'icon-60@2x.png', width: 120, height: 120 },
|
|
{ dest: 'icon-60@3x.png', width: 180, height: 180 },
|
|
{ dest: 'icon-72.png', width: 72, height: 72 },
|
|
{ dest: 'icon-72@2x.png', width: 144, height: 144 },
|
|
{ dest: 'icon-76.png', width: 76, height: 76 },
|
|
{ dest: 'icon-76@2x.png', width: 152, height: 152 },
|
|
{ dest: 'icon-83.5@2x.png', width: 167, height: 167 },
|
|
{ dest: 'icon-1024.png', width: 1024, height: 1024 },
|
|
{ dest: 'icon-29.png', width: 29, height: 29 },
|
|
{ dest: 'icon-29@2x.png', width: 58, height: 58 },
|
|
{ dest: 'icon-29@3x.png', width: 87, height: 87 },
|
|
{ dest: 'icon.png', width: 57, height: 57 },
|
|
{ dest: 'icon@2x.png', width: 114, height: 114 },
|
|
{ dest: 'icon-24@2x.png', width: 48, height: 48 },
|
|
{ dest: 'icon-27.5@2x.png', width: 55, height: 55 },
|
|
{ dest: 'icon-44@2x.png', width: 88, height: 88 },
|
|
{ dest: 'icon-86@2x.png', width: 172, height: 172 },
|
|
{ dest: 'icon-98@2x.png', width: 196, height: 196 }
|
|
];
|
|
|
|
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 },
|
|
{ dest: 'Default-2436h.png', width: 1125, height: 2436 },
|
|
{ dest: 'Default-Landscape-2436h.png', width: 2436, height: 1125 }
|
|
];
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
function updateFileResources (cordovaProject, locations) {
|
|
const platformDir = path.relative(cordovaProject.root, locations.root);
|
|
const files = cordovaProject.projectConfig.getFileResources('ios');
|
|
|
|
const project = projectFile.parse(locations);
|
|
|
|
// if there are resource-file elements in config.xml
|
|
if (files.length === 0) {
|
|
events.emit('verbose', 'This app does not have additional resource files defined');
|
|
return;
|
|
}
|
|
|
|
let resourceMap = {};
|
|
files.forEach(function (res) {
|
|
let src = res.src;
|
|
let target = res.target;
|
|
|
|
if (!target) {
|
|
target = src;
|
|
}
|
|
|
|
let targetPath = path.join(project.resources_dir, target);
|
|
targetPath = path.relative(cordovaProject.root, targetPath);
|
|
|
|
if (!fs.existsSync(targetPath)) {
|
|
project.xcode.addResourceFile(target);
|
|
} else {
|
|
events.emit('warn', 'Overwriting existing resource file at ' + targetPath);
|
|
}
|
|
|
|
resourceMap[targetPath] = src;
|
|
});
|
|
|
|
events.emit('verbose', 'Updating resource files at ' + platformDir);
|
|
FileUpdater.updatePaths(
|
|
resourceMap, { rootDir: cordovaProject.root }, logFileOp);
|
|
|
|
project.write();
|
|
}
|
|
|
|
function cleanFileResources (projectRoot, projectConfig, locations) {
|
|
const platformDir = path.relative(projectRoot, locations.root);
|
|
const files = projectConfig.getFileResources('ios', true);
|
|
if (files.length > 0) {
|
|
events.emit('verbose', 'Cleaning resource files at ' + platformDir);
|
|
|
|
const project = projectFile.parse(locations);
|
|
|
|
var resourceMap = {};
|
|
files.forEach(function (res) {
|
|
let src = res.src;
|
|
let target = res.target;
|
|
|
|
if (!target) {
|
|
target = src;
|
|
}
|
|
|
|
let targetPath = path.join(project.resources_dir, target);
|
|
targetPath = path.relative(projectRoot, targetPath);
|
|
const resfile = path.join('Resources', path.basename(targetPath));
|
|
project.xcode.removeResourceFile(resfile);
|
|
|
|
resourceMap[targetPath] = null;
|
|
});
|
|
|
|
FileUpdater.updatePaths(
|
|
resourceMap, { rootDir: projectRoot, all: true }, logFileOp);
|
|
|
|
project.write();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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 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;
|
|
}
|
|
|
|
/**
|
|
* Determines if the project's build settings may need to be updated for launch storyboard support
|
|
*
|
|
*/
|
|
function checkIfBuildSettingsNeedUpdatedForLaunchStoryboard (platformConfig, infoPlist) {
|
|
var hasLaunchStoryboardImages = platformHasLaunchStoryboardImages(platformConfig);
|
|
var hasLegacyLaunchImages = platformHasLegacyLaunchImages(platformConfig);
|
|
var currentLaunchStoryboard = infoPlist[UI_LAUNCH_STORYBOARD_NAME];
|
|
|
|
if (hasLaunchStoryboardImages && currentLaunchStoryboard === CDV_LAUNCH_STORYBOARD_NAME && !hasLegacyLaunchImages) {
|
|
// don't need legacy launch images if we are using our launch storyboard
|
|
// so we do need to update the project file
|
|
events.emit('verbose', 'Need to update build settings because project is using our launch storyboard.');
|
|
return true;
|
|
} else if (hasLegacyLaunchImages && !currentLaunchStoryboard) {
|
|
// we do need to ensure legacy launch images are used if there's no launch storyboard present
|
|
// so we do need to update the project file
|
|
events.emit('verbose', 'Need to update build settings because project is using legacy launch images and no storyboard.');
|
|
return true;
|
|
}
|
|
events.emit('verbose', 'No need to update build settings for launch storyboard support.');
|
|
return false;
|
|
}
|
|
|
|
function updateBuildSettingsForLaunchStoryboard (proj, platformConfig, infoPlist) {
|
|
var hasLaunchStoryboardImages = platformHasLaunchStoryboardImages(platformConfig);
|
|
var hasLegacyLaunchImages = platformHasLegacyLaunchImages(platformConfig);
|
|
var currentLaunchStoryboard = infoPlist[UI_LAUNCH_STORYBOARD_NAME];
|
|
|
|
if (hasLaunchStoryboardImages && currentLaunchStoryboard === CDV_LAUNCH_STORYBOARD_NAME && !hasLegacyLaunchImages) {
|
|
// don't need legacy launch images if we are using our launch storyboard
|
|
events.emit('verbose', 'Removed ' + LAUNCHIMAGE_BUILD_SETTING + ' because project is using our launch storyboard.');
|
|
proj.removeBuildProperty(LAUNCHIMAGE_BUILD_SETTING);
|
|
} else if (hasLegacyLaunchImages && !currentLaunchStoryboard) {
|
|
// we do need to ensure legacy launch images are used if there's no launch storyboard present
|
|
events.emit('verbose', 'Set ' + LAUNCHIMAGE_BUILD_SETTING + ' to ' + LAUNCHIMAGE_BUILD_SETTING_VALUE + ' because project is using legacy launch images and no storyboard.');
|
|
proj.updateBuildProperty(LAUNCHIMAGE_BUILD_SETTING, LAUNCHIMAGE_BUILD_SETTING_VALUE);
|
|
} else {
|
|
events.emit('verbose', 'Did not update build settings for launch storyboard support.');
|
|
}
|
|
}
|
|
|
|
function splashScreensHaveLaunchStoryboardImages (contentsJSON) {
|
|
/* 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:
|
|
* return !!contentsJSON.images.find(function (item) {
|
|
* return item.filename !== undefined;
|
|
* });
|
|
*/
|
|
return !!contentsJSON.images.reduce(function (p, c) {
|
|
return (c.filename !== undefined) ? c : p;
|
|
}, undefined);
|
|
}
|
|
|
|
function platformHasLaunchStoryboardImages (platformConfig) {
|
|
var splashScreens = platformConfig.getSplashScreens('ios');
|
|
var contentsJSON = getLaunchStoryboardContentsJSON(splashScreens, ''); // note: we don't need a file path here; we're just counting
|
|
return splashScreensHaveLaunchStoryboardImages(contentsJSON);
|
|
}
|
|
|
|
function platformHasLegacyLaunchImages (platformConfig) {
|
|
var splashScreens = platformConfig.getSplashScreens('ios');
|
|
return !!splashScreens.reduce(function (p, c) {
|
|
return (c.width !== undefined || c.height !== undefined) ? c : p;
|
|
}, undefined);
|
|
}
|
|
|
|
/**
|
|
* 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 currentLaunchStoryboard = infoPlist[UI_LAUNCH_STORYBOARD_NAME];
|
|
events.emit('verbose', 'Current launch storyboard ' + currentLaunchStoryboard);
|
|
|
|
var hasLaunchStoryboardImages = platformHasLaunchStoryboardImages(platformConfig);
|
|
|
|
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 info plist 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 info plist to use legacy launch images');
|
|
delete infoPlist[UI_LAUNCH_STORYBOARD_NAME];
|
|
return;
|
|
}
|
|
events.emit('verbose', 'Not changing launch storyboard setting in info plist.');
|
|
}
|
|
|
|
/**
|
|
* 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 = path.relative(cordovaProject.root, 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(cordovaProject.root, 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 = path.relative(projectRoot, 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(projectRoot, 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)
|
|
NSAllowsArbitraryLoadsForMedia, // 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_for_media: currentElement.allows_arbitrary_loads_in_media || currentElement.allows_arbitrary_loads_for_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)
|
|
NSAllowsArbitraryLoadsForMedia, // boolean (default:false)
|
|
}
|
|
|
|
null is returned if the URL cannot be parsed, or is to be skipped for ATS.
|
|
*/
|
|
function parseWhitelistUrlForATS (url, options) {
|
|
// @todo 'url.parse' was deprecated since v11.0.0. Use 'url.URL' constructor instead.
|
|
var href = URL.parse(url); // eslint-disable-line
|
|
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_for_media === 'true');
|
|
if (options.allows_arbitrary_loads_for_media && val) { // default is false
|
|
retObj.NSAllowsArbitraryLoadsForMedia = 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) {
|
|
return null;
|
|
} else 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.NSAllowsArbitraryLoadsForMedia) {
|
|
ats['NSAllowsArbitraryLoadsForMedia'] = 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;
|
|
}
|