ioslib
Version:
iOS Utility Library
414 lines (357 loc) • 12.1 kB
JavaScript
/**
* Detects provisioning profiles.
*
* @module provisioning
*
* @copyright
* Copyright (c) 2014-2016 by Appcelerator, Inc. All Rights Reserved.
*
* @license
* Licensed under the terms of the Apache Public License.
* Please see the LICENSE included with this distribution for details.
*
* @requires certs
*/
const
appc = require('node-appc'),
certs = require('./certs'),
magik = require('./utilities').magik,
fs = require('fs'),
path = require('path'),
__ = appc.i18n(__dirname).__,
provisioningProfilesDirectories = [
'~/Library/Developer/Xcode/UserData/Provisioning Profiles',
'~/Library/MobileDevice/Provisioning Profiles'
]
var cache = null,
watchers = {};
/**
* Fired when the provisioning profiles have been detected or updated.
* @event module:provisioning#detected
* @type {Object}
*/
/**
* Fired when there was an error retreiving the provisioning profiles.
* @event module:provisioning#error
* @type {Error}
*/
exports.detect = detect;
exports.find = find;
exports.watch = watch;
exports.unwatch = unwatch;
/**
* Detects installed provisioning profiles.
*
* @param {Object} [options] - An object containing various settings.
* @param {Boolean} [options.bypassCache=false] - When true, re-detects all provisioning profiles.
* @param {String} [options.profileDir=~/Library/Developer/Xcode/UserData/Provisioning Profiles] - The path to search for provisioning profiles.
* @param {Boolean} [options.unmanaged] - When true, excludes managed provisioning profiles.
* @param {Boolean} [options.validOnly=true] - When true, only returns non-expired, valid provisioning profiles.
* @param {Boolean} [options.watch=false] - If true, watches the specified provisioning profile directory for updates.
* @param {Function} [callback(err, results)] - A function to call with the provisioning profile information.
*
* @emits module:provisioning#detected
* @emits module:provisioning#error
*
* @returns {Handle}
*/
function detect(options, callback) {
return magik(options, callback, function (emitter, options, callback) {
var files = {},
validOnly = options.validOnly === undefined || options.validOnly === true,
profileDirs = getExistingProvisioningProfileDirectories(options.profileDir),
results = {
provisioning: {
profileDir: profileDirs[0],
development: [],
adhoc: [],
enterprise: [],
distribution: [],
},
issues: []
},
valid = {
development: 0,
adhoc: 0,
enterprise: 0,
distribution: 0
},
ppRegExp = /.*\.(mobileprovision|provisionprofile)$/;
if (options.watch) {
var throttleTimer = null;
for (const profileDir of profileDirs) {
if (!watchers[profileDir]) {
watchers[profileDir] = {
handle: fs.watch(profileDir, { persistent: false }, function (event, filename) {
if (!ppRegExp.test(filename)) {
// if it's not a provisioning profile, we don't care about it
return;
}
var file = path.join(profileDir, filename);
if (event === 'rename') {
if (files[file]) {
if (fs.existsSync(file)) {
// change, reload the provisioning profile
parseProfile(file);
} else {
// delete
removeProfile(file);
}
} else {
// add
parseProfile(file);
}
} else if (event === 'change') {
// updated
parseProfile(file);
}
clearTimeout(throttleTimer);
throttleTimer = setTimeout(function () {
detectIssues();
emitter.emit('detected', results);
}, 250);
}),
count: 0
};
}
watchers[profileDir].count++;
}
}
if (cache && !options.bypassCache) {
emitter.emit('detected', cache);
return callback(null, cache);
}
function detectIssues() {
results.issues = [];
if (results.provisioning.development.length > 0 && !valid.development) {
results.issues.push({
id: 'IOS_NO_VALID_DEVELOPMENT_PROVISIONING_PROFILES',
type: 'warning',
message: __('Unable to find any valid iOS development provisioning profiles.') + '\n' +
__('This will prevent you from building apps for testing on iOS devices.')
});
}
if (results.provisioning.adhoc.length > 0 && !valid.adhoc) {
results.issues.push({
id: 'IOS_NO_VALID_ADHOC_PROVISIONING_PROFILES',
type: 'warning',
message: __('Unable to find any valid iOS adhoc provisioning profiles.') + '\n' +
__('This will prevent you from packaging apps for adhoc distribution.')
});
}
if (results.provisioning.distribution.length > 0 && !valid.distribution) {
results.issues.push({
id: 'IOS_NO_VALID_DISTRIBUTION_PROVISIONING_PROFILES',
type: 'warning',
message: __('Unable to find any valid iOS distribution provisioning profiles.') + '\n' +
__('This will prevent you from packaging apps for AppStore distribution.')
});
}
}
function removeProfile(file) {
var r = results[files[file]],
i = 0,
l = r.length;
for (; i < l; i++) {
if (r[i].file === file) {
r.splice(i, 1);
break;
}
}
delete files[file];
}
function parseProfile(file) {
if (!fs.existsSync(file)) {
return;
}
var contents = fs.readFileSync(file).toString(),
i = contents.indexOf('<?xml'),
j = i === -1 ? i : contents.lastIndexOf('</plist>');
if (j === -1) return;
var plist = new appc.plist().parse(contents.substring(i, j + 8)),
dest = 'development', // debug
appPrefix = (plist.ApplicationIdentifierPrefix || []).shift(),
entitlements = plist.Entitlements || {},
expired = false;
if (plist.ProvisionedDevices) {
if (!entitlements['get-task-allow']) {
dest = 'adhoc';
}
} else if (plist.ProvisionsAllDevices) {
dest = 'enterprise';
} else {
dest = 'distribution'; // app store
}
try {
if (plist.ExpirationDate) {
expired = new Date(plist.ExpirationDate) < new Date;
}
} catch (e) {}
if (!expired) {
valid[dest]++;
}
// store which bucket the provisioning profile is in
files[file] && removeProfile(file);
files[file] = dest;
var managed = plist.Name.indexOf('iOS Team Provisioning Profile') !== -1;
if ((!validOnly || !expired) && (!options.unmanaged || !managed)) {
results.provisioning[dest].push({
file: file,
uuid: plist.UUID,
name: plist.Name,
managed: managed,
appPrefix: appPrefix,
creationDate: plist.CreationDate,
expirationDate: plist.ExpirationDate,
expired: expired,
certs: Array.isArray(plist.DeveloperCertificates)
? plist.DeveloperCertificates.map(function (cert) { return cert.value; })
: null,
devices: plist.ProvisionedDevices || null,
team: plist.TeamIdentifier || null,
entitlements: entitlements,
// TODO: remove all of the entitlements below and just use the `entitlements` property
appId: (entitlements['application-identifier'] || entitlements['com.apple.application-identifier'] || '').replace(appPrefix + '.', ''),
getTaskAllow: !!entitlements['get-task-allow'],
apsEnvironment: entitlements['aps-environment'] || ''
});
}
}
for (const profileDir of profileDirs) {
fs.readdirSync(profileDir).forEach(function (name) {
ppRegExp.test(name) && parseProfile(path.join(profileDir, name));
});
}
detectIssues();
cache = results;
emitter.emit('detected', results);
return callback(null, results);
});
};
/**
* Finds all provisioning profiles that match the specified developer cert name
* and iOS device UDID.
*
* @param {Object} [options] - An object containing various settings.
* @param {String} [options.appId] - The app identifier (com.domain.app) to filter by.
* @param {Object|Array<Object>} [options.certs] - One or more certificate descriptors to filter by.
* @param {String|Array<String>} [options.deviceUDIDs] - One or more iOS device UDIDs to filter by.
* @param {Boolean} [options.unmanaged] - When true, excludes managed provisioning profiles.
* @param {Boolean} [options.validOnly=true] - When true, only returns valid profiles.
* @param {Function} callback(err, results) - A function to call with an array of matching provisioning profiles.
*/
function find(options, callback) {
if (typeof options === 'function') {
callback = options;
options = {};
} else if (!options) {
options = {};
}
typeof callback === 'function' || (callback = function () {});
var deviceUDIDs = (Array.isArray(options.deviceUDIDs) ? options.deviceUDIDs : [ options.deviceUDIDs ]).filter(function (a) { return a; }),
certs = (Array.isArray(options.certs) ? options.certs : [ options.certs ]).filter(function (a) { return a; });
options.validOnly = options.validOnly === undefined || options.validOnly === true;
exports.detect(options, function (err, results) {
if (err) {
return callback(err);
} else {
var profiles = [];
function check(scope) {
scope.forEach(function (pp) {
// check app id
if (options.appId && !(new RegExp('^' + pp.appId.replace(/\./g, '\\.').replace(/\*/g, '.*') + '$')).test(options.appId)) {
return;
}
// check certs
if (certs.length) {
var match = false;
for (var i = 0, l = certs.length; i < l; i++) {
var prefix = certs[i].pem.replace(/^-----BEGIN CERTIFICATE-----\n/, '').substring(0, 60);
if (pp.certs.some(function (cert) { return cert.indexOf(prefix) === 0; })) {
match = true;
break;
}
}
if (!match) return;
}
// check device uuids
if (deviceUDIDs.length && (pp.devices === null || !deviceUDIDs.some(function (d) { return pp.devices.indexOf(d) !== -1; }))) {
return;
}
profiles.push(pp);
});
}
check(results.provisioning.development);
check(results.provisioning.distribution);
check(results.provisioning.adhoc);
return callback(null, profiles);
}
});
};
/**
* Watches a provisioning profile directory for file changes.
*
* @param {Object} [options] - An object containing various settings.
* @param {String} [options.profileDir=~/Library/Developer/Xcode/UserData/Provisioning Profiles] - The path to search for provisioning profiles.
* @param {Function} [callback(err, results)] - A function to call with the provisioning profile information.
*
* @returns {Function} A function that unwatches changes.
*/
function watch(options, callback) {
if (typeof options === 'function') {
callback = options;
options = {};
} else if (!options) {
options = {};
}
options.watch = true;
options.bypassCache = true;
exports.detect(options, callback);
return function () {
unwatch(options.profileDir);
};
};
/**
* Stops watching the specified provisioning profile directory.
*
* @param {String} [profileDir=~/Library/Developer/Xcode/UserData/Provisioning Profiles] - The path to the provisioning profile directory.
*/
function unwatch(profileDir) {
var profileDirs = getExistingProvisioningProfileDirectories(profileDir);
for (const profileDir of profileDirs) {
if (!watchers[profileDir]) continue;
if (--watchers[profileDir].count <= 0) {
watchers[profileDir].handle.close();
delete watchers[profileDir];
}
}
};
/**
* Searches for existing provisioning profile directories.
*
* @throws
* @param {string | undefined} profileDir A custom directory set by the developer.
* @returns {string[]} The directories that exist on the filesystem.
*/
function getExistingProvisioningProfileDirectories(profileDir) {
const profileDirectories = [];
for (const directory of [profileDir, ...provisioningProfilesDirectories]) {
if (!directory) {
continue;
}
const resolvedDirectory = appc.fs.resolvePath(directory);
if (fs.existsSync(resolvedDirectory)) {
profileDirectories.push(resolvedDirectory);
}
}
return profileDirectories;
}
/*
* If the app exits, close all filesystem watchers.
*/
process.on('exit', function () {
Object.keys(watchers).forEach(function (w) {
watchers[w].handle.close();
delete watchers[w];
});
});