prepare.js 48 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153
  1. /**
  2. Licensed to the Apache Software Foundation (ASF) under one
  3. or more contributor license agreements. See the NOTICE file
  4. distributed with this work for additional information
  5. regarding copyright ownership. The ASF licenses this file
  6. to you under the Apache License, Version 2.0 (the
  7. "License"); you may not use this file except in compliance
  8. with the License. You may obtain a copy of the License at
  9. http://www.apache.org/licenses/LICENSE-2.0
  10. Unless required by applicable law or agreed to in writing,
  11. software distributed under the License is distributed on an
  12. "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
  13. KIND, either express or implied. See the License for the
  14. specific language governing permissions and limitations
  15. under the License.
  16. */
  17. 'use strict';
  18. var Q = require('q');
  19. var fs = require('fs');
  20. var path = require('path');
  21. var shell = require('shelljs');
  22. var xcode = require('xcode');
  23. var unorm = require('unorm');
  24. var plist = require('plist');
  25. var URL = require('url');
  26. var events = require('cordova-common').events;
  27. var xmlHelpers = require('cordova-common').xmlHelpers;
  28. var ConfigParser = require('cordova-common').ConfigParser;
  29. var CordovaError = require('cordova-common').CordovaError;
  30. var PlatformJson = require('cordova-common').PlatformJson;
  31. var PlatformMunger = require('cordova-common').ConfigChanges.PlatformMunger;
  32. var PluginInfoProvider = require('cordova-common').PluginInfoProvider;
  33. var FileUpdater = require('cordova-common').FileUpdater;
  34. var projectFile = require('./projectFile');
  35. // launch storyboard and related constants
  36. var LAUNCHIMAGE_BUILD_SETTING = 'ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME';
  37. var LAUNCHIMAGE_BUILD_SETTING_VALUE = 'LaunchImage';
  38. var UI_LAUNCH_STORYBOARD_NAME = 'UILaunchStoryboardName';
  39. var CDV_LAUNCH_STORYBOARD_NAME = 'CDVLaunchScreen';
  40. var IMAGESET_COMPACT_SIZE_CLASS = 'compact';
  41. var CDV_ANY_SIZE_CLASS = 'any';
  42. module.exports.prepare = function (cordovaProject, options) {
  43. var self = this;
  44. var platformJson = PlatformJson.load(this.locations.root, 'ios');
  45. var munger = new PlatformMunger('ios', this.locations.root, platformJson, new PluginInfoProvider());
  46. this._config = updateConfigFile(cordovaProject.projectConfig, munger, this.locations);
  47. // Update own www dir with project's www assets and plugins' assets and js-files
  48. return Q.when(updateWww(cordovaProject, this.locations))
  49. .then(function () {
  50. // update project according to config.xml changes.
  51. return updateProject(self._config, self.locations);
  52. })
  53. .then(function () {
  54. updateIcons(cordovaProject, self.locations);
  55. updateSplashScreens(cordovaProject, self.locations);
  56. updateLaunchStoryboardImages(cordovaProject, self.locations);
  57. updateFileResources(cordovaProject, self.locations);
  58. })
  59. .then(function () {
  60. events.emit('verbose', 'Prepared iOS project successfully');
  61. });
  62. };
  63. module.exports.clean = function (options) {
  64. // A cordovaProject isn't passed into the clean() function, because it might have
  65. // been called from the platform shell script rather than the CLI. Check for the
  66. // noPrepare option passed in by the non-CLI clean script. If that's present, or if
  67. // there's no config.xml found at the project root, then don't clean prepared files.
  68. var projectRoot = path.resolve(this.root, '../..');
  69. var projectConfigFile = path.join(projectRoot, 'config.xml');
  70. if ((options && options.noPrepare) || !fs.existsSync(projectConfigFile) ||
  71. !fs.existsSync(this.locations.configXml)) {
  72. return Q();
  73. }
  74. var projectConfig = new ConfigParser(this.locations.configXml);
  75. var self = this;
  76. return Q().then(function () {
  77. cleanWww(projectRoot, self.locations);
  78. cleanIcons(projectRoot, projectConfig, self.locations);
  79. cleanSplashScreens(projectRoot, projectConfig, self.locations);
  80. cleanLaunchStoryboardImages(projectRoot, projectConfig, self.locations);
  81. cleanFileResources(projectRoot, projectConfig, self.locations);
  82. });
  83. };
  84. /**
  85. * Updates config files in project based on app's config.xml and config munge,
  86. * generated by plugins.
  87. *
  88. * @param {ConfigParser} sourceConfig A project's configuration that will
  89. * be merged into platform's config.xml
  90. * @param {ConfigChanges} configMunger An initialized ConfigChanges instance
  91. * for this platform.
  92. * @param {Object} locations A map of locations for this platform
  93. *
  94. * @return {ConfigParser} An instance of ConfigParser, that
  95. * represents current project's configuration. When returned, the
  96. * configuration is already dumped to appropriate config.xml file.
  97. */
  98. function updateConfigFile (sourceConfig, configMunger, locations) {
  99. events.emit('verbose', 'Generating platform-specific config.xml from defaults for iOS at ' + locations.configXml);
  100. // First cleanup current config and merge project's one into own
  101. // Overwrite platform config.xml with defaults.xml.
  102. shell.cp('-f', locations.defaultConfigXml, locations.configXml);
  103. // Then apply config changes from global munge to all config files
  104. // in project (including project's config)
  105. configMunger.reapply_global_munge().save_all();
  106. events.emit('verbose', 'Merging project\'s config.xml into platform-specific iOS config.xml');
  107. // Merge changes from app's config.xml into platform's one
  108. var config = new ConfigParser(locations.configXml);
  109. xmlHelpers.mergeXml(sourceConfig.doc.getroot(),
  110. config.doc.getroot(), 'ios', /* clobber= */true);
  111. config.write();
  112. return config;
  113. }
  114. /**
  115. * Logs all file operations via the verbose event stream, indented.
  116. */
  117. function logFileOp (message) {
  118. events.emit('verbose', ' ' + message);
  119. }
  120. /**
  121. * Updates platform 'www' directory by replacing it with contents of
  122. * 'platform_www' and app www. Also copies project's overrides' folder into
  123. * the platform 'www' folder
  124. *
  125. * @param {Object} cordovaProject An object which describes cordova project.
  126. * @param {boolean} destinations An object that contains destinations
  127. * paths for www files.
  128. */
  129. function updateWww (cordovaProject, destinations) {
  130. var sourceDirs = [
  131. path.relative(cordovaProject.root, cordovaProject.locations.www),
  132. path.relative(cordovaProject.root, destinations.platformWww)
  133. ];
  134. // If project contains 'merges' for our platform, use them as another overrides
  135. var merges_path = path.join(cordovaProject.root, 'merges', 'ios');
  136. if (fs.existsSync(merges_path)) {
  137. events.emit('verbose', 'Found "merges/ios" folder. Copying its contents into the iOS project.');
  138. sourceDirs.push(path.join('merges', 'ios'));
  139. }
  140. var targetDir = path.relative(cordovaProject.root, destinations.www);
  141. events.emit(
  142. 'verbose', 'Merging and updating files from [' + sourceDirs.join(', ') + '] to ' + targetDir);
  143. FileUpdater.mergeAndUpdateDir(
  144. sourceDirs, targetDir, { rootDir: cordovaProject.root }, logFileOp);
  145. }
  146. /**
  147. * Cleans all files from the platform 'www' directory.
  148. */
  149. function cleanWww (projectRoot, locations) {
  150. var targetDir = path.relative(projectRoot, locations.www);
  151. events.emit('verbose', 'Cleaning ' + targetDir);
  152. // No source paths are specified, so mergeAndUpdateDir() will clear the target directory.
  153. FileUpdater.mergeAndUpdateDir(
  154. [], targetDir, { rootDir: projectRoot, all: true }, logFileOp);
  155. }
  156. /**
  157. * Updates project structure and AndroidManifest according to project's configuration.
  158. *
  159. * @param {ConfigParser} platformConfig A project's configuration that will
  160. * be used to update project
  161. * @param {Object} locations A map of locations for this platform (In/Out)
  162. */
  163. function updateProject (platformConfig, locations) {
  164. // CB-6992 it is necessary to normalize characters
  165. // because node and shell scripts handles unicode symbols differently
  166. // We need to normalize the name to NFD form since iOS uses NFD unicode form
  167. var name = unorm.nfd(platformConfig.name());
  168. var pkg = platformConfig.getAttribute('ios-CFBundleIdentifier') || platformConfig.packageName();
  169. var version = platformConfig.version();
  170. var displayName = platformConfig.shortName && platformConfig.shortName();
  171. var originalName = path.basename(locations.xcodeCordovaProj);
  172. // Update package id (bundle id)
  173. var plistFile = path.join(locations.xcodeCordovaProj, originalName + '-Info.plist');
  174. var infoPlist = plist.parse(fs.readFileSync(plistFile, 'utf8'));
  175. infoPlist['CFBundleIdentifier'] = pkg;
  176. // Update version (bundle version)
  177. infoPlist['CFBundleShortVersionString'] = version;
  178. var CFBundleVersion = platformConfig.getAttribute('ios-CFBundleVersion') || default_CFBundleVersion(version);
  179. infoPlist['CFBundleVersion'] = CFBundleVersion;
  180. if (platformConfig.getAttribute('defaultlocale')) {
  181. infoPlist['CFBundleDevelopmentRegion'] = platformConfig.getAttribute('defaultlocale');
  182. }
  183. if (displayName) {
  184. infoPlist['CFBundleDisplayName'] = displayName;
  185. }
  186. // replace Info.plist ATS entries according to <access> and <allow-navigation> config.xml entries
  187. var ats = writeATSEntries(platformConfig);
  188. if (Object.keys(ats).length > 0) {
  189. infoPlist['NSAppTransportSecurity'] = ats;
  190. } else {
  191. delete infoPlist['NSAppTransportSecurity'];
  192. }
  193. handleOrientationSettings(platformConfig, infoPlist);
  194. updateProjectPlistForLaunchStoryboard(platformConfig, infoPlist);
  195. var info_contents = plist.build(infoPlist);
  196. info_contents = info_contents.replace(/<string>[\s\r\n]*<\/string>/g, '<string></string>');
  197. fs.writeFileSync(plistFile, info_contents, 'utf-8');
  198. events.emit('verbose', 'Wrote out iOS Bundle Identifier "' + pkg + '" and iOS Bundle Version "' + version + '" to ' + plistFile);
  199. return handleBuildSettings(platformConfig, locations, infoPlist).then(function () {
  200. if (name === originalName) {
  201. events.emit('verbose', 'iOS Product Name has not changed (still "' + originalName + '")');
  202. return Q();
  203. } else { // CB-11712 <name> was changed, we don't support it'
  204. var errorString =
  205. 'The product name change (<name> tag) in config.xml is not supported dynamically.\n' +
  206. 'To change your product name, you have to remove, then add your ios platform again.\n' +
  207. 'Make sure you save your plugins beforehand using `cordova plugin save`.\n' +
  208. '\tcordova plugin save\n' +
  209. '\tcordova platform rm ios\n' +
  210. '\tcordova platform add ios\n'
  211. ;
  212. return Q.reject(new CordovaError(errorString));
  213. }
  214. });
  215. }
  216. function handleOrientationSettings (platformConfig, infoPlist) {
  217. switch (getOrientationValue(platformConfig)) {
  218. case 'portrait':
  219. infoPlist['UIInterfaceOrientation'] = [ 'UIInterfaceOrientationPortrait' ];
  220. infoPlist['UISupportedInterfaceOrientations'] = [ 'UIInterfaceOrientationPortrait', 'UIInterfaceOrientationPortraitUpsideDown' ];
  221. infoPlist['UISupportedInterfaceOrientations~ipad'] = [ 'UIInterfaceOrientationPortrait', 'UIInterfaceOrientationPortraitUpsideDown' ];
  222. break;
  223. case 'landscape':
  224. infoPlist['UIInterfaceOrientation'] = [ 'UIInterfaceOrientationLandscapeLeft' ];
  225. infoPlist['UISupportedInterfaceOrientations'] = [ 'UIInterfaceOrientationLandscapeLeft', 'UIInterfaceOrientationLandscapeRight' ];
  226. infoPlist['UISupportedInterfaceOrientations~ipad'] = [ 'UIInterfaceOrientationLandscapeLeft', 'UIInterfaceOrientationLandscapeRight' ];
  227. break;
  228. case 'all':
  229. infoPlist['UIInterfaceOrientation'] = [ 'UIInterfaceOrientationPortrait' ];
  230. infoPlist['UISupportedInterfaceOrientations'] = [ 'UIInterfaceOrientationPortrait', 'UIInterfaceOrientationPortraitUpsideDown', 'UIInterfaceOrientationLandscapeLeft', 'UIInterfaceOrientationLandscapeRight' ];
  231. infoPlist['UISupportedInterfaceOrientations~ipad'] = [ 'UIInterfaceOrientationPortrait', 'UIInterfaceOrientationPortraitUpsideDown', 'UIInterfaceOrientationLandscapeLeft', 'UIInterfaceOrientationLandscapeRight' ];
  232. break;
  233. case 'default':
  234. infoPlist['UISupportedInterfaceOrientations'] = [ 'UIInterfaceOrientationPortrait', 'UIInterfaceOrientationLandscapeLeft', 'UIInterfaceOrientationLandscapeRight' ];
  235. infoPlist['UISupportedInterfaceOrientations~ipad'] = [ 'UIInterfaceOrientationPortrait', 'UIInterfaceOrientationPortraitUpsideDown', 'UIInterfaceOrientationLandscapeLeft', 'UIInterfaceOrientationLandscapeRight' ];
  236. delete infoPlist['UIInterfaceOrientation'];
  237. }
  238. }
  239. function handleBuildSettings (platformConfig, locations, infoPlist) {
  240. var targetDevice = parseTargetDevicePreference(platformConfig.getPreference('target-device', 'ios'));
  241. var deploymentTarget = platformConfig.getPreference('deployment-target', 'ios');
  242. var needUpdatedBuildSettingsForLaunchStoryboard = checkIfBuildSettingsNeedUpdatedForLaunchStoryboard(platformConfig, infoPlist);
  243. // no build settings provided and we don't need to update build settings for launch storyboards,
  244. // then we don't need to parse and update .pbxproj file
  245. if (!targetDevice && !deploymentTarget && !needUpdatedBuildSettingsForLaunchStoryboard) {
  246. return Q();
  247. }
  248. var proj = new xcode.project(locations.pbxproj); /* eslint new-cap : 0 */
  249. try {
  250. proj.parseSync();
  251. } catch (err) {
  252. return Q.reject(new CordovaError('Could not parse project.pbxproj: ' + err));
  253. }
  254. if (targetDevice) {
  255. events.emit('verbose', 'Set TARGETED_DEVICE_FAMILY to ' + targetDevice + '.');
  256. proj.updateBuildProperty('TARGETED_DEVICE_FAMILY', targetDevice);
  257. }
  258. if (deploymentTarget) {
  259. events.emit('verbose', 'Set IPHONEOS_DEPLOYMENT_TARGET to "' + deploymentTarget + '".');
  260. proj.updateBuildProperty('IPHONEOS_DEPLOYMENT_TARGET', deploymentTarget);
  261. }
  262. updateBuildSettingsForLaunchStoryboard(proj, platformConfig, infoPlist);
  263. fs.writeFileSync(locations.pbxproj, proj.writeSync(), 'utf-8');
  264. return Q();
  265. }
  266. function mapIconResources (icons, iconsDir) {
  267. // See https://developer.apple.com/library/ios/documentation/UserExperience/Conceptual/MobileHIG/IconMatrix.html
  268. // for launch images sizes reference.
  269. var platformIcons = [
  270. {dest: 'icon-20.png', width: 20, height: 20},
  271. {dest: 'icon-20@2x.png', width: 40, height: 40},
  272. {dest: 'icon-20@3x.png', width: 60, height: 60},
  273. {dest: 'icon-40.png', width: 40, height: 40},
  274. {dest: 'icon-40@2x.png', width: 80, height: 80},
  275. {dest: 'icon-50.png', width: 50, height: 50},
  276. {dest: 'icon-50@2x.png', width: 100, height: 100},
  277. {dest: 'icon-60@2x.png', width: 120, height: 120},
  278. {dest: 'icon-60@3x.png', width: 180, height: 180},
  279. {dest: 'icon-72.png', width: 72, height: 72},
  280. {dest: 'icon-72@2x.png', width: 144, height: 144},
  281. {dest: 'icon-76.png', width: 76, height: 76},
  282. {dest: 'icon-76@2x.png', width: 152, height: 152},
  283. {dest: 'icon-83.5@2x.png', width: 167, height: 167},
  284. {dest: 'icon-1024.png', width: 1024, height: 1024},
  285. {dest: 'icon-small.png', width: 29, height: 29},
  286. {dest: 'icon-small@2x.png', width: 58, height: 58},
  287. {dest: 'icon-small@3x.png', width: 87, height: 87},
  288. {dest: 'icon.png', width: 57, height: 57},
  289. {dest: 'icon@2x.png', width: 114, height: 114},
  290. {dest: 'AppIcon24x24@2x.png', width: 48, height: 48},
  291. {dest: 'AppIcon27.5x27.5@2x.png', width: 55, height: 55},
  292. {dest: 'AppIcon29x29@2x.png', width: 58, height: 58},
  293. {dest: 'AppIcon29x29@3x.png', width: 87, height: 87},
  294. {dest: 'AppIcon40x40@2x.png', width: 80, height: 80},
  295. {dest: 'AppIcon44x44@2x.png', width: 88, height: 88},
  296. {dest: 'AppIcon86x86@2x.png', width: 172, height: 172},
  297. {dest: 'AppIcon98x98@2x.png', width: 196, height: 196}
  298. ];
  299. var pathMap = {};
  300. platformIcons.forEach(function (item) {
  301. var icon = icons.getBySize(item.width, item.height) || icons.getDefault();
  302. if (icon) {
  303. var target = path.join(iconsDir, item.dest);
  304. pathMap[target] = icon.src;
  305. }
  306. });
  307. return pathMap;
  308. }
  309. function getIconsDir (projectRoot, platformProjDir) {
  310. var iconsDir;
  311. var xcassetsExists = folderExists(path.join(projectRoot, platformProjDir, 'Images.xcassets/'));
  312. if (xcassetsExists) {
  313. iconsDir = path.join(platformProjDir, 'Images.xcassets/AppIcon.appiconset/');
  314. } else {
  315. iconsDir = path.join(platformProjDir, 'Resources/icons/');
  316. }
  317. return iconsDir;
  318. }
  319. function updateIcons (cordovaProject, locations) {
  320. var icons = cordovaProject.projectConfig.getIcons('ios');
  321. if (icons.length === 0) {
  322. events.emit('verbose', 'This app does not have icons defined');
  323. return;
  324. }
  325. var platformProjDir = path.relative(cordovaProject.root, locations.xcodeCordovaProj);
  326. var iconsDir = getIconsDir(cordovaProject.root, platformProjDir);
  327. var resourceMap = mapIconResources(icons, iconsDir);
  328. events.emit('verbose', 'Updating icons at ' + iconsDir);
  329. FileUpdater.updatePaths(
  330. resourceMap, { rootDir: cordovaProject.root }, logFileOp);
  331. }
  332. function cleanIcons (projectRoot, projectConfig, locations) {
  333. var icons = projectConfig.getIcons('ios');
  334. if (icons.length > 0) {
  335. var platformProjDir = path.relative(projectRoot, locations.xcodeCordovaProj);
  336. var iconsDir = getIconsDir(projectRoot, platformProjDir);
  337. var resourceMap = mapIconResources(icons, iconsDir);
  338. Object.keys(resourceMap).forEach(function (targetIconPath) {
  339. resourceMap[targetIconPath] = null;
  340. });
  341. events.emit('verbose', 'Cleaning icons at ' + iconsDir);
  342. // Source paths are removed from the map, so updatePaths() will delete the target files.
  343. FileUpdater.updatePaths(
  344. resourceMap, { rootDir: projectRoot, all: true }, logFileOp);
  345. }
  346. }
  347. function mapSplashScreenResources (splashScreens, splashScreensDir) {
  348. var platformSplashScreens = [
  349. {dest: 'Default~iphone.png', width: 320, height: 480},
  350. {dest: 'Default@2x~iphone.png', width: 640, height: 960},
  351. {dest: 'Default-Portrait~ipad.png', width: 768, height: 1024},
  352. {dest: 'Default-Portrait@2x~ipad.png', width: 1536, height: 2048},
  353. {dest: 'Default-Landscape~ipad.png', width: 1024, height: 768},
  354. {dest: 'Default-Landscape@2x~ipad.png', width: 2048, height: 1536},
  355. {dest: 'Default-568h@2x~iphone.png', width: 640, height: 1136},
  356. {dest: 'Default-667h.png', width: 750, height: 1334},
  357. {dest: 'Default-736h.png', width: 1242, height: 2208},
  358. {dest: 'Default-Landscape-736h.png', width: 2208, height: 1242},
  359. {dest: 'Default-2436h.png', width: 1125, height: 2436},
  360. {dest: 'Default-Landscape-2436h.png', width: 2436, height: 1125}
  361. ];
  362. var pathMap = {};
  363. platformSplashScreens.forEach(function (item) {
  364. var splash = splashScreens.getBySize(item.width, item.height);
  365. if (splash) {
  366. var target = path.join(splashScreensDir, item.dest);
  367. pathMap[target] = splash.src;
  368. }
  369. });
  370. return pathMap;
  371. }
  372. function getSplashScreensDir (projectRoot, platformProjDir) {
  373. var splashScreensDir;
  374. var xcassetsExists = folderExists(path.join(projectRoot, platformProjDir, 'Images.xcassets/'));
  375. if (xcassetsExists) {
  376. splashScreensDir = path.join(platformProjDir, 'Images.xcassets/LaunchImage.launchimage/');
  377. } else {
  378. splashScreensDir = path.join(platformProjDir, 'Resources/splash/');
  379. }
  380. return splashScreensDir;
  381. }
  382. function updateSplashScreens (cordovaProject, locations) {
  383. var splashScreens = cordovaProject.projectConfig.getSplashScreens('ios');
  384. if (splashScreens.length === 0) {
  385. events.emit('verbose', 'This app does not have splash screens defined');
  386. return;
  387. }
  388. var platformProjDir = path.relative(cordovaProject.root, locations.xcodeCordovaProj);
  389. var splashScreensDir = getSplashScreensDir(cordovaProject.root, platformProjDir);
  390. var resourceMap = mapSplashScreenResources(splashScreens, splashScreensDir);
  391. events.emit('verbose', 'Updating splash screens at ' + splashScreensDir);
  392. FileUpdater.updatePaths(
  393. resourceMap, { rootDir: cordovaProject.root }, logFileOp);
  394. }
  395. function cleanSplashScreens (projectRoot, projectConfig, locations) {
  396. var splashScreens = projectConfig.getSplashScreens('ios');
  397. if (splashScreens.length > 0) {
  398. var platformProjDir = path.relative(projectRoot, locations.xcodeCordovaProj);
  399. var splashScreensDir = getSplashScreensDir(projectRoot, platformProjDir);
  400. var resourceMap = mapIconResources(splashScreens, splashScreensDir);
  401. Object.keys(resourceMap).forEach(function (targetSplashPath) {
  402. resourceMap[targetSplashPath] = null;
  403. });
  404. events.emit('verbose', 'Cleaning splash screens at ' + splashScreensDir);
  405. // Source paths are removed from the map, so updatePaths() will delete the target files.
  406. FileUpdater.updatePaths(
  407. resourceMap, { rootDir: projectRoot, all: true }, logFileOp);
  408. }
  409. }
  410. function updateFileResources (cordovaProject, locations) {
  411. const platformDir = path.relative(cordovaProject.root, locations.root);
  412. const files = cordovaProject.projectConfig.getFileResources('ios');
  413. const project = projectFile.parse(locations);
  414. // if there are resource-file elements in config.xml
  415. if (files.length === 0) {
  416. events.emit('verbose', 'This app does not have additional resource files defined');
  417. return;
  418. }
  419. let resourceMap = {};
  420. files.forEach(function (res) {
  421. let src = res.src;
  422. let target = res.target;
  423. if (!target) {
  424. target = src;
  425. }
  426. let targetPath = path.join(project.resources_dir, target);
  427. targetPath = path.relative(cordovaProject.root, targetPath);
  428. project.xcode.addResourceFile(target);
  429. resourceMap[targetPath] = src;
  430. });
  431. events.emit('verbose', 'Updating resource files at ' + platformDir);
  432. FileUpdater.updatePaths(
  433. resourceMap, { rootDir: cordovaProject.root }, logFileOp);
  434. project.write();
  435. }
  436. function cleanFileResources (projectRoot, projectConfig, locations) {
  437. const platformDir = path.relative(projectRoot, locations.root);
  438. const files = projectConfig.getFileResources('ios', true);
  439. if (files.length > 0) {
  440. events.emit('verbose', 'Cleaning resource files at ' + platformDir);
  441. const project = projectFile.parse(locations);
  442. var resourceMap = {};
  443. files.forEach(function (res) {
  444. let src = res.src;
  445. let target = res.target;
  446. if (!target) {
  447. target = src;
  448. }
  449. let targetPath = path.join(project.resources_dir, target);
  450. targetPath = path.relative(projectRoot, targetPath);
  451. const resfile = path.join('Resources', path.basename(targetPath));
  452. project.xcode.removeResourceFile(resfile);
  453. resourceMap[targetPath] = null;
  454. });
  455. FileUpdater.updatePaths(
  456. resourceMap, {rootDir: projectRoot, all: true}, logFileOp);
  457. project.write();
  458. }
  459. }
  460. /**
  461. * Returns an array of images for each possible idiom, scale, and size class. The images themselves are
  462. * located in the platform's splash images by their pattern (@scale~idiom~sizesize). All possible
  463. * combinations are returned, but not all will have a `filename` property. If the latter isn't present,
  464. * the device won't attempt to load an image matching the same traits. If the filename is present,
  465. * the device will try to load the image if it corresponds to the traits.
  466. *
  467. * The resulting return looks like this:
  468. *
  469. * [
  470. * {
  471. * idiom: 'universal|ipad|iphone',
  472. * scale: '1x|2x|3x',
  473. * width: 'any|com',
  474. * height: 'any|com',
  475. * filename: undefined|'Default@scale~idiom~widthheight.png',
  476. * src: undefined|'path/to/original/matched/image/from/splash/screens.png',
  477. * target: undefined|'path/to/asset/library/Default@scale~idiom~widthheight.png'
  478. * }, ...
  479. * ]
  480. *
  481. * @param {Array<Object>} splashScreens splash screens as defined in config.xml for this platform
  482. * @param {string} launchStoryboardImagesDir project-root/Images.xcassets/LaunchStoryboard.imageset/
  483. * @return {Array<Object>}
  484. */
  485. function mapLaunchStoryboardContents (splashScreens, launchStoryboardImagesDir) {
  486. var platformLaunchStoryboardImages = [];
  487. var idioms = ['universal', 'ipad', 'iphone'];
  488. var scalesForIdiom = {
  489. universal: ['1x', '2x', '3x'],
  490. ipad: ['1x', '2x'],
  491. iphone: ['1x', '2x', '3x']
  492. };
  493. var sizes = ['com', 'any'];
  494. idioms.forEach(function (idiom) {
  495. scalesForIdiom[idiom].forEach(function (scale) {
  496. sizes.forEach(function (width) {
  497. sizes.forEach(function (height) {
  498. var item = {
  499. idiom: idiom,
  500. scale: scale,
  501. width: width,
  502. height: height
  503. };
  504. /* examples of the search pattern:
  505. * scale ~ idiom ~ width height
  506. * @2x ~ universal ~ any any
  507. * @3x ~ iphone ~ com any
  508. * @2x ~ ipad ~ com any
  509. */
  510. var searchPattern = '@' + scale + '~' + idiom + '~' + width + height;
  511. /* because old node versions don't have Array.find, the below is
  512. * functionally equivalent to this:
  513. * var launchStoryboardImage = splashScreens.find(function(item) {
  514. * return item.src.indexOf(searchPattern) >= 0;
  515. * });
  516. */
  517. var launchStoryboardImage = splashScreens.reduce(function (p, c) {
  518. return (c.src.indexOf(searchPattern) >= 0) ? c : p;
  519. }, undefined);
  520. if (launchStoryboardImage) {
  521. item.filename = 'Default' + searchPattern + '.png';
  522. item.src = launchStoryboardImage.src;
  523. item.target = path.join(launchStoryboardImagesDir, item.filename);
  524. }
  525. platformLaunchStoryboardImages.push(item);
  526. });
  527. });
  528. });
  529. });
  530. return platformLaunchStoryboardImages;
  531. }
  532. /**
  533. * Returns a dictionary representing the source and destination paths for the launch storyboard images
  534. * that need to be copied.
  535. *
  536. * The resulting return looks like this:
  537. *
  538. * {
  539. * 'target-path': 'source-path',
  540. * ...
  541. * }
  542. *
  543. * @param {Array<Object>} splashScreens splash screens as defined in config.xml for this platform
  544. * @param {string} launchStoryboardImagesDir project-root/Images.xcassets/LaunchStoryboard.imageset/
  545. * @return {Object}
  546. */
  547. function mapLaunchStoryboardResources (splashScreens, launchStoryboardImagesDir) {
  548. var platformLaunchStoryboardImages = mapLaunchStoryboardContents(splashScreens, launchStoryboardImagesDir);
  549. var pathMap = {};
  550. platformLaunchStoryboardImages.forEach(function (item) {
  551. if (item.target) {
  552. pathMap[item.target] = item.src;
  553. }
  554. });
  555. return pathMap;
  556. }
  557. /**
  558. * Builds the object that represents the contents.json file for the LaunchStoryboard image set.
  559. *
  560. * The resulting return looks like this:
  561. *
  562. * {
  563. * images: [
  564. * {
  565. * idiom: 'universal|ipad|iphone',
  566. * scale: '1x|2x|3x',
  567. * width-class: undefined|'compact',
  568. * height-class: undefined|'compact'
  569. * }, ...
  570. * ],
  571. * info: {
  572. * author: 'Xcode',
  573. * version: 1
  574. * }
  575. * }
  576. *
  577. * A bit of minor logic is used to map from the array of images returned from mapLaunchStoryboardContents
  578. * to the format requried by Xcode.
  579. *
  580. * @param {Array<Object>} splashScreens splash screens as defined in config.xml for this platform
  581. * @param {string} launchStoryboardImagesDir project-root/Images.xcassets/LaunchStoryboard.imageset/
  582. * @return {Object}
  583. */
  584. function getLaunchStoryboardContentsJSON (splashScreens, launchStoryboardImagesDir) {
  585. var platformLaunchStoryboardImages = mapLaunchStoryboardContents(splashScreens, launchStoryboardImagesDir);
  586. var contentsJSON = {
  587. images: [],
  588. info: {
  589. author: 'Xcode',
  590. version: 1
  591. }
  592. };
  593. contentsJSON.images = platformLaunchStoryboardImages.map(function (item) {
  594. var newItem = {
  595. idiom: item.idiom,
  596. scale: item.scale
  597. };
  598. // Xcode doesn't want any size class property if the class is "any"
  599. // If our size class is "com", Xcode wants "compact".
  600. if (item.width !== CDV_ANY_SIZE_CLASS) {
  601. newItem['width-class'] = IMAGESET_COMPACT_SIZE_CLASS;
  602. }
  603. if (item.height !== CDV_ANY_SIZE_CLASS) {
  604. newItem['height-class'] = IMAGESET_COMPACT_SIZE_CLASS;
  605. }
  606. // Xcode doesn't want a filename property if there's no image for these traits
  607. if (item.filename) {
  608. newItem.filename = item.filename;
  609. }
  610. return newItem;
  611. });
  612. return contentsJSON;
  613. }
  614. /**
  615. * Determines if the project's build settings may need to be updated for launch storyboard support
  616. *
  617. */
  618. function checkIfBuildSettingsNeedUpdatedForLaunchStoryboard (platformConfig, infoPlist) {
  619. var hasLaunchStoryboardImages = platformHasLaunchStoryboardImages(platformConfig);
  620. var hasLegacyLaunchImages = platformHasLegacyLaunchImages(platformConfig);
  621. var currentLaunchStoryboard = infoPlist[UI_LAUNCH_STORYBOARD_NAME];
  622. if (hasLaunchStoryboardImages && currentLaunchStoryboard === CDV_LAUNCH_STORYBOARD_NAME && !hasLegacyLaunchImages) {
  623. // don't need legacy launch images if we are using our launch storyboard
  624. // so we do need to update the project file
  625. events.emit('verbose', 'Need to update build settings because project is using our launch storyboard.');
  626. return true;
  627. } else if (hasLegacyLaunchImages && !currentLaunchStoryboard) {
  628. // we do need to ensure legacy launch images are used if there's no launch storyboard present
  629. // so we do need to update the project file
  630. events.emit('verbose', 'Need to update build settings because project is using legacy launch images and no storyboard.');
  631. return true;
  632. }
  633. events.emit('verbose', 'No need to update build settings for launch storyboard support.');
  634. return false;
  635. }
  636. function updateBuildSettingsForLaunchStoryboard (proj, platformConfig, infoPlist) {
  637. var hasLaunchStoryboardImages = platformHasLaunchStoryboardImages(platformConfig);
  638. var hasLegacyLaunchImages = platformHasLegacyLaunchImages(platformConfig);
  639. var currentLaunchStoryboard = infoPlist[UI_LAUNCH_STORYBOARD_NAME];
  640. if (hasLaunchStoryboardImages && currentLaunchStoryboard === CDV_LAUNCH_STORYBOARD_NAME && !hasLegacyLaunchImages) {
  641. // don't need legacy launch images if we are using our launch storyboard
  642. events.emit('verbose', 'Removed ' + LAUNCHIMAGE_BUILD_SETTING + ' because project is using our launch storyboard.');
  643. proj.removeBuildProperty(LAUNCHIMAGE_BUILD_SETTING);
  644. } else if (hasLegacyLaunchImages && !currentLaunchStoryboard) {
  645. // we do need to ensure legacy launch images are used if there's no launch storyboard present
  646. events.emit('verbose', 'Set ' + LAUNCHIMAGE_BUILD_SETTING + ' to ' + LAUNCHIMAGE_BUILD_SETTING_VALUE + ' because project is using legacy launch images and no storyboard.');
  647. proj.updateBuildProperty(LAUNCHIMAGE_BUILD_SETTING, LAUNCHIMAGE_BUILD_SETTING_VALUE);
  648. } else {
  649. events.emit('verbose', 'Did not update build settings for launch storyboard support.');
  650. }
  651. }
  652. function splashScreensHaveLaunchStoryboardImages (contentsJSON) {
  653. /* do we have any launch images do we have for our launch storyboard?
  654. * Again, for old Node versions, the below code is equivalent to this:
  655. * return !!contentsJSON.images.find(function (item) {
  656. * return item.filename !== undefined;
  657. * });
  658. */
  659. return !!contentsJSON.images.reduce(function (p, c) {
  660. return (c.filename !== undefined) ? c : p;
  661. }, undefined);
  662. }
  663. function platformHasLaunchStoryboardImages (platformConfig) {
  664. var splashScreens = platformConfig.getSplashScreens('ios');
  665. var contentsJSON = getLaunchStoryboardContentsJSON(splashScreens, ''); // note: we don't need a file path here; we're just counting
  666. return splashScreensHaveLaunchStoryboardImages(contentsJSON);
  667. }
  668. function platformHasLegacyLaunchImages (platformConfig) {
  669. var splashScreens = platformConfig.getSplashScreens('ios');
  670. return !!splashScreens.reduce(function (p, c) {
  671. return (c.width !== undefined || c.height !== undefined) ? c : p;
  672. }, undefined);
  673. }
  674. /**
  675. * Updates the project's plist based upon our launch storyboard images. If there are no images, then we should
  676. * fall back to the regular launch images that might be supplied (that is, our app will be scaled on an iPad Pro),
  677. * and if there are some images, we need to alter the UILaunchStoryboardName property to point to
  678. * CDVLaunchScreen.
  679. *
  680. * There's some logic here to avoid overwriting changes the user might have made to their plist if they are using
  681. * their own launch storyboard.
  682. */
  683. function updateProjectPlistForLaunchStoryboard (platformConfig, infoPlist) {
  684. var currentLaunchStoryboard = infoPlist[UI_LAUNCH_STORYBOARD_NAME];
  685. events.emit('verbose', 'Current launch storyboard ' + currentLaunchStoryboard);
  686. var hasLaunchStoryboardImages = platformHasLaunchStoryboardImages(platformConfig);
  687. if (hasLaunchStoryboardImages && !currentLaunchStoryboard) {
  688. // only change the launch storyboard if we have images to use AND the current value is blank
  689. // if it's not blank, we've either done this before, or the user has their own launch storyboard
  690. events.emit('verbose', 'Changing info plist to use our launch storyboard');
  691. infoPlist[UI_LAUNCH_STORYBOARD_NAME] = CDV_LAUNCH_STORYBOARD_NAME;
  692. return;
  693. }
  694. if (!hasLaunchStoryboardImages && currentLaunchStoryboard === CDV_LAUNCH_STORYBOARD_NAME) {
  695. // only revert to using the launch images if we have don't have any images for the launch storyboard
  696. // but only clear it if current launch storyboard is our storyboard; the user might be using their
  697. // own storyboard instead.
  698. events.emit('verbose', 'Changing info plist to use legacy launch images');
  699. delete infoPlist[UI_LAUNCH_STORYBOARD_NAME];
  700. return;
  701. }
  702. events.emit('verbose', 'Not changing launch storyboard setting in info plist.');
  703. }
  704. /**
  705. * Returns the directory for the Launch Storyboard image set, if image sets are being used. If they aren't
  706. * being used, returns null.
  707. *
  708. * @param {string} projectRoot The project's root directory
  709. * @param {string} platformProjDir The platform's project directory
  710. */
  711. function getLaunchStoryboardImagesDir (projectRoot, platformProjDir) {
  712. var launchStoryboardImagesDir;
  713. var xcassetsExists = folderExists(path.join(projectRoot, platformProjDir, 'Images.xcassets/'));
  714. if (xcassetsExists) {
  715. launchStoryboardImagesDir = path.join(platformProjDir, 'Images.xcassets/LaunchStoryboard.imageset/');
  716. } else {
  717. // if we don't have a asset library for images, we can't do the storyboard.
  718. launchStoryboardImagesDir = null;
  719. }
  720. return launchStoryboardImagesDir;
  721. }
  722. /**
  723. * Update the images for the Launch Storyboard and updates the image set's contents.json file appropriately.
  724. *
  725. * @param {Object} cordovaProject The cordova project
  726. * @param {Object} locations A dictionary containing useful location paths
  727. */
  728. function updateLaunchStoryboardImages (cordovaProject, locations) {
  729. var splashScreens = cordovaProject.projectConfig.getSplashScreens('ios');
  730. var platformProjDir = path.relative(cordovaProject.root, locations.xcodeCordovaProj);
  731. var launchStoryboardImagesDir = getLaunchStoryboardImagesDir(cordovaProject.root, platformProjDir);
  732. if (launchStoryboardImagesDir) {
  733. var resourceMap = mapLaunchStoryboardResources(splashScreens, launchStoryboardImagesDir);
  734. var contentsJSON = getLaunchStoryboardContentsJSON(splashScreens, launchStoryboardImagesDir);
  735. events.emit('verbose', 'Updating launch storyboard images at ' + launchStoryboardImagesDir);
  736. FileUpdater.updatePaths(
  737. resourceMap, { rootDir: cordovaProject.root }, logFileOp);
  738. events.emit('verbose', 'Updating Storyboard image set contents.json');
  739. fs.writeFileSync(path.join(cordovaProject.root, launchStoryboardImagesDir, 'Contents.json'),
  740. JSON.stringify(contentsJSON, null, 2));
  741. }
  742. }
  743. /**
  744. * Removes the images from the launch storyboard's image set and updates the image set's contents.json
  745. * file appropriately.
  746. *
  747. * @param {string} projectRoot Path to the project root
  748. * @param {Object} projectConfig The project's config.xml
  749. * @param {Object} locations A dictionary containing useful location paths
  750. */
  751. function cleanLaunchStoryboardImages (projectRoot, projectConfig, locations) {
  752. var splashScreens = projectConfig.getSplashScreens('ios');
  753. var platformProjDir = path.relative(projectRoot, locations.xcodeCordovaProj);
  754. var launchStoryboardImagesDir = getLaunchStoryboardImagesDir(projectRoot, platformProjDir);
  755. if (launchStoryboardImagesDir) {
  756. var resourceMap = mapLaunchStoryboardResources(splashScreens, launchStoryboardImagesDir);
  757. var contentsJSON = getLaunchStoryboardContentsJSON(splashScreens, launchStoryboardImagesDir);
  758. Object.keys(resourceMap).forEach(function (targetPath) {
  759. resourceMap[targetPath] = null;
  760. });
  761. events.emit('verbose', 'Cleaning storyboard image set at ' + launchStoryboardImagesDir);
  762. // Source paths are removed from the map, so updatePaths() will delete the target files.
  763. FileUpdater.updatePaths(
  764. resourceMap, { rootDir: projectRoot, all: true }, logFileOp);
  765. // delete filename from contents.json
  766. contentsJSON.images.forEach(function (image) {
  767. image.filename = undefined;
  768. });
  769. events.emit('verbose', 'Updating Storyboard image set contents.json');
  770. fs.writeFileSync(path.join(projectRoot, launchStoryboardImagesDir, 'Contents.json'),
  771. JSON.stringify(contentsJSON, null, 2));
  772. }
  773. }
  774. /**
  775. * Queries ConfigParser object for the orientation <preference> value. Warns if
  776. * global preference value is not supported by platform.
  777. *
  778. * @param {Object} platformConfig ConfigParser object
  779. *
  780. * @return {String} Global/platform-specific orientation in lower-case
  781. * (or empty string if both are undefined).
  782. */
  783. function getOrientationValue (platformConfig) {
  784. var ORIENTATION_DEFAULT = 'default';
  785. var orientation = platformConfig.getPreference('orientation');
  786. if (!orientation) {
  787. return '';
  788. }
  789. orientation = orientation.toLowerCase();
  790. // Check if the given global orientation is supported
  791. if (['default', 'portrait', 'landscape', 'all'].indexOf(orientation) >= 0) {
  792. return orientation;
  793. }
  794. events.emit('warn', 'Unrecognized value for Orientation preference: ' + orientation +
  795. '. Defaulting to value: ' + ORIENTATION_DEFAULT + '.');
  796. return ORIENTATION_DEFAULT;
  797. }
  798. /*
  799. Parses all <access> and <allow-navigation> entries and consolidates duplicates (for ATS).
  800. Returns an object with a Hostname as the key, and the value an object with properties:
  801. {
  802. Hostname, // String
  803. NSExceptionAllowsInsecureHTTPLoads, // boolean
  804. NSIncludesSubdomains, // boolean
  805. NSExceptionMinimumTLSVersion, // String
  806. NSExceptionRequiresForwardSecrecy, // boolean
  807. NSRequiresCertificateTransparency, // boolean
  808. // the three below _only_ show when the Hostname is '*'
  809. // if any of the three are set, it disables setting NSAllowsArbitraryLoads
  810. // (Apple already enforces this in ATS)
  811. NSAllowsArbitraryLoadsInWebContent, // boolean (default: false)
  812. NSAllowsLocalNetworking, // boolean (default: false)
  813. NSAllowsArbitraryLoadsForMedia, // boolean (default:false)
  814. }
  815. */
  816. function processAccessAndAllowNavigationEntries (config) {
  817. var accesses = config.getAccesses();
  818. var allow_navigations = config.getAllowNavigations();
  819. return allow_navigations
  820. // we concat allow_navigations and accesses, after processing accesses
  821. .concat(accesses.map(function (obj) {
  822. // map accesses to a common key interface using 'href', not origin
  823. obj.href = obj.origin;
  824. delete obj.origin;
  825. return obj;
  826. }))
  827. // we reduce the array to an object with all the entries processed (key is Hostname)
  828. .reduce(function (previousReturn, currentElement) {
  829. var options = {
  830. minimum_tls_version: currentElement.minimum_tls_version,
  831. requires_forward_secrecy: currentElement.requires_forward_secrecy,
  832. requires_certificate_transparency: currentElement.requires_certificate_transparency,
  833. allows_arbitrary_loads_for_media: currentElement.allows_arbitrary_loads_in_media || currentElement.allows_arbitrary_loads_for_media,
  834. allows_arbitrary_loads_in_web_content: currentElement.allows_arbitrary_loads_in_web_content,
  835. allows_local_networking: currentElement.allows_local_networking
  836. };
  837. var obj = parseWhitelistUrlForATS(currentElement.href, options);
  838. if (obj) {
  839. // we 'union' duplicate entries
  840. var item = previousReturn[obj.Hostname];
  841. if (!item) {
  842. item = {};
  843. }
  844. for (var o in obj) {
  845. if (obj.hasOwnProperty(o)) {
  846. item[o] = obj[o];
  847. }
  848. }
  849. previousReturn[obj.Hostname] = item;
  850. }
  851. return previousReturn;
  852. }, {});
  853. }
  854. /*
  855. Parses a URL and returns an object with these keys:
  856. {
  857. Hostname, // String
  858. NSExceptionAllowsInsecureHTTPLoads, // boolean (default: false)
  859. NSIncludesSubdomains, // boolean (default: false)
  860. NSExceptionMinimumTLSVersion, // String (default: 'TLSv1.2')
  861. NSExceptionRequiresForwardSecrecy, // boolean (default: true)
  862. NSRequiresCertificateTransparency, // boolean (default: false)
  863. // the three below _only_ apply when the Hostname is '*'
  864. // if any of the three are set, it disables setting NSAllowsArbitraryLoads
  865. // (Apple already enforces this in ATS)
  866. NSAllowsArbitraryLoadsInWebContent, // boolean (default: false)
  867. NSAllowsLocalNetworking, // boolean (default: false)
  868. NSAllowsArbitraryLoadsForMedia, // boolean (default:false)
  869. }
  870. null is returned if the URL cannot be parsed, or is to be skipped for ATS.
  871. */
  872. function parseWhitelistUrlForATS (url, options) {
  873. var href = URL.parse(url);
  874. var retObj = {};
  875. retObj.Hostname = href.hostname;
  876. // Guiding principle: we only set values in retObj if they are NOT the default
  877. if (url === '*') {
  878. retObj.Hostname = '*';
  879. var val;
  880. val = (options.allows_arbitrary_loads_in_web_content === 'true');
  881. if (options.allows_arbitrary_loads_in_web_content && val) { // default is false
  882. retObj.NSAllowsArbitraryLoadsInWebContent = true;
  883. }
  884. val = (options.allows_arbitrary_loads_for_media === 'true');
  885. if (options.allows_arbitrary_loads_for_media && val) { // default is false
  886. retObj.NSAllowsArbitraryLoadsForMedia = true;
  887. }
  888. val = (options.allows_local_networking === 'true');
  889. if (options.allows_local_networking && val) { // default is false
  890. retObj.NSAllowsLocalNetworking = true;
  891. }
  892. return retObj;
  893. }
  894. if (!retObj.Hostname) {
  895. // check origin, if it allows subdomains (wildcard in hostname), we set NSIncludesSubdomains to YES. Default is NO
  896. var subdomain1 = '/*.'; // wildcard in hostname
  897. var subdomain2 = '*://*.'; // wildcard in hostname and protocol
  898. var subdomain3 = '*://'; // wildcard in protocol only
  899. if (href.pathname.indexOf(subdomain1) === 0) {
  900. retObj.NSIncludesSubdomains = true;
  901. retObj.Hostname = href.pathname.substring(subdomain1.length);
  902. } else if (href.pathname.indexOf(subdomain2) === 0) {
  903. retObj.NSIncludesSubdomains = true;
  904. retObj.Hostname = href.pathname.substring(subdomain2.length);
  905. } else if (href.pathname.indexOf(subdomain3) === 0) {
  906. retObj.Hostname = href.pathname.substring(subdomain3.length);
  907. } else {
  908. // Handling "scheme:*" case to avoid creating of a blank key in NSExceptionDomains.
  909. return null;
  910. }
  911. }
  912. if (options.minimum_tls_version && options.minimum_tls_version !== 'TLSv1.2') { // default is TLSv1.2
  913. retObj.NSExceptionMinimumTLSVersion = options.minimum_tls_version;
  914. }
  915. var rfs = (options.requires_forward_secrecy === 'true');
  916. if (options.requires_forward_secrecy && !rfs) { // default is true
  917. retObj.NSExceptionRequiresForwardSecrecy = false;
  918. }
  919. var rct = (options.requires_certificate_transparency === 'true');
  920. if (options.requires_certificate_transparency && rct) { // default is false
  921. retObj.NSRequiresCertificateTransparency = true;
  922. }
  923. // if the scheme is HTTP, we set NSExceptionAllowsInsecureHTTPLoads to YES. Default is NO
  924. if (href.protocol === 'http:') {
  925. retObj.NSExceptionAllowsInsecureHTTPLoads = true;
  926. } else if (!href.protocol && href.pathname.indexOf('*:/') === 0) { // wilcard in protocol
  927. retObj.NSExceptionAllowsInsecureHTTPLoads = true;
  928. }
  929. return retObj;
  930. }
  931. /*
  932. App Transport Security (ATS) writer from <access> and <allow-navigation> tags
  933. in config.xml
  934. */
  935. function writeATSEntries (config) {
  936. var pObj = processAccessAndAllowNavigationEntries(config);
  937. var ats = {};
  938. for (var hostname in pObj) {
  939. if (pObj.hasOwnProperty(hostname)) {
  940. var entry = pObj[hostname];
  941. // Guiding principle: we only set values if they are available
  942. if (hostname === '*') {
  943. // always write this, for iOS 9, since in iOS 10 it will be overriden if
  944. // any of the other three keys are written
  945. ats['NSAllowsArbitraryLoads'] = true;
  946. // at least one of the overriding keys is present
  947. if (entry.NSAllowsArbitraryLoadsInWebContent) {
  948. ats['NSAllowsArbitraryLoadsInWebContent'] = true;
  949. }
  950. if (entry.NSAllowsArbitraryLoadsForMedia) {
  951. ats['NSAllowsArbitraryLoadsForMedia'] = true;
  952. }
  953. if (entry.NSAllowsLocalNetworking) {
  954. ats['NSAllowsLocalNetworking'] = true;
  955. }
  956. continue;
  957. }
  958. var exceptionDomain = {};
  959. for (var key in entry) {
  960. if (entry.hasOwnProperty(key) && key !== 'Hostname') {
  961. exceptionDomain[key] = entry[key];
  962. }
  963. }
  964. if (!ats['NSExceptionDomains']) {
  965. ats['NSExceptionDomains'] = {};
  966. }
  967. ats['NSExceptionDomains'][hostname] = exceptionDomain;
  968. }
  969. }
  970. return ats;
  971. }
  972. function folderExists (folderPath) {
  973. try {
  974. var stat = fs.statSync(folderPath);
  975. return stat && stat.isDirectory();
  976. } catch (e) {
  977. return false;
  978. }
  979. }
  980. // Construct a default value for CFBundleVersion as the version with any
  981. // -rclabel stripped=.
  982. function default_CFBundleVersion (version) {
  983. return version.split('-')[0];
  984. }
  985. // Converts cordova specific representation of target device to XCode value
  986. function parseTargetDevicePreference (value) {
  987. if (!value) return null;
  988. var map = {'universal': '"1,2"', 'handset': '"1"', 'tablet': '"2"'};
  989. if (map[value.toLowerCase()]) {
  990. return map[value.toLowerCase()];
  991. }
  992. events.emit('warn', 'Unrecognized value for target-device preference: ' + value + '.');
  993. return null;
  994. }