12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151 |
- /**
- Licensed to the Apache Software Foundation (ASF) under one
- or more contributor license agreements. See the NOTICE file
- distributed with this work for additional information
- regarding copyright ownership. The ASF licenses this file
- to you under the Apache License, Version 2.0 (the
- "License"); you may not use this file except in compliance
- with the License. You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing,
- software distributed under the License is distributed on an
- "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- KIND, either express or implied. See the License for the
- specific language governing permissions and limitations
- under the License.
- */
-
- 'use strict';
-
- const fs = require('fs-extra');
- const path = require('path');
- const unorm = require('unorm');
- const plist = require('plist');
- const URL = require('url');
- const events = require('cordova-common').events;
- const xmlHelpers = require('cordova-common').xmlHelpers;
- const ConfigParser = require('cordova-common').ConfigParser;
- const CordovaError = require('cordova-common').CordovaError;
- const PlatformJson = require('cordova-common').PlatformJson;
- const PlatformMunger = require('cordova-common').ConfigChanges.PlatformMunger;
- const PluginInfoProvider = require('cordova-common').PluginInfoProvider;
- const FileUpdater = require('cordova-common').FileUpdater;
- const projectFile = require('./projectFile');
-
- // launch storyboard and related constants
- const IMAGESET_COMPACT_SIZE_CLASS = 'compact';
- const CDV_ANY_SIZE_CLASS = 'any';
-
- module.exports.prepare = function (cordovaProject, options) {
- const platformJson = PlatformJson.load(this.locations.root, 'ios');
- const munger = new PlatformMunger('ios', this.locations.root, platformJson, new PluginInfoProvider());
-
- this._config = updateConfigFile(cordovaProject.projectConfig, munger, this.locations);
-
- // Update own www dir with project's www assets and plugins' assets and js-files
- return updateWww(cordovaProject, this.locations)
- // update project according to config.xml changes.
- .then(() => updateProject(this._config, this.locations))
- .then(() => {
- updateIcons(cordovaProject, this.locations);
- updateLaunchStoryboardImages(cordovaProject, this.locations);
- updateBackgroundColor(cordovaProject, this.locations);
- updateFileResources(cordovaProject, this.locations);
- })
- .then(() => {
- alertDeprecatedPreference(this._config);
- })
- .then(() => {
- events.emit('verbose', 'Prepared iOS project successfully');
- });
- };
-
- module.exports.clean = function (options) {
- // A cordovaProject isn't passed into the clean() function, because it might have
- // been called from the platform shell script rather than the CLI. Check for the
- // noPrepare option passed in by the non-CLI clean script. If that's present, or if
- // there's no config.xml found at the project root, then don't clean prepared files.
- const projectRoot = path.resolve(this.root, '../..');
- const projectConfigFile = path.join(projectRoot, 'config.xml');
- if ((options && options.noPrepare) || !fs.existsSync(projectConfigFile) ||
- !fs.existsSync(this.locations.configXml)) {
- return Promise.resolve();
- }
-
- const projectConfig = new ConfigParser(this.locations.configXml);
-
- return Promise.resolve().then(() => {
- cleanWww(projectRoot, this.locations);
- cleanIcons(projectRoot, projectConfig, this.locations);
- cleanLaunchStoryboardImages(projectRoot, projectConfig, this.locations);
- cleanBackgroundColor(projectRoot, projectConfig, this.locations);
- cleanFileResources(projectRoot, projectConfig, this.locations);
- });
- };
-
- /**
- * Updates config files in project based on app's config.xml and config munge,
- * generated by plugins.
- *
- * @param {ConfigParser} sourceConfig A project's configuration that will
- * be merged into platform's config.xml
- * @param {ConfigChanges} configMunger An initialized ConfigChanges instance
- * for this platform.
- * @param {Object} locations A map of locations for this platform
- *
- * @return {ConfigParser} An instance of ConfigParser, that
- * represents current project's configuration. When returned, the
- * configuration is already dumped to appropriate config.xml file.
- */
- function updateConfigFile (sourceConfig, configMunger, locations) {
- events.emit('verbose', `Generating platform-specific config.xml from defaults for iOS at ${locations.configXml}`);
-
- // First cleanup current config and merge project's one into own
- // Overwrite platform config.xml with defaults.xml.
- fs.copySync(locations.defaultConfigXml, locations.configXml);
-
- // Then apply config changes from global munge to all config files
- // in project (including project's config)
- configMunger.reapply_global_munge().save_all();
-
- events.emit('verbose', 'Merging project\'s config.xml into platform-specific iOS config.xml');
- // Merge changes from app's config.xml into platform's one
- const config = new ConfigParser(locations.configXml);
- xmlHelpers.mergeXml(sourceConfig.doc.getroot(),
- config.doc.getroot(), 'ios', /* clobber= */true);
-
- config.write();
- return config;
- }
-
- /**
- * Logs all file operations via the verbose event stream, indented.
- */
- function logFileOp (message) {
- events.emit('verbose', ` ${message}`);
- }
-
- /**
- * Updates platform 'www' directory by replacing it with contents of
- * 'platform_www' and app www. Also copies project's overrides' folder into
- * the platform 'www' folder
- *
- * @param {Object} cordovaProject An object which describes cordova project.
- * @param {boolean} destinations An object that contains destinations
- * paths for www files.
- */
- function updateWww (cordovaProject, destinations) {
- const sourceDirs = [
- path.relative(cordovaProject.root, cordovaProject.locations.www),
- path.relative(cordovaProject.root, destinations.platformWww)
- ];
-
- // If project contains 'merges' for our platform, use them as another overrides
- const merges_path = path.join(cordovaProject.root, 'merges', 'ios');
- if (fs.existsSync(merges_path)) {
- events.emit('verbose', 'Found "merges/ios" folder. Copying its contents into the iOS project.');
- sourceDirs.push(path.join('merges', 'ios'));
- }
-
- const targetDir = path.relative(cordovaProject.root, destinations.www);
- events.emit(
- 'verbose', `Merging and updating files from [${sourceDirs.join(', ')}] to ${targetDir}`);
- FileUpdater.mergeAndUpdateDir(
- sourceDirs, targetDir, { rootDir: cordovaProject.root }, logFileOp);
-
- return Promise.resolve();
- }
-
- /**
- * Cleans all files from the platform 'www' directory.
- */
- function cleanWww (projectRoot, locations) {
- const targetDir = path.relative(projectRoot, locations.www);
- events.emit('verbose', `Cleaning ${targetDir}`);
-
- // No source paths are specified, so mergeAndUpdateDir() will clear the target directory.
- FileUpdater.mergeAndUpdateDir(
- [], targetDir, { rootDir: projectRoot, all: true }, logFileOp);
- }
-
- /**
- * Updates project structure and AndroidManifest according to project's configuration.
- *
- * @param {ConfigParser} platformConfig A project's configuration that will
- * be used to update project
- * @param {Object} locations A map of locations for this platform (In/Out)
- */
- function updateProject (platformConfig, locations) {
- // CB-6992 it is necessary to normalize characters
- // because node and shell scripts handles unicode symbols differently
- // We need to normalize the name to NFD form since iOS uses NFD unicode form
- const name = unorm.nfd(platformConfig.name());
- const version = platformConfig.version();
- const displayName = platformConfig.shortName && platformConfig.shortName();
-
- const originalName = path.basename(locations.xcodeCordovaProj);
-
- // Update package id (bundle id)
- const plistFile = path.join(locations.xcodeCordovaProj, `${originalName}-Info.plist`);
- const infoPlist = plist.parse(fs.readFileSync(plistFile, 'utf8'));
-
- // Update version (bundle version)
- infoPlist.CFBundleShortVersionString = version;
- const CFBundleVersion = platformConfig.getAttribute('ios-CFBundleVersion') || default_CFBundleVersion(version);
- infoPlist.CFBundleVersion = CFBundleVersion;
-
- if (platformConfig.getAttribute('defaultlocale')) {
- infoPlist.CFBundleDevelopmentRegion = platformConfig.getAttribute('defaultlocale');
- }
-
- if (displayName) {
- infoPlist.CFBundleDisplayName = displayName;
- }
-
- // replace Info.plist ATS entries according to <access> and <allow-navigation> config.xml entries
- const ats = writeATSEntries(platformConfig);
- if (Object.keys(ats).length > 0) {
- infoPlist.NSAppTransportSecurity = ats;
- } else {
- delete infoPlist.NSAppTransportSecurity;
- }
-
- handleOrientationSettings(platformConfig, infoPlist);
-
- /* eslint-disable no-tabs */
- // Write out the plist file with the same formatting as Xcode does
- let info_contents = plist.build(infoPlist, { indent: '\t', offset: -1 });
- /* eslint-enable no-tabs */
-
- info_contents = info_contents.replace(/<string>[\s\r\n]*<\/string>/g, '<string></string>');
- fs.writeFileSync(plistFile, info_contents, 'utf-8');
- events.emit('verbose', `Wrote out iOS Bundle Version "${version}" to ${plistFile}`);
-
- return handleBuildSettings(platformConfig, locations, infoPlist).then(() => {
- if (name === originalName) {
- events.emit('verbose', `iOS Product Name has not changed (still "${originalName}")`);
- return Promise.resolve();
- } else { // CB-11712 <name> was changed, we don't support it'
- const errorString =
- 'The product name change (<name> tag) in config.xml is not supported dynamically.\n' +
- 'To change your product name, you have to remove, then add your ios platform again.\n' +
- 'Make sure you save your plugins beforehand using `cordova plugin save`.\n' +
- '\tcordova plugin save\n' +
- '\tcordova platform rm ios\n' +
- '\tcordova platform add ios\n';
-
- return Promise.reject(new CordovaError(errorString));
- }
- });
- }
-
- function handleOrientationSettings (platformConfig, infoPlist) {
- switch (getOrientationValue(platformConfig)) {
- case 'portrait':
- infoPlist.UIInterfaceOrientation = ['UIInterfaceOrientationPortrait'];
- infoPlist.UISupportedInterfaceOrientations = ['UIInterfaceOrientationPortrait', 'UIInterfaceOrientationPortraitUpsideDown'];
- infoPlist['UISupportedInterfaceOrientations~ipad'] = ['UIInterfaceOrientationPortrait', 'UIInterfaceOrientationPortraitUpsideDown'];
- break;
- case 'landscape':
- infoPlist.UIInterfaceOrientation = ['UIInterfaceOrientationLandscapeLeft'];
- infoPlist.UISupportedInterfaceOrientations = ['UIInterfaceOrientationLandscapeLeft', 'UIInterfaceOrientationLandscapeRight'];
- infoPlist['UISupportedInterfaceOrientations~ipad'] = ['UIInterfaceOrientationLandscapeLeft', 'UIInterfaceOrientationLandscapeRight'];
- break;
- case 'all':
- infoPlist.UIInterfaceOrientation = ['UIInterfaceOrientationPortrait'];
- infoPlist.UISupportedInterfaceOrientations = ['UIInterfaceOrientationPortrait', 'UIInterfaceOrientationPortraitUpsideDown', 'UIInterfaceOrientationLandscapeLeft', 'UIInterfaceOrientationLandscapeRight'];
- infoPlist['UISupportedInterfaceOrientations~ipad'] = ['UIInterfaceOrientationPortrait', 'UIInterfaceOrientationPortraitUpsideDown', 'UIInterfaceOrientationLandscapeLeft', 'UIInterfaceOrientationLandscapeRight'];
- break;
- case 'default':
- infoPlist.UISupportedInterfaceOrientations = ['UIInterfaceOrientationPortrait', 'UIInterfaceOrientationLandscapeLeft', 'UIInterfaceOrientationLandscapeRight'];
- infoPlist['UISupportedInterfaceOrientations~ipad'] = ['UIInterfaceOrientationPortrait', 'UIInterfaceOrientationPortraitUpsideDown', 'UIInterfaceOrientationLandscapeLeft', 'UIInterfaceOrientationLandscapeRight'];
- delete infoPlist.UIInterfaceOrientation;
- }
- }
-
- function handleBuildSettings (platformConfig, locations, infoPlist) {
- const pkg = platformConfig.getAttribute('ios-CFBundleIdentifier') || platformConfig.packageName();
- const targetDevice = parseTargetDevicePreference(platformConfig.getPreference('target-device', 'ios'));
- const deploymentTarget = platformConfig.getPreference('deployment-target', 'ios');
- const swiftVersion = platformConfig.getPreference('SwiftVersion', 'ios');
-
- let project;
-
- try {
- project = projectFile.parse(locations);
- } catch (err) {
- return Promise.reject(new CordovaError(`Could not parse ${locations.pbxproj}: ${err}`));
- }
-
- const origPkg = project.xcode.getBuildProperty('PRODUCT_BUNDLE_IDENTIFIER', undefined, platformConfig.name());
-
- // no build settings provided and we don't need to update build settings for launch storyboards,
- // then we don't need to parse and update .pbxproj file
- if (origPkg === pkg && !targetDevice && !deploymentTarget && !swiftVersion) {
- return Promise.resolve();
- }
-
- if (origPkg !== pkg) {
- events.emit('verbose', `Set PRODUCT_BUNDLE_IDENTIFIER to ${pkg}.`);
- project.xcode.updateBuildProperty('PRODUCT_BUNDLE_IDENTIFIER', pkg, null, platformConfig.name());
- }
-
- if (targetDevice) {
- events.emit('verbose', `Set TARGETED_DEVICE_FAMILY to ${targetDevice}.`);
- project.xcode.updateBuildProperty('TARGETED_DEVICE_FAMILY', targetDevice);
- }
-
- if (deploymentTarget) {
- events.emit('verbose', `Set IPHONEOS_DEPLOYMENT_TARGET to "${deploymentTarget}".`);
- project.xcode.updateBuildProperty('IPHONEOS_DEPLOYMENT_TARGET', deploymentTarget);
- }
-
- if (swiftVersion) {
- events.emit('verbose', `Set SwiftVersion to "${swiftVersion}".`);
- project.xcode.updateBuildProperty('SWIFT_VERSION', swiftVersion);
- }
-
- project.write();
-
- return Promise.resolve();
- }
-
- function mapIconResources (icons, iconsDir) {
- // See https://developer.apple.com/library/ios/documentation/UserExperience/Conceptual/MobileHIG/IconMatrix.html
- // for launch images sizes reference.
- const platformIcons = [
- { dest: 'icon-20.png', width: 20, height: 20 },
- { dest: 'icon-20@2x.png', width: 40, height: 40 },
- { dest: 'icon-20@3x.png', width: 60, height: 60 },
- { dest: 'icon-40.png', width: 40, height: 40 },
- { dest: 'icon-40@2x.png', width: 80, height: 80 },
- { dest: 'icon-50.png', width: 50, height: 50 },
- { dest: 'icon-50@2x.png', width: 100, height: 100 },
- { dest: 'icon-60@2x.png', width: 120, height: 120 },
- { dest: 'icon-60@3x.png', width: 180, height: 180 },
- { dest: 'icon-72.png', width: 72, height: 72 },
- { dest: 'icon-72@2x.png', width: 144, height: 144 },
- { dest: 'icon-76.png', width: 76, height: 76 },
- { dest: 'icon-76@2x.png', width: 152, height: 152 },
- { dest: 'icon-83.5@2x.png', width: 167, height: 167 },
- { dest: 'icon-1024.png', width: 1024, height: 1024 },
- { dest: 'icon-29.png', width: 29, height: 29 },
- { dest: 'icon-29@2x.png', width: 58, height: 58 },
- { dest: 'icon-29@3x.png', width: 87, height: 87 },
- { dest: 'icon.png', width: 57, height: 57 },
- { dest: 'icon@2x.png', width: 114, height: 114 },
- { dest: 'icon-24@2x.png', width: 48, height: 48 },
- { dest: 'icon-27.5@2x.png', width: 55, height: 55 },
- { dest: 'icon-44@2x.png', width: 88, height: 88 },
- { dest: 'icon-86@2x.png', width: 172, height: 172 },
- { dest: 'icon-98@2x.png', width: 196, height: 196 }
- ];
-
- const pathMap = {};
- platformIcons.forEach(item => {
- const icon = icons.getBySize(item.width, item.height) || icons.getDefault();
- if (icon) {
- const target = path.join(iconsDir, item.dest);
- pathMap[target] = icon.src;
- }
- });
- return pathMap;
- }
-
- function getIconsDir (projectRoot, platformProjDir) {
- let iconsDir;
- const xcassetsExists = folderExists(path.join(projectRoot, platformProjDir, 'Images.xcassets/'));
-
- if (xcassetsExists) {
- iconsDir = path.join(platformProjDir, 'Images.xcassets/AppIcon.appiconset/');
- } else {
- iconsDir = path.join(platformProjDir, 'Resources/icons/');
- }
-
- return iconsDir;
- }
-
- function updateIcons (cordovaProject, locations) {
- const icons = cordovaProject.projectConfig.getIcons('ios');
-
- if (icons.length === 0) {
- events.emit('verbose', 'This app does not have icons defined');
- return;
- }
-
- const platformProjDir = path.relative(cordovaProject.root, locations.xcodeCordovaProj);
- const iconsDir = getIconsDir(cordovaProject.root, platformProjDir);
- const resourceMap = mapIconResources(icons, iconsDir);
- events.emit('verbose', `Updating icons at ${iconsDir}`);
- FileUpdater.updatePaths(
- resourceMap, { rootDir: cordovaProject.root }, logFileOp);
- }
-
- function cleanIcons (projectRoot, projectConfig, locations) {
- const icons = projectConfig.getIcons('ios');
- if (icons.length > 0) {
- const platformProjDir = path.relative(projectRoot, locations.xcodeCordovaProj);
- const iconsDir = getIconsDir(projectRoot, platformProjDir);
- const resourceMap = mapIconResources(icons, iconsDir);
- Object.keys(resourceMap).forEach(targetIconPath => {
- resourceMap[targetIconPath] = null;
- });
- events.emit('verbose', `Cleaning icons at ${iconsDir}`);
-
- // Source paths are removed from the map, so updatePaths() will delete the target files.
- FileUpdater.updatePaths(
- resourceMap, { rootDir: projectRoot, all: true }, logFileOp);
- }
- }
-
- /**
- * Returns the directory for the BackgroundColor.colorset asset, or null if no
- * xcassets exist.
- *
- * @param {string} projectRoot The project's root directory
- * @param {string} platformProjDir The platform's project directory
- */
- function getBackgroundColorDir (projectRoot, platformProjDir) {
- if (folderExists(path.join(projectRoot, platformProjDir, 'Images.xcassets/'))) {
- return path.join(platformProjDir, 'Images.xcassets', 'BackgroundColor.colorset');
- } else {
- return null;
- }
- }
-
- function colorPreferenceToComponents (pref) {
- if (!pref || !pref.match(/^(#[0-9A-F]{3}|(0x|#)([0-9A-F]{2})?[0-9A-F]{6})$/)) {
- return {
- platform: 'ios',
- reference: 'systemBackgroundColor'
- };
- }
-
- let red = 'FF';
- let green = 'FF';
- let blue = 'FF';
- let alpha = 1.0;
-
- if (pref[0] === '#' && pref.length === 4) {
- red = pref[1] + pref[1];
- green = pref[2] + pref[2];
- blue = pref[3] + pref[3];
- }
-
- if (pref.length >= 7 && (pref[0] === '#' || pref.substring(0, 2) === '0x')) {
- let offset = pref[0] === '#' ? 1 : 2;
-
- if (pref.substring(offset).length === 8) {
- alpha = parseInt(pref.substring(offset, offset + 2), 16) / 255.0;
- offset += 2;
- }
-
- red = pref.substring(offset, offset + 2);
- green = pref.substring(offset + 2, offset + 4);
- blue = pref.substring(offset + 4, offset + 6);
- }
-
- return {
- 'color-space': 'srgb',
- components: {
- red: '0x' + red,
- green: '0x' + green,
- blue: '0x' + blue,
- alpha: alpha.toFixed(3)
- }
- };
- }
-
- /**
- * Update the background color Contents.json in xcassets.
- *
- * @param {Object} cordovaProject The cordova project
- * @param {Object} locations A dictionary containing useful location paths
- */
- function updateBackgroundColor (cordovaProject, locations) {
- const pref = cordovaProject.projectConfig.getPreference('BackgroundColor', 'ios') || '';
-
- const platformProjDir = path.relative(cordovaProject.root, locations.xcodeCordovaProj);
- const backgroundColorDir = getBackgroundColorDir(cordovaProject.root, platformProjDir);
-
- if (backgroundColorDir) {
- const contentsJSON = {
- colors: [{
- idiom: 'universal',
- color: colorPreferenceToComponents(pref)
- }],
- info: {
- author: 'Xcode',
- version: 1
- }
- };
-
- events.emit('verbose', 'Updating Background Color color set Contents.json');
- fs.writeFileSync(path.join(cordovaProject.root, backgroundColorDir, 'Contents.json'),
- JSON.stringify(contentsJSON, null, 2));
- }
- }
-
- /**
- * Resets the background color Contents.json in xcassets to default.
- *
- * @param {string} projectRoot Path to the project root
- * @param {Object} projectConfig The project's config.xml
- * @param {Object} locations A dictionary containing useful location paths
- */
- function cleanBackgroundColor (projectRoot, projectConfig, locations) {
- const platformProjDir = path.relative(projectRoot, locations.xcodeCordovaProj);
- const backgroundColorDir = getBackgroundColorDir(projectRoot, platformProjDir);
-
- if (backgroundColorDir) {
- const contentsJSON = {
- colors: [{
- idiom: 'universal',
- color: colorPreferenceToComponents(null)
- }],
- info: {
- author: 'Xcode',
- version: 1
- }
- };
-
- events.emit('verbose', 'Cleaning Background Color color set Contents.json');
- fs.writeFileSync(path.join(projectRoot, backgroundColorDir, 'Contents.json'),
- JSON.stringify(contentsJSON, null, 2));
- }
- }
-
- function updateFileResources (cordovaProject, locations) {
- const platformDir = path.relative(cordovaProject.root, locations.root);
- const files = cordovaProject.projectConfig.getFileResources('ios');
-
- const project = projectFile.parse(locations);
-
- // if there are resource-file elements in config.xml
- if (files.length === 0) {
- events.emit('verbose', 'This app does not have additional resource files defined');
- return;
- }
-
- const resourceMap = {};
- files.forEach(res => {
- const src = res.src;
- let target = res.target;
-
- if (!target) {
- target = src;
- }
-
- let targetPath = path.join(project.resources_dir, target);
- targetPath = path.relative(cordovaProject.root, targetPath);
-
- if (!fs.existsSync(targetPath)) {
- project.xcode.addResourceFile(target);
- } else {
- events.emit('warn', `Overwriting existing resource file at ${targetPath}`);
- }
-
- resourceMap[targetPath] = src;
- });
-
- events.emit('verbose', `Updating resource files at ${platformDir}`);
- FileUpdater.updatePaths(
- resourceMap, { rootDir: cordovaProject.root }, logFileOp);
-
- project.write();
- }
-
- function alertDeprecatedPreference (configParser) {
- const deprecatedToNewPreferences = {
- MediaPlaybackRequiresUserAction: {
- newPreference: 'MediaTypesRequiringUserActionForPlayback',
- isDeprecated: true
- },
- MediaPlaybackAllowsAirPlay: {
- newPreference: 'AllowsAirPlayForMediaPlayback',
- isDeprecated: false
- }
- };
-
- Object.keys(deprecatedToNewPreferences).forEach(oldKey => {
- if (configParser.getPreference(oldKey)) {
- const isDeprecated = deprecatedToNewPreferences[oldKey].isDeprecated;
- const verb = isDeprecated ? 'has been' : 'is being';
- const newPreferenceKey = deprecatedToNewPreferences[oldKey].newPreference;
-
- // Create the Log Message
- const log = [`The preference name "${oldKey}" ${verb} deprecated.`];
- if (newPreferenceKey) {
- log.push(`It is recommended to replace this preference with "${newPreferenceKey}."`);
- } else {
- log.push('There is no replacement for this preference.');
- }
-
- /**
- * If the preference has been deprecated, the usage of the old preference is no longer used.
- * Therefore, the following line is not appended. It is added only if the old preference is still used.
- * We are only keeping the top lines for deprecated items only for an additional major release when
- * the pre-warning was not provided in a past major release due to a necessary quick deprecation.
- * Typically caused by implementation nature or third-party requirement changes.
- */
- if (!isDeprecated) {
- log.push('Please note that this preference will be removed in the near future.');
- }
-
- events.emit('warn', log.join(' '));
- }
- });
- }
-
- function cleanFileResources (projectRoot, projectConfig, locations) {
- const platformDir = path.relative(projectRoot, locations.root);
- const files = projectConfig.getFileResources('ios', true);
- if (files.length > 0) {
- events.emit('verbose', `Cleaning resource files at ${platformDir}`);
-
- const project = projectFile.parse(locations);
-
- const resourceMap = {};
- files.forEach(res => {
- const src = res.src;
- let target = res.target;
-
- if (!target) {
- target = src;
- }
-
- let targetPath = path.join(project.resources_dir, target);
- targetPath = path.relative(projectRoot, targetPath);
- const resfile = path.join('Resources', path.basename(targetPath));
- project.xcode.removeResourceFile(resfile);
-
- resourceMap[targetPath] = null;
- });
-
- FileUpdater.updatePaths(
- resourceMap, { rootDir: projectRoot, all: true }, logFileOp);
-
- project.write();
- }
- }
-
- /**
- * Returns an array of images for each possible idiom, scale, and size class. The images themselves are
- * located in the platform's splash images by their pattern (@scale~idiom~sizesize). All possible
- * combinations are returned, but not all will have a `filename` property. If the latter isn't present,
- * the device won't attempt to load an image matching the same traits. If the filename is present,
- * the device will try to load the image if it corresponds to the traits.
- *
- * The resulting return looks like this:
- *
- * [
- * {
- * idiom: 'universal|ipad|iphone',
- * scale: '1x|2x|3x',
- * width: 'any|com',
- * height: 'any|com',
- * filename: undefined|'Default@scale~idiom~widthheight.png',
- * src: undefined|'path/to/original/matched/image/from/splash/screens.png',
- * target: undefined|'path/to/asset/library/Default@scale~idiom~widthheight.png',
- * appearence: undefined|'dark'|'light'
- * }, ...
- * ]
- *
- * @param {Array<Object>} splashScreens splash screens as defined in config.xml for this platform
- * @param {string} launchStoryboardImagesDir project-root/Images.xcassets/LaunchStoryboard.imageset/
- * @return {Array<Object>}
- */
- function mapLaunchStoryboardContents (splashScreens, launchStoryboardImagesDir) {
- const platformLaunchStoryboardImages = [];
- const idioms = ['universal', 'ipad', 'iphone'];
- const scalesForIdiom = {
- universal: ['1x', '2x', '3x'],
- ipad: ['1x', '2x'],
- iphone: ['1x', '2x', '3x']
- };
- const sizes = ['com', 'any'];
- const appearences = ['', 'dark', 'light'];
-
- idioms.forEach(idiom => {
- scalesForIdiom[idiom].forEach(scale => {
- sizes.forEach(width => {
- sizes.forEach(height => {
- appearences.forEach(appearence => {
- const item = { idiom, scale, width, height };
-
- if (appearence !== '') {
- item.appearence = appearence;
- }
-
- /* examples of the search pattern:
- * scale ~ idiom ~ width height ~ appearence
- * @2x ~ universal ~ any any
- * @3x ~ iphone ~ com any ~ dark
- * @2x ~ ipad ~ com any ~ light
- */
- const searchPattern = '@' + scale + '~' + idiom + '~' + width + height + (appearence ? '~' + appearence : '');
-
- /* because old node versions don't have Array.find, the below is
- * functionally equivalent to this:
- * var launchStoryboardImage = splashScreens.find(function(item) {
- * return (item.src.indexOf(searchPattern) >= 0) ? (appearence !== '' ? true : ((item.src.indexOf(searchPattern + '~light') >= 0 || (item.src.indexOf(searchPattern + '~dark') >= 0)) ? false : true)) : false;
- * });
- */
- const launchStoryboardImage = splashScreens.reduce(
- (p, c) => (c.src.indexOf(searchPattern) >= 0) ? (appearence !== '' ? c : ((c.src.indexOf(searchPattern + '~light') >= 0 || (c.src.indexOf(searchPattern + '~dark') >= 0)) ? p : c)) : p,
- undefined
- );
-
- if (launchStoryboardImage) {
- item.filename = `Default${searchPattern}.png`;
- item.src = launchStoryboardImage.src;
- item.target = path.join(launchStoryboardImagesDir, item.filename);
- }
-
- platformLaunchStoryboardImages.push(item);
- });
- });
- });
- });
- });
- return platformLaunchStoryboardImages;
- }
-
- /**
- * Returns a dictionary representing the source and destination paths for the launch storyboard images
- * that need to be copied.
- *
- * The resulting return looks like this:
- *
- * {
- * 'target-path': 'source-path',
- * ...
- * }
- *
- * @param {Array<Object>} splashScreens splash screens as defined in config.xml for this platform
- * @param {string} launchStoryboardImagesDir project-root/Images.xcassets/LaunchStoryboard.imageset/
- * @return {Object}
- */
- function mapLaunchStoryboardResources (splashScreens, launchStoryboardImagesDir) {
- const platformLaunchStoryboardImages = mapLaunchStoryboardContents(splashScreens, launchStoryboardImagesDir);
- const pathMap = {};
- platformLaunchStoryboardImages.forEach(item => {
- if (item.target) {
- pathMap[item.target] = item.src;
- }
- });
- return pathMap;
- }
-
- /**
- * Builds the object that represents the contents.json file for the LaunchStoryboard image set.
- *
- * The resulting return looks like this:
- *
- * {
- * images: [
- * {
- * idiom: 'universal|ipad|iphone',
- * scale: '1x|2x|3x',
- * width-class: undefined|'compact',
- * height-class: undefined|'compact'
- * ...
- * }, ...
- * ],
- * info: {
- * author: 'Xcode',
- * version: 1
- * }
- * }
- *
- * A bit of minor logic is used to map from the array of images returned from mapLaunchStoryboardContents
- * to the format requried by Xcode.
- *
- * @param {Array<Object>} splashScreens splash screens as defined in config.xml for this platform
- * @param {string} launchStoryboardImagesDir project-root/Images.xcassets/LaunchStoryboard.imageset/
- * @return {Object}
- */
- function getLaunchStoryboardContentsJSON (splashScreens, launchStoryboardImagesDir) {
- const platformLaunchStoryboardImages = mapLaunchStoryboardContents(splashScreens, launchStoryboardImagesDir);
- const contentsJSON = {
- images: [],
- info: {
- author: 'Xcode',
- version: 1
- }
- };
- contentsJSON.images = platformLaunchStoryboardImages.map(item => {
- const newItem = {
- idiom: item.idiom,
- scale: item.scale
- };
-
- // Xcode doesn't want any size class property if the class is "any"
- // If our size class is "com", Xcode wants "compact".
- if (item.width !== CDV_ANY_SIZE_CLASS) {
- newItem['width-class'] = IMAGESET_COMPACT_SIZE_CLASS;
- }
- if (item.height !== CDV_ANY_SIZE_CLASS) {
- newItem['height-class'] = IMAGESET_COMPACT_SIZE_CLASS;
- }
-
- if (item.appearence) {
- newItem.appearances = [{ appearance: 'luminosity', value: item.appearence }];
- }
-
- // Xcode doesn't want a filename property if there's no image for these traits
- if (item.filename) {
- newItem.filename = item.filename;
- }
- return newItem;
- });
- return contentsJSON;
- }
-
- /**
- * Returns the directory for the Launch Storyboard image set, if image sets are being used. If they aren't
- * being used, returns null.
- *
- * @param {string} projectRoot The project's root directory
- * @param {string} platformProjDir The platform's project directory
- */
- function getLaunchStoryboardImagesDir (projectRoot, platformProjDir) {
- let launchStoryboardImagesDir;
- const xcassetsExists = folderExists(path.join(projectRoot, platformProjDir, 'Images.xcassets/'));
-
- if (xcassetsExists) {
- launchStoryboardImagesDir = path.join(platformProjDir, 'Images.xcassets/LaunchStoryboard.imageset/');
- } else {
- // if we don't have a asset library for images, we can't do the storyboard.
- launchStoryboardImagesDir = null;
- }
-
- return launchStoryboardImagesDir;
- }
-
- /**
- * Update the images for the Launch Storyboard and updates the image set's contents.json file appropriately.
- *
- * @param {Object} cordovaProject The cordova project
- * @param {Object} locations A dictionary containing useful location paths
- */
- function updateLaunchStoryboardImages (cordovaProject, locations) {
- const splashScreens = cordovaProject.projectConfig.getSplashScreens('ios');
- const platformProjDir = path.relative(cordovaProject.root, locations.xcodeCordovaProj);
- const launchStoryboardImagesDir = getLaunchStoryboardImagesDir(cordovaProject.root, platformProjDir);
-
- if (launchStoryboardImagesDir) {
- const resourceMap = mapLaunchStoryboardResources(splashScreens, launchStoryboardImagesDir);
- const contentsJSON = getLaunchStoryboardContentsJSON(splashScreens, launchStoryboardImagesDir);
-
- events.emit('verbose', `Updating launch storyboard images at ${launchStoryboardImagesDir}`);
- FileUpdater.updatePaths(
- resourceMap, { rootDir: cordovaProject.root }, logFileOp);
-
- events.emit('verbose', 'Updating Storyboard image set contents.json');
- fs.writeFileSync(path.join(cordovaProject.root, launchStoryboardImagesDir, 'Contents.json'),
- JSON.stringify(contentsJSON, null, 2));
- }
- }
-
- /**
- * Removes the images from the launch storyboard's image set and updates the image set's contents.json
- * file appropriately.
- *
- * @param {string} projectRoot Path to the project root
- * @param {Object} projectConfig The project's config.xml
- * @param {Object} locations A dictionary containing useful location paths
- */
- function cleanLaunchStoryboardImages (projectRoot, projectConfig, locations) {
- const splashScreens = projectConfig.getSplashScreens('ios');
- const platformProjDir = path.relative(projectRoot, locations.xcodeCordovaProj);
- const launchStoryboardImagesDir = getLaunchStoryboardImagesDir(projectRoot, platformProjDir);
- if (launchStoryboardImagesDir) {
- const resourceMap = mapLaunchStoryboardResources(splashScreens, launchStoryboardImagesDir);
- const contentsJSON = getLaunchStoryboardContentsJSON(splashScreens, launchStoryboardImagesDir);
-
- Object.keys(resourceMap).forEach(targetPath => {
- resourceMap[targetPath] = null;
- });
- events.emit('verbose', `Cleaning storyboard image set at ${launchStoryboardImagesDir}`);
-
- // Source paths are removed from the map, so updatePaths() will delete the target files.
- FileUpdater.updatePaths(
- resourceMap, { rootDir: projectRoot, all: true }, logFileOp);
-
- // delete filename from contents.json
- contentsJSON.images.forEach(image => {
- image.filename = undefined;
- });
-
- events.emit('verbose', 'Updating Storyboard image set contents.json');
- fs.writeFileSync(path.join(projectRoot, launchStoryboardImagesDir, 'Contents.json'),
- JSON.stringify(contentsJSON, null, 2));
- }
- }
-
- /**
- * Queries ConfigParser object for the orientation <preference> value. Warns if
- * global preference value is not supported by platform.
- *
- * @param {Object} platformConfig ConfigParser object
- *
- * @return {String} Global/platform-specific orientation in lower-case
- * (or empty string if both are undefined).
- */
- function getOrientationValue (platformConfig) {
- const ORIENTATION_DEFAULT = 'default';
-
- let orientation = platformConfig.getPreference('orientation');
- if (!orientation) {
- return '';
- }
-
- orientation = orientation.toLowerCase();
-
- // Check if the given global orientation is supported
- if (['default', 'portrait', 'landscape', 'all'].indexOf(orientation) >= 0) {
- return orientation;
- }
-
- events.emit('warn', `Unrecognized value for Orientation preference: ${orientation}. Defaulting to value: ${ORIENTATION_DEFAULT}.`);
-
- return ORIENTATION_DEFAULT;
- }
-
- /*
- Parses all <access> and <allow-navigation> entries and consolidates duplicates (for ATS).
- Returns an object with a Hostname as the key, and the value an object with properties:
- {
- Hostname, // String
- NSExceptionAllowsInsecureHTTPLoads, // boolean
- NSIncludesSubdomains, // boolean
- NSExceptionMinimumTLSVersion, // String
- NSExceptionRequiresForwardSecrecy, // boolean
- NSRequiresCertificateTransparency, // boolean
-
- // the three below _only_ show when the Hostname is '*'
- // if any of the three are set, it disables setting NSAllowsArbitraryLoads
- // (Apple already enforces this in ATS)
- NSAllowsArbitraryLoadsInWebContent, // boolean (default: false)
- NSAllowsLocalNetworking, // boolean (default: false)
- NSAllowsArbitraryLoadsForMedia, // boolean (default:false)
-
- }
- */
- function processAccessAndAllowNavigationEntries (config) {
- const accesses = config.getAccesses();
- const allow_navigations = config.getAllowNavigations();
-
- return allow_navigations
- // we concat allow_navigations and accesses, after processing accesses
- .concat(accesses.map(obj => {
- // map accesses to a common key interface using 'href', not origin
- obj.href = obj.origin;
- delete obj.origin;
- return obj;
- }))
- // we reduce the array to an object with all the entries processed (key is Hostname)
- .reduce((previousReturn, currentElement) => {
- const options = {
- minimum_tls_version: currentElement.minimum_tls_version,
- requires_forward_secrecy: currentElement.requires_forward_secrecy,
- requires_certificate_transparency: currentElement.requires_certificate_transparency,
- allows_arbitrary_loads_for_media: currentElement.allows_arbitrary_loads_in_media || currentElement.allows_arbitrary_loads_for_media,
- allows_arbitrary_loads_in_web_content: currentElement.allows_arbitrary_loads_in_web_content,
- allows_local_networking: currentElement.allows_local_networking
- };
- const obj = parseWhitelistUrlForATS(currentElement.href, options);
-
- if (obj) {
- // we 'union' duplicate entries
- let item = previousReturn[obj.Hostname];
- if (!item) {
- item = {};
- }
- for (const o in obj) {
- if (Object.prototype.hasOwnProperty.call(obj, o)) {
- item[o] = obj[o];
- }
- }
- previousReturn[obj.Hostname] = item;
- }
- return previousReturn;
- }, {});
- }
-
- /*
- Parses a URL and returns an object with these keys:
- {
- Hostname, // String
- NSExceptionAllowsInsecureHTTPLoads, // boolean (default: false)
- NSIncludesSubdomains, // boolean (default: false)
- NSExceptionMinimumTLSVersion, // String (default: 'TLSv1.2')
- NSExceptionRequiresForwardSecrecy, // boolean (default: true)
- NSRequiresCertificateTransparency, // boolean (default: false)
-
- // the three below _only_ apply when the Hostname is '*'
- // if any of the three are set, it disables setting NSAllowsArbitraryLoads
- // (Apple already enforces this in ATS)
- NSAllowsArbitraryLoadsInWebContent, // boolean (default: false)
- NSAllowsLocalNetworking, // boolean (default: false)
- NSAllowsArbitraryLoadsForMedia, // boolean (default:false)
- }
-
- null is returned if the URL cannot be parsed, or is to be skipped for ATS.
- */
- function parseWhitelistUrlForATS (url, options) {
- // @todo 'url.parse' was deprecated since v11.0.0. Use 'url.URL' constructor instead.
- const href = URL.parse(url); // eslint-disable-line
- const retObj = {};
- retObj.Hostname = href.hostname;
-
- // Guiding principle: we only set values in retObj if they are NOT the default
-
- if (url === '*') {
- retObj.Hostname = '*';
- let val;
-
- val = (options.allows_arbitrary_loads_in_web_content === 'true');
- if (options.allows_arbitrary_loads_in_web_content && val) { // default is false
- retObj.NSAllowsArbitraryLoadsInWebContent = true;
- }
-
- val = (options.allows_arbitrary_loads_for_media === 'true');
- if (options.allows_arbitrary_loads_for_media && val) { // default is false
- retObj.NSAllowsArbitraryLoadsForMedia = true;
- }
-
- val = (options.allows_local_networking === 'true');
- if (options.allows_local_networking && val) { // default is false
- retObj.NSAllowsLocalNetworking = true;
- }
-
- return retObj;
- }
-
- if (!retObj.Hostname) {
- // check origin, if it allows subdomains (wildcard in hostname), we set NSIncludesSubdomains to YES. Default is NO
- const subdomain1 = '/*.'; // wildcard in hostname
- const subdomain2 = '*://*.'; // wildcard in hostname and protocol
- const subdomain3 = '*://'; // wildcard in protocol only
- if (!href.pathname) {
- return null;
- } else if (href.pathname.indexOf(subdomain1) === 0) {
- retObj.NSIncludesSubdomains = true;
- retObj.Hostname = href.pathname.substring(subdomain1.length);
- } else if (href.pathname.indexOf(subdomain2) === 0) {
- retObj.NSIncludesSubdomains = true;
- retObj.Hostname = href.pathname.substring(subdomain2.length);
- } else if (href.pathname.indexOf(subdomain3) === 0) {
- retObj.Hostname = href.pathname.substring(subdomain3.length);
- } else {
- // Handling "scheme:*" case to avoid creating of a blank key in NSExceptionDomains.
- return null;
- }
- }
-
- if (options.minimum_tls_version && options.minimum_tls_version !== 'TLSv1.2') { // default is TLSv1.2
- retObj.NSExceptionMinimumTLSVersion = options.minimum_tls_version;
- }
-
- const rfs = (options.requires_forward_secrecy === 'true');
- if (options.requires_forward_secrecy && !rfs) { // default is true
- retObj.NSExceptionRequiresForwardSecrecy = false;
- }
-
- const rct = (options.requires_certificate_transparency === 'true');
- if (options.requires_certificate_transparency && rct) { // default is false
- retObj.NSRequiresCertificateTransparency = true;
- }
-
- // if the scheme is HTTP, we set NSExceptionAllowsInsecureHTTPLoads to YES. Default is NO
- if (href.protocol === 'http:') {
- retObj.NSExceptionAllowsInsecureHTTPLoads = true;
- } else if (!href.protocol && href.pathname.indexOf('*:/') === 0) { // wilcard in protocol
- retObj.NSExceptionAllowsInsecureHTTPLoads = true;
- }
-
- return retObj;
- }
-
- /*
- App Transport Security (ATS) writer from <access> and <allow-navigation> tags
- in config.xml
- */
- function writeATSEntries (config) {
- const pObj = processAccessAndAllowNavigationEntries(config);
-
- const ats = {};
-
- for (const hostname in pObj) {
- if (Object.prototype.hasOwnProperty.call(pObj, hostname)) {
- const entry = pObj[hostname];
-
- // Guiding principle: we only set values if they are available
-
- if (hostname === '*') {
- // always write this, for iOS 9, since in iOS 10 it will be overriden if
- // any of the other three keys are written
- ats.NSAllowsArbitraryLoads = true;
-
- // at least one of the overriding keys is present
- if (entry.NSAllowsArbitraryLoadsInWebContent) {
- ats.NSAllowsArbitraryLoadsInWebContent = true;
- }
- if (entry.NSAllowsArbitraryLoadsForMedia) {
- ats.NSAllowsArbitraryLoadsForMedia = true;
- }
- if (entry.NSAllowsLocalNetworking) {
- ats.NSAllowsLocalNetworking = true;
- }
-
- continue;
- }
-
- const exceptionDomain = {};
-
- for (const key in entry) {
- if (Object.prototype.hasOwnProperty.call(entry, key) && key !== 'Hostname') {
- exceptionDomain[key] = entry[key];
- }
- }
-
- if (!ats.NSExceptionDomains) {
- ats.NSExceptionDomains = {};
- }
-
- ats.NSExceptionDomains[hostname] = exceptionDomain;
- }
- }
-
- return ats;
- }
-
- function folderExists (folderPath) {
- try {
- const stat = fs.statSync(folderPath);
- return stat && stat.isDirectory();
- } catch (e) {
- return false;
- }
- }
-
- // Construct a default value for CFBundleVersion as the version with any
- // -rclabel stripped=.
- function default_CFBundleVersion (version) {
- return version.split('-')[0];
- }
-
- // Converts cordova specific representation of target device to XCode value
- function parseTargetDevicePreference (value) {
- if (!value) return null;
- const map = { universal: '"1,2"', handset: '"1"', tablet: '"2"' };
- if (map[value.toLowerCase()]) {
- return map[value.toLowerCase()];
- }
- events.emit('warn', `Unrecognized value for target-device preference: ${value}.`);
- return null;
- }
|