Modules (188)

Package

Description

Functions for working with extension packages

Dependencies

Variables

Private

_nodeConnection

Type
NodeConnection
    var _nodeConnection;
Private

_nodeConnectionDeferred

Type
jQuery.Deferred.<NodeConnection>
    var _nodeConnectionDeferred = $.Deferred();
Private

_uniqueId

Type
number
    var _uniqueId = 0;

    function _extensionManagerCall(callback) {
        if (_nodeConnection.domains.extensionManager) {
            return callback(_nodeConnection.domains.extensionManager);
        } else {
            return new $.Deferred().reject("extensionManager domain is undefined").promise();
        }
    }

Functions

Private Public API

_getNodeConnectionDeferred

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.

Returns: jQuery.Deferred
The deferred that manages the node connection
    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;
});

cancelDownload

Attempts to synchronously cancel the given pending download. This may not be possible, e.g. if the download has already finished.

downloadId number
Identifier previously passed to download()
    function cancelDownload(downloadId) {
        return _extensionManagerCall(function (extensionManager) {
            return extensionManager.abortDownload(downloadId);
        });
    }
Public API

disable

Disables the extension at the given path.

path string
The absolute path to the extension to disable.
Returns: $.Promise
A promise that's resolved when the extenion is disabled, or rejected if there was an error.
    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();
    }

download

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.

url string
URL of the file to be downloaded
downloadId number
Unique number to identify this request
Returns: $.Promise
    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();
        });
    }
Public API

enable

Enables the extension at the given path.

path string
The absolute path to the extension to enable.
Returns: $.Promise
A promise that's resolved when the extenion is enable, or rejected if there was an error.
    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();
    }
Public API

formatError

Converts an error object as returned by install(), installFromPath() or installFromURL() into a flattened, localized string.

error string,Array.<string>
Returns: 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);
        }
    }

githubURLFilter

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).

urlInfo {url:string,parsed:Array.<string>,filenameHint:string}
    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";
                }
            }
        }
    }
Public API

install

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.

path string
Absolute path to the package zip file
nameHint nullable string
Hint for the extension folder's name (used in favor of path's filename if present, and if no package metadata present).
_doUpdate nullable boolean
private argument used to signal an update
Returns: $.Promise
A promise that is resolved with information about the package (which may include errors, in which case the extension was disabled), or rejected with an error object.
    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();
        });
    }
Public API

installFromPath

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.

path string
Absolute path to the package zip file
filenameHint nullable string
Hint for the extension folder's name (used in favor of path's filename if present, and if no package metadata present).
Returns: $.Promise
A promise that is rejected if there are errors during install or the extension is disabled.
    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();
    }
Public API

installFromURL

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.

Returns: {promise: $.Promise,cancel: function():boolean}
    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.
            }
        };
    }
Public API

installUpdate

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.

path string
to package file
nameHint nullable string
Hint for the extension folder's name (used in favor of path's filename if present, and if no package metadata present).
Returns: $.Promise
A promise that is resolved when the extension is successfully installed or rejected if there is a problem.
    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();
    }
Public API

remove

Removes the extension at the given path.

path string
The absolute path to the extension to remove.
Returns: $.Promise
A promise that's resolved when the extension is removed, or rejected if there was an error.
    function remove(path) {
        return _extensionManagerCall(function (extensionManager) {
            return extensionManager.remove(path);
        });
    }

toggleDefaultExtension

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);
    }
Public API

validate

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.

Absolute string
path to the package zip file
validation {requirePackageJSON: ?boolean}
options
Returns: $.Promise
A promise that is resolved with information about the package
    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();
        });
    }