Functions for working with extension packages
Allows access to the deferred that manages the node connection. This is only for unit tests. Messing with this not in testing will potentially break everything.
function _getNodeConnectionDeferred() {
return _nodeConnectionDeferred;
}
// Initializes node connection
// TODO: duplicates code from StaticServer
// TODO: can this be done lazily?
AppInit.appReady(function () {
_nodeConnection = new NodeConnection();
_nodeConnection.connect(true).then(function () {
var domainPath = FileUtils.getNativeBracketsDirectoryPath() + "/" + FileUtils.getNativeModuleDirectoryPath(module) + "/node/ExtensionManagerDomain";
_nodeConnection.loadDomains(domainPath, true)
.then(
function () {
_nodeConnectionDeferred.resolve();
},
function () { // Failed to connect
console.error("[Extensions] Failed to connect to node", arguments);
_nodeConnectionDeferred.reject();
}
);
});
});
// For unit tests only
exports._getNodeConnectionDeferred = _getNodeConnectionDeferred;
exports.installFromURL = installFromURL;
exports.installFromPath = installFromPath;
exports.validate = validate;
exports.install = install;
exports.remove = remove;
exports.disable = disable;
exports.enable = enable;
exports.installUpdate = installUpdate;
exports.formatError = formatError;
exports.InstallationStatuses = InstallationStatuses;
});
Attempts to synchronously cancel the given pending download. This may not be possible, e.g. if the download has already finished.
function cancelDownload(downloadId) {
return _extensionManagerCall(function (extensionManager) {
return extensionManager.abortDownload(downloadId);
});
}
Disables the extension at the given path.
function disable(path) {
var result = new $.Deferred(),
file = FileSystem.getFileForPath(path + "/.disabled");
var defaultExtensionPath = ExtensionLoader.getDefaultExtensionPath();
if (file.fullPath.indexOf(defaultExtensionPath) === 0) {
toggleDefaultExtension(path, false);
result.resolve();
return result.promise();
}
file.write("", function (err) {
if (err) {
return result.reject(err);
}
result.resolve();
});
return result.promise();
}
Downloads from the given URL to a temporary location. On success, resolves with the path of the downloaded file (typically in a temp folder) and a hint for the real filename. On failure, rejects with an error object.
function download(url, downloadId) {
return _extensionManagerCall(function (extensionManager) {
var d = new $.Deferred();
// Validate URL
// TODO: PathUtils fails to parse URLs that are missing the protocol part (e.g. starts immediately with "www...")
var parsed = PathUtils.parseUrl(url);
if (!parsed.hostname) { // means PathUtils failed to parse at all
d.reject(Errors.MALFORMED_URL);
return d.promise();
}
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
d.reject(Errors.UNSUPPORTED_PROTOCOL);
return d.promise();
}
var urlInfo = { url: url, parsed: parsed, filenameHint: parsed.filename };
githubURLFilter(urlInfo);
// Decide download destination
var filename = urlInfo.filenameHint;
filename = filename.replace(/[^a-zA-Z0-9_\- \(\)\.]/g, "_"); // make sure it's a valid filename
if (!filename) { // in case of URL ending in "/"
filename = "extension.zip";
}
// Download the bits (using Node since brackets-shell doesn't support binary file IO)
var r = extensionManager.downloadFile(downloadId, urlInfo.url, PreferencesManager.get("proxy"));
r.done(function (result) {
d.resolve({ localPath: FileUtils.convertWindowsPathToUnixPath(result), filenameHint: urlInfo.filenameHint });
}).fail(function (err) {
d.reject(err);
});
return d.promise();
});
}
Enables the extension at the given path.
function enable(path) {
var result = new $.Deferred(),
file = FileSystem.getFileForPath(path + "/.disabled");
function afterEnable() {
ExtensionLoader.loadExtension(FileUtils.getBaseName(path), { baseUrl: path }, "main")
.done(result.resolve)
.fail(result.reject);
}
var defaultExtensionPath = ExtensionLoader.getDefaultExtensionPath();
if (file.fullPath.indexOf(defaultExtensionPath) === 0) {
toggleDefaultExtension(path, true);
afterEnable();
return result.promise();
}
file.unlink(function (err) {
if (err) {
return result.reject(err);
}
afterEnable();
});
return result.promise();
}
Converts an error object as returned by install(), installFromPath() or installFromURL() into a flattened, localized string.
function formatError(error) {
function localize(key) {
if (Strings[key]) {
return Strings[key];
}
console.log("Unknown installation error", key);
return Strings.UNKNOWN_ERROR;
}
if (Array.isArray(error)) {
error[0] = localize(error[0]);
return StringUtils.format.apply(window, error);
} else {
return localize(error);
}
}
Special case handling to make the common case of downloading from GitHub easier; modifies 'urlInfo' as needed. Converts a bare GitHub repo URL to the corresponding master ZIP URL; or if given a direct master ZIP URL already, sets a nicer download filename (both cases use the repo name).
function githubURLFilter(urlInfo) {
if (urlInfo.parsed.hostname === "github.com" || urlInfo.parsed.hostname === "www.github.com") {
// Is it a URL to the root of a repo? (/user/repo)
var match = /^\/[^\/?]+\/([^\/?]+)(\/?)$/.exec(urlInfo.parsed.pathname);
if (match) {
if (!match[2]) {
urlInfo.url += "/";
}
urlInfo.url += "archive/master.zip";
urlInfo.filenameHint = match[1] + ".zip";
} else {
// Is it a URL directly to the repo's 'master.zip'? (/user/repo/archive/master.zip)
match = /^\/[^\/?]+\/([^\/?]+)\/archive\/master.zip$/.exec(urlInfo.parsed.pathname);
if (match) {
urlInfo.filenameHint = match[1] + ".zip";
}
}
}
}
Validates and installs the package at the given path. Validation and installation is handled by the Node process.
The extension will be installed into the user's extensions directory. If the user already has the extension installed, it will instead go into their disabled extensions directory.
The promise is resolved with an object: { errors: Array.<{string}>, metadata: { name:string, version:string, ... }, disabledReason:string, installedTo:string, commonPrefix:string } metadata is pulled straight from package.json and is likely to be undefined if there are errors. It is null if there was no package.json.
disabledReason is either null or the reason the extension was installed disabled.
function install(path, nameHint, _doUpdate) {
return _extensionManagerCall(function (extensionManager) {
var d = new $.Deferred(),
destinationDirectory = ExtensionLoader.getUserExtensionPath(),
disabledDirectory = destinationDirectory.replace(/\/user$/, "/disabled"),
systemDirectory = FileUtils.getNativeBracketsDirectoryPath() + "/extensions/default/";
var operation = _doUpdate ? "update" : "install";
extensionManager[operation](path, destinationDirectory, {
disabledDirectory: disabledDirectory,
systemExtensionDirectory: systemDirectory,
apiVersion: brackets.metadata.apiVersion,
nameHint: nameHint,
proxy: PreferencesManager.get("proxy")
})
.done(function (result) {
result.keepFile = false;
if (result.installationStatus !== InstallationStatuses.INSTALLED || _doUpdate) {
d.resolve(result);
} else {
// This was a new extension and everything looked fine.
// We load it into Brackets right away.
ExtensionLoader.loadExtension(result.name, {
// On Windows, it looks like Node converts Unix-y paths to backslashy paths.
// We need to convert them back.
baseUrl: FileUtils.convertWindowsPathToUnixPath(result.installedTo)
}, "main").then(function () {
d.resolve(result);
}, function () {
d.reject(Errors.ERROR_LOADING);
});
}
})
.fail(function (error) {
d.reject(error);
});
return d.promise();
});
}
On success, resolves with an extension metadata object; at that point, the extension has already started running in Brackets. On failure (including validation errors), rejects with an error object.
An error object consists of either a string error code OR an array where the first entry is the error code and the remaining entries are further info. The error code string is one of either ExtensionsDomain.Errors or Package.Errors. Use formatError() to convert an error object to a friendly, localized error message.
function installFromPath(path, filenameHint) {
var d = new $.Deferred();
install(path, filenameHint)
.done(function (result) {
result.keepFile = true;
var installationStatus = result.installationStatus;
if (installationStatus === InstallationStatuses.ALREADY_INSTALLED ||
installationStatus === InstallationStatuses.NEEDS_UPDATE ||
installationStatus === InstallationStatuses.SAME_VERSION ||
installationStatus === InstallationStatuses.OLDER_VERSION) {
d.resolve(result);
} else {
if (result.errors && result.errors.length > 0) {
// Validation errors - for now, only return the first one
d.reject(result.errors[0]);
} else if (result.disabledReason) {
// Extension valid but left disabled (wrong API version, extension name collision, etc.)
d.reject(result.disabledReason);
} else {
// Success! Extension is now running in Brackets
d.resolve(result);
}
}
})
.fail(function (err) {
d.reject(err);
});
return d.promise();
}
On success, resolves with an extension metadata object; at that point, the extension has already started running in Brackets. On failure (including validation errors), rejects with an error object.
An error object consists of either a string error code OR an array where the first entry is the error code and the remaining entries are further info. The error code string is one of either ExtensionsDomain.Errors or Package.Errors. Use formatError() to convert an error object to a friendly, localized error message.
The returned cancel() function will attempt to cancel installation, but it is not guaranteed to succeed. If cancel() succeeds, the Promise is rejected with a CANCELED error code. If we're unable to cancel, the Promise is resolved or rejected normally, as if cancel() had never been called.
function installFromURL(url) {
var STATE_DOWNLOADING = 1,
STATE_INSTALLING = 2,
STATE_SUCCEEDED = 3,
STATE_FAILED = 4;
var d = new $.Deferred();
var state = STATE_DOWNLOADING;
var downloadId = (_uniqueId++);
download(url, downloadId)
.done(function (downloadResult) {
state = STATE_INSTALLING;
installFromPath(downloadResult.localPath, downloadResult.filenameHint)
.done(function (result) {
var installationStatus = result.installationStatus;
state = STATE_SUCCEEDED;
result.localPath = downloadResult.localPath;
result.keepFile = false;
if (installationStatus === InstallationStatuses.INSTALLED) {
// Delete temp file
FileSystem.getFileForPath(downloadResult.localPath).unlink();
}
d.resolve(result);
})
.fail(function (err) {
// File IO errors, internal error in install()/validate(), or extension startup crashed
state = STATE_FAILED;
FileSystem.getFileForPath(downloadResult.localPath).unlink();
d.reject(err); // TODO: needs to be err.message ?
});
})
.fail(function (err) {
// Download error (the Node-side download code cleans up any partial ZIP file)
state = STATE_FAILED;
d.reject(err);
});
return {
promise: d.promise(),
cancel: function () {
if (state === STATE_DOWNLOADING) {
// This will trigger download()'s fail() handler with CANCELED as the err code
cancelDownload(downloadId);
}
// Else it's too late to cancel; we'll continue on through the done() chain and emit
// a success result (calling done() handlers) if all else goes well.
}
};
}
Install an extension update located at path. This assumes that the installation was previously attempted and an installationStatus of "ALREADY_INSTALLED", "NEEDS_UPDATE", "SAME_VERSION", or "OLDER_VERSION" was the result.
This workflow ensures that there should not generally be validation errors because the first pass at installation the extension looked at the metadata and installed packages.
function installUpdate(path, nameHint) {
var d = new $.Deferred();
install(path, nameHint, true)
.done(function (result) {
if (result.installationStatus !== InstallationStatuses.INSTALLED) {
d.reject(result.errors);
} else {
d.resolve(result);
}
})
.fail(function (error) {
d.reject(error);
});
return d.promise();
}
Removes the extension at the given path.
function remove(path) {
return _extensionManagerCall(function (extensionManager) {
return extensionManager.remove(path);
});
}
This function manages the PREF_EXTENSIONS_DEFAULT_DISABLED preference holding an array of default extension paths that should not be loaded on Brackets startup
function toggleDefaultExtension(path, enabled) {
var arr = PreferencesManager.get(PREF_EXTENSIONS_DEFAULT_DISABLED);
if (!Array.isArray(arr)) { arr = []; }
var io = arr.indexOf(path);
if (enabled === true && io !== -1) {
arr.splice(io, 1);
} else if (enabled === false && io === -1) {
arr.push(path);
}
PreferencesManager.set(PREF_EXTENSIONS_DEFAULT_DISABLED, arr);
}
TODO: can this go away now that we never call it directly?
Validates the package at the given path. The actual validation is handled by the Node server.
The promise is resolved with an object: { errors: Array.<{string}>, metadata: { name:string, version:string, ... } } metadata is pulled straight from package.json and will be undefined if there are errors or null if the extension did not include package.json.
function validate(path, options) {
return _extensionManagerCall(function (extensionManager) {
var d = new $.Deferred();
// make sure proxy is attached to options before calling validate
// so npm can use it in the domain
options = options || {};
options.proxy = PreferencesManager.get("proxy");
extensionManager.validate(path, options)
.done(function (result) {
d.resolve({
errors: result.errors,
metadata: result.metadata
});
})
.fail(function (error) {
d.reject(error);
});
return d.promise();
});
}