Repositorio del curso CCOM4030 el semestre B91 del proyecto Artesanías con el Instituto de Cultura

prepare.js 50KB

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