src/extensibility/ExtensionManager.js
/*
* Copyright (c) 2013 - present Adobe Systems Incorporated. All rights reserved.
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*
*/
/*jslint regexp: true */
/*unittests: ExtensionManager*/
/**
* The ExtensionManager fetches/caches the extension registry and provides
* information about the status of installed extensions. ExtensionManager raises the
* following events:
* - statusChange - indicates that an extension has been installed/uninstalled or
* its status has otherwise changed. Second parameter is the id of the
* extension.
* - registryUpdate - indicates that an existing extension was synchronized
* with new data from the registry.
*/
define(function (require, exports, module) {
"use strict";
var _ = require("thirdparty/lodash"),
EventDispatcher = require("utils/EventDispatcher"),
Package = require("extensibility/Package"),
AppInit = require("utils/AppInit"),
Async = require("utils/Async"),
ExtensionLoader = require("utils/ExtensionLoader"),
ExtensionUtils = require("utils/ExtensionUtils"),
FileSystem = require("filesystem/FileSystem"),
FileUtils = require("file/FileUtils"),
PreferencesManager = require("preferences/PreferencesManager"),
Strings = require("strings"),
StringUtils = require("utils/StringUtils"),
ThemeManager = require("view/ThemeManager");
// semver.browser is an AMD-compatible module
var semver = require("thirdparty/semver.browser");
/**
* @private
* @type {$.Deferred} Keeps track of the current registry download so that if a request is already
* in progress and another request to download the registry comes in, we don't send yet another request.
* This is primarily used when multiple view models need to download the registry at the same time.
*/
var pendingDownloadRegistry = null;
/**
* Extension status constants.
*/
var ENABLED = "enabled",
DISABLED = "disabled",
START_FAILED = "startFailed";
/**
* Extension location constants.
*/
var LOCATION_DEFAULT = "default",
LOCATION_DEV = "dev",
LOCATION_USER = "user",
LOCATION_UNKNOWN = "unknown";
/**
* Extension auto-install folder. Also used for preferences key.
*/
var FOLDER_AUTOINSTALL = "auto-install-extensions";
/**
* @private
* @type {Object.<string, {metadata: Object, path: string, status: string}>}
* The set of all known extensions, both from the registry and locally installed.
* The keys are either "name" from package.json (for extensions that have package metadata)
* or the last segment of local file paths (for installed legacy extensions
* with no package metadata). The fields of each record are:
* registryInfo: object containing the info for this id from the main registry (containing metadata, owner,
* and versions). This will be null for legacy extensions.
* installInfo: object containing the info for a locally-installed extension:
* metadata: the package metadata loaded from the local package.json, or null if it's a legacy extension.
* This will be different from registryInfo.metadata if there's a newer version in the registry.
* path: the local path to the extension folder on disk
* locationType: general type of installation; one of the LOCATION_* constants above
* status: the current status, one of the status constants above
*/
var extensions = {};
/**
* Requested changes to the installed extensions.
*/
var _idsToRemove = {},
_idsToUpdate = {},
_idsToDisable = {};
PreferencesManager.stateManager.definePreference(FOLDER_AUTOINSTALL, "object", undefined);
PreferencesManager.definePreference("extensions.sort", "string", "publishedDate", {
description: Strings.SORT_EXTENSION_METHOD
});
/**
* @private
* Synchronizes the information between the public registry and the installed
* extensions. Specifically, this makes the `owner` available in each and sets
* an `updateAvailable` flag.
*
* @param {string} id of the extension to synchronize
*/
function synchronizeEntry(id) {
var entry = extensions[id];
// Do nothing if we only have one set of data
if (!entry || !entry.installInfo || !entry.registryInfo) {
return;
}
entry.installInfo.owner = entry.registryInfo.owner;
// Assume false
entry.installInfo.updateAvailable = false;
entry.registryInfo.updateAvailable = false;
entry.installInfo.updateCompatible = false;
entry.registryInfo.updateCompatible = false;
var currentVersion = entry.installInfo.metadata ? entry.installInfo.metadata.version : null;
if (currentVersion && semver.lt(currentVersion, entry.registryInfo.metadata.version)) {
// Note: available update may still be incompatible; we check for this when rendering the Update button in ExtensionManagerView._renderItem()
entry.registryInfo.updateAvailable = true;
entry.installInfo.updateAvailable = true;
// Calculate updateCompatible to check if there's an update for current version of Brackets
var lastCompatibleVersionInfo = _.findLast(entry.registryInfo.versions, function (versionInfo) {
return !versionInfo.brackets || semver.satisfies(brackets.metadata.apiVersion, versionInfo.brackets);
});
if (lastCompatibleVersionInfo && lastCompatibleVersionInfo.version && semver.lt(currentVersion, lastCompatibleVersionInfo.version)) {
entry.installInfo.updateCompatible = true;
entry.registryInfo.updateCompatible = true;
entry.installInfo.lastCompatibleVersion = lastCompatibleVersionInfo.version;
entry.registryInfo.lastCompatibleVersion = lastCompatibleVersionInfo.version;
}
}
exports.trigger("registryUpdate", id);
}
/**
* @private
* Verifies if an extension is a theme based on the presence of the field "theme"
* in the package.json. If it is a theme, then the theme file is just loaded by the
* ThemeManager
*
* @param {string} id of the theme extension to load
*/
function loadTheme(id) {
var extension = extensions[id];
if (extension.installInfo && extension.installInfo.metadata && extension.installInfo.metadata.theme) {
ThemeManager.loadPackage(extension.installInfo);
}
}
/**
* @private
* Sets our data. For unit testing only.
*/
function _setExtensions(newExtensions) {
exports.extensions = extensions = newExtensions;
Object.keys(extensions).forEach(function (id) {
synchronizeEntry(id);
});
}
/**
* @private
* Clears out our existing data. For unit testing only.
*/
function _reset() {
exports.extensions = extensions = {};
_idsToRemove = {};
_idsToUpdate = {};
_idsToDisable = {};
}
/**
* Downloads the registry of Brackets extensions and stores the information in our
* extension info.
*
* @return {$.Promise} a promise that's resolved with the registry JSON data
* or rejected if the server can't be reached.
*/
function downloadRegistry() {
if (pendingDownloadRegistry) {
return pendingDownloadRegistry.promise();
}
pendingDownloadRegistry = new $.Deferred();
$.ajax({
url: brackets.config.extension_registry,
dataType: "json",
cache: false
})
.done(function (data) {
exports.hasDownloadedRegistry = true;
Object.keys(data).forEach(function (id) {
if (!extensions[id]) {
extensions[id] = {};
}
extensions[id].registryInfo = data[id];
synchronizeEntry(id);
});
exports.trigger("registryDownload");
pendingDownloadRegistry.resolve();
})
.fail(function () {
pendingDownloadRegistry.reject();
})
.always(function () {
// Make sure to clean up the pending registry so that new requests can be made.
pendingDownloadRegistry = null;
});
return pendingDownloadRegistry.promise();
}
/**
* @private
* When an extension is loaded, fetches the package.json and stores the extension in our map.
* @param {$.Event} e The event object
* @param {string} path The local path of the loaded extension's folder.
*/
function _handleExtensionLoad(e, path) {
function setData(metadata) {
var locationType,
id = metadata.name,
userExtensionPath = ExtensionLoader.getUserExtensionPath();
if (path.indexOf(userExtensionPath) === 0) {
locationType = LOCATION_USER;
} else {
var segments = path.split("/"), parent;
if (segments.length > 2) {
parent = segments[segments.length - 2];
}
if (parent === "dev") {
locationType = LOCATION_DEV;
} else if (parent === "default") {
locationType = LOCATION_DEFAULT;
} else {
locationType = LOCATION_UNKNOWN;
}
}
if (!extensions[id]) {
extensions[id] = {};
}
extensions[id].installInfo = {
metadata: metadata,
path: path,
locationType: locationType,
status: (e.type === "loadFailed" ? START_FAILED : (e.type === "disabled" ? DISABLED : ENABLED))
};
synchronizeEntry(id);
loadTheme(id);
exports.trigger("statusChange", id);
}
function deduceMetadata() {
var match = path.match(/\/([^\/]+)$/),
name = (match && match[1]) || path,
metadata = { name: name, title: name };
return metadata;
}
ExtensionUtils.loadMetadata(path)
.done(function (metadata) {
setData(metadata);
})
.fail(function (disabled) {
// If there's no package.json, this is a legacy extension. It was successfully loaded,
// but we don't have an official ID or metadata for it, so we just create an id and
// "title" for it (which is the last segment of its pathname)
// and record that it's enabled.
var metadata = deduceMetadata();
metadata.disabled = disabled;
setData(metadata);
});
}
/**
* Determines if the given versions[] entry is compatible with the given Brackets API version, and if not
* specifies why.
* @param {Object} extVersion
* @param {string} apiVersion
* @return {{isCompatible: boolean, requiresNewer: ?boolean, compatibleVersion: ?string}}
*/
function getCompatibilityInfoForVersion(extVersion, apiVersion) {
var requiredVersion = (extVersion.brackets || (extVersion.engines && extVersion.engines.brackets)),
result = {};
result.isCompatible = !requiredVersion || semver.satisfies(apiVersion, requiredVersion);
if (result.isCompatible) {
result.compatibleVersion = extVersion.version;
} else {
// Find out reason for incompatibility
if (requiredVersion.charAt(0) === '<') {
result.requiresNewer = false;
} else if (requiredVersion.charAt(0) === '>') {
result.requiresNewer = true;
} else if (requiredVersion.charAt(0) === "~") {
var compareVersion = requiredVersion.slice(1);
// Need to add .0s to this style of range in order to compare (since valid version
// numbers must have major/minor/patch).
if (compareVersion.match(/^[0-9]+$/)) {
compareVersion += ".0.0";
} else if (compareVersion.match(/^[0-9]+\.[0-9]+$/)) {
compareVersion += ".0";
}
result.requiresNewer = semver.lt(apiVersion, compareVersion);
}
}
return result;
}
/**
* Finds the newest version of the entry that is compatible with the given Brackets API version, if any.
* @param {Object} entry The registry entry to check.
* @param {string} apiVersion The Brackets API version to check against.
* @return {{isCompatible: boolean, requiresNewer: ?boolean, compatibleVersion: ?string, isLatestVersion: boolean}}
* Result contains an "isCompatible" member saying whether it's compatible. If compatible, "compatibleVersion"
* specifies the newest version that is compatible and "isLatestVersion" indicates if this is the absolute
* latest version of the extension or not. If !isCompatible or !isLatestVersion, "requiresNewer" says whether
* the latest version is incompatible due to requiring a newer (vs. older) version of Brackets.
*/
function getCompatibilityInfo(entry, apiVersion) {
if (!entry.versions) {
var fallback = getCompatibilityInfoForVersion(entry.metadata, apiVersion);
if (fallback.isCompatible) {
fallback.isLatestVersion = true;
}
return fallback;
}
var i = entry.versions.length - 1,
latestInfo = getCompatibilityInfoForVersion(entry.versions[i], apiVersion);
if (latestInfo.isCompatible) {
latestInfo.isLatestVersion = true;
return latestInfo;
} else {
// Look at earlier versions (skipping very latest version since we already checked it)
for (i--; i >= 0; i--) {
var compatInfo = getCompatibilityInfoForVersion(entry.versions[i], apiVersion);
if (compatInfo.isCompatible) {
compatInfo.isLatestVersion = false;
compatInfo.requiresNewer = latestInfo.requiresNewer;
return compatInfo;
}
}
// No version is compatible, so just return info for the latest version
return latestInfo;
}
}
/**
* Given an extension id and version number, returns the URL for downloading that extension from
* the repository. Does not guarantee that the extension exists at that URL.
* @param {string} id The extension's name from the metadata.
* @param {string} version The version to download.
* @return {string} The URL to download the extension from.
*/
function getExtensionURL(id, version) {
return StringUtils.format(brackets.config.extension_url, id, version);
}
/**
* Removes the installed extension with the given id.
* @param {string} id The id of the extension to remove.
* @return {$.Promise} A promise that's resolved when the extension is removed or
* rejected with an error if there's a problem with the removal.
*/
function remove(id) {
var result = new $.Deferred();
if (extensions[id] && extensions[id].installInfo) {
Package.remove(extensions[id].installInfo.path)
.done(function () {
extensions[id].installInfo = null;
result.resolve();
exports.trigger("statusChange", id);
})
.fail(function (err) {
result.reject(err);
});
} else {
result.reject(StringUtils.format(Strings.EXTENSION_NOT_INSTALLED, id));
}
return result.promise();
}
/**
* @private
*
* Disables or enables the installed extensions.
*
* @param {string} id The id of the extension to disable or enable.
* @param {boolean} enable A boolean indicating whether to enable or disable.
* @return {$.Promise} A promise that's resolved when the extension action is
* completed or rejected with an error that prevents the action from completion.
*/
function _enableOrDisable(id, enable) {
var result = new $.Deferred(),
extension = extensions[id];
if (extension && extension.installInfo) {
Package[(enable ? "enable" : "disable")](extension.installInfo.path)
.done(function () {
extension.installInfo.status = enable ? ENABLED : DISABLED;
extension.installInfo.metadata.disabled = !enable;
result.resolve();
exports.trigger("statusChange", id);
})
.fail(function (err) {
result.reject(err);
});
} else {
result.reject(StringUtils.format(Strings.EXTENSION_NOT_INSTALLED, id));
}
return result.promise();
}
/**
* Disables the installed extension with the given id.
*
* @param {string} id The id of the extension to disable.
* @return {$.Promise} A promise that's resolved when the extenion is disabled or
* rejected with an error that prevented the disabling.
*/
function disable(id) {
return _enableOrDisable(id, false);
}
/**
* Enables the installed extension with the given id.
*
* @param {string} id The id of the extension to enable.
* @return {$.Promise} A promise that's resolved when the extenion is enabled or
* rejected with an error that prevented the enabling.
*/
function enable(id) {
return _enableOrDisable(id, true);
}
/**
* Updates an installed extension with the given package file.
* @param {string} id of the extension
* @param {string} packagePath path to the package file
* @param {boolean=} keepFile Flag to keep extension package file, default=false
* @return {$.Promise} A promise that's resolved when the extension is updated or
* rejected with an error if there's a problem with the update.
*/
function update(id, packagePath, keepFile) {
return Package.installUpdate(packagePath, id).done(function () {
if (!keepFile) {
FileSystem.getFileForPath(packagePath).unlink();
}
});
}
/**
* Deletes any temporary files left behind by extensions that
* were marked for update.
*/
function cleanupUpdates() {
Object.keys(_idsToUpdate).forEach(function (id) {
var installResult = _idsToUpdate[id],
keepFile = installResult.keepFile,
filename = installResult.localPath;
if (filename && !keepFile) {
FileSystem.getFileForPath(filename).unlink();
}
});
_idsToUpdate = {};
}
/**
* Unmarks all extensions marked for removal.
*/
function unmarkAllForRemoval() {
_idsToRemove = {};
}
/**
* Marks an extension for later removal, or unmarks an extension previously marked.
* @param {string} id The id of the extension to mark for removal.
* @param {boolean} mark Whether to mark or unmark it.
*/
function markForRemoval(id, mark) {
if (mark) {
_idsToRemove[id] = true;
} else {
delete _idsToRemove[id];
}
exports.trigger("statusChange", id);
}
/**
* Returns true if an extension is marked for removal.
* @param {string} id The id of the extension to check.
* @return {boolean} true if it's been marked for removal, false otherwise.
*/
function isMarkedForRemoval(id) {
return !!(_idsToRemove[id]);
}
/**
* Returns true if there are any extensions marked for removal.
* @return {boolean} true if there are extensions to remove
*/
function hasExtensionsToRemove() {
return Object.keys(_idsToRemove).length > 0;
}
/**
* Marks an extension for disabling later, or unmarks an extension previously marked.
*
* @param {string} id The id of the extension
* @param {boolean} mark Whether to mark or unmark the extension.
*/
function markForDisabling(id, mark) {
if (mark) {
_idsToDisable[id] = true;
} else {
delete _idsToDisable[id];
}
exports.trigger("statusChange", id);
}
/**
* Returns true if an extension is mark for disabling.
*
* @param {string} id The id of the extension to check.
* @return {boolean} true if it's been mark for disabling, false otherwise.
*/
function isMarkedForDisabling(id) {
return !!(_idsToDisable[id]);
}
/**
* Returns true if there are any extensions marked for disabling.
* @return {boolean} true if there are extensions to disable
*/
function hasExtensionsToDisable() {
return Object.keys(_idsToDisable).length > 0;
}
/**
* Unmarks all the extensions that have been marked for disabling.
*/
function unmarkAllForDisabling() {
_idsToDisable = {};
}
/**
* If a downloaded package appears to be an update, mark the extension for update.
* If an extension was previously marked for removal, marking for update will
* turn off the removal mark.
* @param {Object} installationResult info about the install provided by the Package.download function
*/
function updateFromDownload(installationResult) {
if (installationResult.keepFile === undefined) {
installationResult.keepFile = false;
}
var installationStatus = installationResult.installationStatus;
if (installationStatus === Package.InstallationStatuses.ALREADY_INSTALLED ||
installationStatus === Package.InstallationStatuses.NEEDS_UPDATE ||
installationStatus === Package.InstallationStatuses.SAME_VERSION ||
installationStatus === Package.InstallationStatuses.OLDER_VERSION) {
var id = installationResult.name;
delete _idsToRemove[id];
_idsToUpdate[id] = installationResult;
exports.trigger("statusChange", id);
}
}
/**
* Removes the mark for an extension to be updated on restart. Also deletes the
* downloaded package file.
* @param {string} id The id of the extension for which the update is being removed
*/
function removeUpdate(id) {
var installationResult = _idsToUpdate[id];
if (!installationResult) {
return;
}
if (installationResult.localPath && !installationResult.keepFile) {
FileSystem.getFileForPath(installationResult.localPath).unlink();
}
delete _idsToUpdate[id];
exports.trigger("statusChange", id);
}
/**
* Returns true if an extension is marked for update.
* @param {string} id The id of the extension to check.
* @return {boolean} true if it's been marked for update, false otherwise.
*/
function isMarkedForUpdate(id) {
return !!(_idsToUpdate[id]);
}
/**
* Returns true if there are any extensions marked for update.
* @return {boolean} true if there are extensions to update
*/
function hasExtensionsToUpdate() {
return Object.keys(_idsToUpdate).length > 0;
}
/**
* Removes extensions previously marked for removal.
* @return {$.Promise} A promise that's resolved when all extensions are removed, or rejected
* if one or more extensions can't be removed. When rejected, the argument will be an
* array of error objects, each of which contains an "item" property with the id of the
* failed extension and an "error" property with the actual error.
*/
function removeMarkedExtensions() {
return Async.doInParallel_aggregateErrors(
Object.keys(_idsToRemove),
function (id) {
return remove(id);
}
);
}
/**
* Disables extensions marked for disabling.
*
* If the return promise is rejected, the argument will contain an array of objects. Each
* element is an object identifying the extension failed with "item" property set to the
* extension id which has failed to be disabled and "error" property set to the error.
*
* @return {$.Promise} A promise that's resolved when all extensions marked for disabling are
* disabled or rejected if one or more extensions can't be disabled.
*/
function disableMarkedExtensions() {
return Async.doInParallel_aggregateErrors(
Object.keys(_idsToDisable),
function (id) {
return disable(id);
}
);
}
/**
* Updates extensions previously marked for update.
* @return {$.Promise} A promise that's resolved when all extensions are updated, or rejected
* if one or more extensions can't be updated. When rejected, the argument will be an
* array of error objects, each of which contains an "item" property with the id of the
* failed extension and an "error" property with the actual error.
*/
function updateExtensions() {
return Async.doInParallel_aggregateErrors(
Object.keys(_idsToUpdate),
function (id) {
var installationResult = _idsToUpdate[id];
return update(installationResult.name, installationResult.localPath, installationResult.keepFile);
}
);
}
/**
* Gets an array of extensions that are currently installed and can be updated to a new version
* @return {Array.<{id: string, installVersion: string, registryVersion: string}>}
* where id = extensionId
* installVersion = currently installed version of extension
* registryVersion = latest version compatible with current Brackets
*/
function getAvailableUpdates() {
var result = [];
Object.keys(extensions).forEach(function (extensionId) {
var extensionInfo = extensions[extensionId];
// skip extensions that are not installed or are not in the registry
if (!extensionInfo.installInfo || !extensionInfo.registryInfo) {
return;
}
if (extensionInfo.registryInfo.updateCompatible) {
result.push({
id: extensionId,
installVersion: extensionInfo.installInfo.metadata.version,
registryVersion: extensionInfo.registryInfo.lastCompatibleVersion
});
}
});
return result;
}
/**
* Takes the array returned from getAvailableUpdates() as an input and removes those entries
* that are no longer current - when currently installed version of an extension
* is equal or newer than registryVersion returned by getAvailableUpdates().
* This function is designed to work without the necessity to download extension registry
* @param {Array.<{id: string, installVersion: string, registryVersion: string}>} updates
* previous output of getAvailableUpdates()
* @return {Array.<{id: string, installVersion: string, registryVersion: string}>}
* filtered input as function description
*/
function cleanAvailableUpdates(updates) {
return updates.reduce(function (arr, updateInfo) {
var extDefinition = extensions[updateInfo.id];
if (!extDefinition || !extDefinition.installInfo) {
// extension has been uninstalled in the meantime
return arr;
}
var installedVersion = extDefinition.installInfo.metadata.version;
if (semver.lt(installedVersion, updateInfo.registryVersion)) {
arr.push(updateInfo);
}
return arr;
}, []);
}
/**
* @private
* Find valid extensions in specified path
* @param {string} dirPath Directory with extensions
* @param {Object} autoExtensions Object that maps names of previously auto-installed
* extensions {string} to installed version {string}.
* @return {$.Promise} Promise that resolves with arrays for extensions to update and install
*/
function _getAutoInstallFiles(dirPath, autoExtensions) {
var zipFiles = [],
installZips = [],
updateZips = [],
deferred = new $.Deferred();
FileSystem.getDirectoryForPath(dirPath).getContents(function (err, contents) {
if (!err) {
zipFiles = contents.filter(function (dirItem) {
return (dirItem.isFile && FileUtils.getFileExtension(dirItem.fullPath) === "zip");
});
}
// Parse zip files and separate new installs vs. updates
Async.doInParallel_aggregateErrors(zipFiles, function (file) {
var zipFilePromise = new $.Deferred();
// Call validate() so that we open the local zip file and parse the
// package.json. We need the name to detect if this zip will be a
// new install or an update.
Package.validate(file.fullPath, { requirePackageJSON: true }).done(function (info) {
if (info.errors.length) {
zipFilePromise.reject(Package.formatError(info.errors));
return;
}
var extensionInfo, installedVersion, zipArray, existingItem,
extensionName = info.metadata.name,
autoExtVersion = autoExtensions[extensionName];
// Verify extension has not already been auto-installed/updated
if (autoExtVersion && semver.lte(info.metadata.version, autoExtVersion)) {
// Have already auto installed/updated version >= version of this extension
zipFilePromise.reject();
return;
}
// Verify extension has not already been installed/updated by some other means
extensionInfo = extensions[extensionName];
installedVersion = extensionInfo && extensionInfo.installInfo && extensionInfo.installInfo.metadata.version;
if (installedVersion && semver.lte(info.metadata.version, installedVersion)) {
// Have already manually installed/updated version >= version of this extension
zipFilePromise.reject();
return;
}
// Update appropriate zip array. There could be multiple zip files for an
// extension, so make sure only the latest is stored
zipArray = (installedVersion) ? updateZips : installZips;
zipArray.some(function (zip) {
if (zip.info.metadata.name === extensionName) {
existingItem = zip;
return true;
}
return false;
});
if (existingItem) {
if (semver.lt(existingItem.info.metadata.version, info.metadata.version)) {
existingItem.file = file;
existingItem.info = info;
}
} else {
zipArray.push({ file: file, info: info });
}
zipFilePromise.resolve();
}).fail(function (err) {
zipFilePromise.reject(Package.formatError(err));
});
return zipFilePromise.promise();
}).fail(function (errorArray) {
// Async.doInParallel() fails if some are successful, so write errors
// to console and always resolve
errorArray.forEach(function (errorObj) {
// If we rejected without an error argument, it means it was no problem
// (e.g. same version of extension is already installed)
if (errorObj.error) {
if (errorObj.error.forEach) {
console.error("Errors for", errorObj.item);
errorObj.error.forEach(function (error) {
console.error(Package.formatError(error));
});
} else {
console.error("Error for", errorObj.item, errorObj);
}
}
});
}).always(function () {
deferred.resolve({
installZips: installZips,
updateZips: updateZips
});
});
});
return deferred.promise();
}
/**
* @private
* Auto-install extensions bundled with installer
* @return {$.Promise} Promise that resolves when finished
*/
function _autoInstallExtensions() {
var dirPath = FileUtils.getDirectoryPath(FileUtils.getNativeBracketsDirectoryPath()) + FOLDER_AUTOINSTALL + "/",
autoExtensions = PreferencesManager.getViewState(FOLDER_AUTOINSTALL) || {},
deferred = new $.Deferred();
_getAutoInstallFiles(dirPath, autoExtensions).done(function (result) {
var installPromise = Async.doSequentially(result.installZips, function (zip) {
autoExtensions[zip.info.metadata.name] = zip.info.metadata.version;
return Package.installFromPath(zip.file.fullPath);
});
var updatePromise = installPromise.always(function () {
return Async.doSequentially(result.updateZips, function (zip) {
autoExtensions[zip.info.metadata.name] = zip.info.metadata.version;
return Package.installUpdate(zip.file.fullPath);
});
});
// Always resolve the outer promise
updatePromise.always(function () {
// Keep track of auto-installed extensions so we only install an extension once
PreferencesManager.setViewState(FOLDER_AUTOINSTALL, autoExtensions);
deferred.resolve();
});
});
return deferred.promise();
}
AppInit.appReady(function () {
Package._getNodeConnectionDeferred().done(function () {
_autoInstallExtensions();
});
});
// Listen to extension load and loadFailed events
ExtensionLoader
.on("load", _handleExtensionLoad)
.on("loadFailed", _handleExtensionLoad)
.on("disabled", _handleExtensionLoad);
EventDispatcher.makeEventDispatcher(exports);
// Public exports
exports.downloadRegistry = downloadRegistry;
exports.getCompatibilityInfo = getCompatibilityInfo;
exports.getExtensionURL = getExtensionURL;
exports.remove = remove;
exports.update = update;
exports.disable = disable;
exports.enable = enable;
exports.extensions = extensions;
exports.cleanupUpdates = cleanupUpdates;
exports.markForRemoval = markForRemoval;
exports.isMarkedForRemoval = isMarkedForRemoval;
exports.unmarkAllForRemoval = unmarkAllForRemoval;
exports.hasExtensionsToRemove = hasExtensionsToRemove;
exports.markForDisabling = markForDisabling;
exports.isMarkedForDisabling = isMarkedForDisabling;
exports.unmarkAllForDisabling = unmarkAllForDisabling;
exports.hasExtensionsToDisable = hasExtensionsToDisable;
exports.updateFromDownload = updateFromDownload;
exports.removeUpdate = removeUpdate;
exports.isMarkedForUpdate = isMarkedForUpdate;
exports.hasExtensionsToUpdate = hasExtensionsToUpdate;
exports.removeMarkedExtensions = removeMarkedExtensions;
exports.disableMarkedExtensions = disableMarkedExtensions;
exports.updateExtensions = updateExtensions;
exports.getAvailableUpdates = getAvailableUpdates;
exports.cleanAvailableUpdates = cleanAvailableUpdates;
exports.hasDownloadedRegistry = false;
exports.ENABLED = ENABLED;
exports.DISABLED = DISABLED;
exports.START_FAILED = START_FAILED;
exports.LOCATION_DEFAULT = LOCATION_DEFAULT;
exports.LOCATION_DEV = LOCATION_DEV;
exports.LOCATION_USER = LOCATION_USER;
exports.LOCATION_UNKNOWN = LOCATION_UNKNOWN;
// For unit testing only
exports._getAutoInstallFiles = _getAutoInstallFiles;
exports._reset = _reset;
exports._setExtensions = _setExtensions;
});