diff --git a/configs/client-default.js b/configs/client-default.js
index 924a5a0d..d4b0a7b4 100644
--- a/configs/client-default.js
+++ b/configs/client-default.js
@@ -120,7 +120,9 @@ module.exports = function(options) {
region: options.region,
pid: options.project.id,
servers: options.vfsServers,
- updateServers: hosted
+ updateServers: hosted,
+ strictRegion: options.strictRegion
+ || options.mode === "beta" && "beta"
},
{
packagePath: "plugins/c9.ide.auth/auth",
diff --git a/plugins/c9.core/api.js b/plugins/c9.core/api.js
new file mode 100644
index 00000000..65e049f0
--- /dev/null
+++ b/plugins/c9.core/api.js
@@ -0,0 +1,101 @@
+define(function(require, exports, module) {
+ "use strict";
+
+ main.consumes = ["Plugin", "auth", "ext"];
+ main.provides = ["api"];
+ return main;
+
+ function main(options, imports, register) {
+ var Plugin = imports.Plugin;
+ var auth = imports.auth;
+
+ /***** Initialization *****/
+
+ var plugin = new Plugin("Ajax.org", main.consumes);
+ var apiUrl = options.apiUrl || "";
+ var pid = options.projectId;
+
+ var BASICAUTH;
+
+ // Set api to ext
+ imports.ext.api = plugin;
+
+ /***** Methods *****/
+
+ var REST_METHODS = ["get", "post", "put", "delete", "patch"];
+
+ function wrapMethod(urlPrefix, method) {
+ return function(url, options, callback) {
+ url = apiUrl + urlPrefix + url;
+ if (!callback) {
+ callback = options;
+ options = {};
+ }
+ var headers = options.headers = options.headers || {};
+ headers.Accept = headers.Accept || "application/json";
+ options.method = method.toUpperCase();
+ if (!options.timeout)
+ options.timeout = 60000;
+
+ if (BASICAUTH) {
+ options.username = BASICAUTH[0];
+ options.password = BASICAUTH[1];
+ }
+
+ auth.request(url, options, function(err, data, res) {
+ if (err) {
+ err = (data && data.error) || err;
+ err.message = err.message || String(err);
+ return callback(err, data, res);
+ }
+ callback(err, data, res);
+ });
+ };
+ }
+
+ function apiWrapper(urlPrefix) {
+ var wrappers = REST_METHODS.map(wrapMethod.bind(null, urlPrefix));
+ var wrappedApi = {};
+ for (var i = 0; i < wrappers.length; i++)
+ wrappedApi[REST_METHODS[i]] = wrappers[i];
+ return wrappedApi;
+ }
+
+ var collab = apiWrapper("/collab/" + pid + "/");
+ var user = apiWrapper("/user/");
+ var preview = apiWrapper("/preview/");
+ var project = apiWrapper("/projects/" + pid + "/");
+ var users = apiWrapper("/users/");
+ var packages = apiWrapper("/packages/");
+ var stats = apiWrapper("/stats/");
+ var settings = apiWrapper("/settings/");
+ var vfs = apiWrapper("/vfs/");
+
+ /***** Register and define API *****/
+
+ /**
+ * Provides C9 API access
+ * @singleton
+ **/
+ plugin.freezePublicAPI({
+ get apiUrl() { return apiUrl; },
+
+ get basicAuth() { throw new Error("Permission Denied"); },
+ set basicAuth(v) { BASICAUTH = v.split(":"); },
+
+ collab: collab,
+ user: user,
+ preview: preview,
+ project: project,
+ users: users,
+ packages: packages,
+ stats: stats,
+ settings: settings,
+ vfs: vfs
+ });
+
+ register(null, {
+ api: plugin
+ });
+ }
+});
\ No newline at end of file
diff --git a/plugins/c9.core/c9.js b/plugins/c9.core/c9.js
new file mode 100644
index 00000000..0e310961
--- /dev/null
+++ b/plugins/c9.core/c9.js
@@ -0,0 +1,421 @@
+/**
+ * Provides core functionality to Cloud9. This module sets up the plugin
+ * system, event system and settings.
+ *
+ * @module c9.core
+ * @main c9.core
+ */
+define(function(require, module, exports) {
+ main.consumes = ["Plugin", "ext", "vfs"];
+ main.provides = ["c9"];
+ return main;
+
+ function main(options, imports, register) {
+ var Plugin = imports.Plugin;
+ var vfs = imports.vfs;
+
+ /***** Initialization *****/
+
+ var plugin = new Plugin("Ajax.org", main.consumes);
+ var emit = plugin.getEmitter();
+ emit.setMaxListeners(500);
+
+ imports.ext.vfs = imports.vfs;
+
+ var loaded = false;
+ var loggedIn = false;
+ var isReady = false;
+ var state = 0;
+
+ var STORAGE = 1 << 1;
+ var NETWORK = 1 << 2;
+ var PROCESS = 1 << 3;
+ var LOCAL = 1 << 4;
+
+ // Copy configuration settings - To Be Deprecated!
+ var skipProps = { "consumes": 1, "provides": 1, "install": 1, "name": 1 };
+ for (var prop in options) {
+ if (!skipProps[prop])
+ plugin[prop] = options[prop];
+ }
+ var totalLoadTime, startLoadTime;
+
+ function load() {
+ if (loaded) return false;
+ loaded = true;
+
+ loggedIn = parseInt(plugin.uid, 10) > 0;
+
+ if (vfs.connection)
+ setStatus(state | STORAGE | PROCESS);
+ if (vfs.connected)
+ setStatus(state | NETWORK);
+ if (plugin.local)
+ setStatus(state | LOCAL);
+
+ vfs.on("connecting", function() {
+ emit("connecting");
+ }, plugin);
+
+ vfs.on("disconnect", function(reason) {
+ setStatus(state & ~STORAGE & ~PROCESS & ~NETWORK);
+ emit("disconnect");
+ }, plugin);
+
+ vfs.on("connect", function() {
+ setStatus(state | NETWORK | STORAGE | PROCESS);
+ emit("connect");
+ }, plugin);
+
+ vfs.on("error", function(message) {
+ setStatus(state & ~STORAGE & ~PROCESS);
+ // TODO: Don't display all errors?
+ if (emit("showerrormessage", message) !== false) {
+ console.error(
+ "Error on server",
+ "Received following error from server:",
+ JSON.stringify(message.message)
+ );
+ }
+ }, plugin);
+
+ vfs.on("message", function(message) {
+ emit("message", message);
+ }, plugin);
+
+ vfs.on("away", function() {
+ emit("away");
+ }, plugin);
+
+ vfs.on("back", function() {
+ emit("back");
+ }, plugin);
+
+ // Before unload
+ window.addEventListener("beforeunload", beforequit);
+
+ // Unload
+ window.addEventListener("unload", quit);
+ }
+
+ /***** Methods *****/
+
+ function setStatus(s) {
+ state = s;
+ emit("stateChange", {state: s, last: state});
+ }
+
+ function has(check) {
+ return (state & check) ? true : false;
+ }
+
+ function ready(){
+ isReady = true;
+ emit.sticky("ready");
+ }
+
+ function beforequit(){
+ emit("beforequit");
+ }
+
+ function quit(){
+ emit("quit");
+ }
+
+ function toExternalPath(path) {
+ if (plugin.platform == "win32")
+ path = path.replace(/^[/]+/, "").replace(/[/]+/g, "\\");
+ return path;
+ }
+
+ function toInternalPath(path) {
+ if (plugin.platform == "win32") {
+ path = path.replace(/[\\/]+/g, "/");
+ path = path.replace(/^\/*(\w):/, function(_, a) {
+ return "/" + a.toUpperCase() + ":";
+ });
+ }
+ return path;
+ }
+
+ /***** Lifecycle *****/
+
+ plugin.on("load", function(){
+ load();
+ });
+ plugin.on("enable", function(){
+
+ });
+ plugin.on("disable", function(){
+
+ });
+ plugin.on("unload", function(){
+ loaded = false;
+ });
+
+ /***** Register and define API *****/
+
+ /**
+ * Main c9 object for Cloud9 which holds error handlers
+ * of the entire application as well as the state for availability of resources
+ * @singleton
+ **/
+ plugin.freezePublicAPI({
+ /**
+ * use this constant to see if storage capabilities are currently
+ * available. This is relevant for {@link fs}.
+ *
+ * c9.has(c9.STORAGE); // Will return true if storage is available
+ *
+ * @property {Number} STORAGE
+ * @readonly
+ */
+ STORAGE: STORAGE,
+ /**
+ * use this constant to see if network capabilities are currently
+ * available. This is relevant for {@link net#connect}.
+ *
+ * c9.has(c9.NETWORK); // Will return true if network is available
+ *
+ * @property {Number} NETWORK
+ * @readonly
+ */
+ NETWORK: NETWORK,
+ /**
+ * use this constant to see if process control capabilities are
+ * currently available. This is relevant for {@link proc#spawn},
+ * {@link proc#execFile} and {@link proc#pty}.
+ *
+ * c9.has(c9.PROCESS); // Will return true if storage is available
+ *
+ * @property {Number} PROCESS
+ * @readonly
+ */
+ PROCESS: PROCESS,
+ /**
+ * use this constant to see if Cloud9 is running locally and the
+ * local runtime is available.
+ *
+ * c9.has(c9.LOCAL); // Will return true if storage is available
+ *
+ * @property {Number} LOCAL
+ * @readonly
+ */
+ LOCAL: LOCAL,
+
+ /**
+ * @property {String} workspaceDir
+ * @readonly
+ */
+ /**
+ * @property {Boolean} debug
+ * @readonly
+ */
+ /**
+ * @property {Number} sessionId
+ * @readonly
+ */
+ /**
+ * @property {String} workspaceId
+ * @readonly
+ */
+ /**
+ * @property {Boolean} readonly
+ * @readonly
+ */
+ /**
+ * @property {String} projectName
+ * @readonly
+ */
+ /**
+ * @property {String} version
+ * @readonly
+ */
+ /**
+ * @property {Boolean} hosted
+ * @readonly
+ */
+ /**
+ * @property {Boolean} local
+ * @readonly
+ */
+
+ /**
+ * Specifies whether the user is logged in to Cloud9.
+ * @property {Boolean} loggedIn
+ * @readonly
+ */
+ get loggedIn(){ return loggedIn; },
+ /**
+ * the connection object that manages the connection between Cloud9
+ * and the workspace server. Cloud9 uses Engine.IO to manage this
+ * connection.
+ * @property {Object} connection
+ * @readonly
+ */
+ get connection(){ return vfs.connection; },
+ /**
+ * Specifies whether Cloud9 is connceted to the workspace server
+ * @property {Boolean} connected
+ * @readonly
+ */
+ get connected(){ return vfs.connected; },
+ /**
+ * a bitmask of the constants {@link c9#NETWORK}, {@link c9#STORAGE},
+ * {@link c9#PROCESS}, {@link c9#LOCAL}. Use this for complex
+ * queries.
+ * See also: {@link c9#has}
+ *
+ * @property {Number} status
+ * @readonly
+ */
+ get status(){ return state; },
+ /**
+ * the URL from which Cloud9 is loaded.
+ * @property {String} location
+ * @readonly
+ */
+ get location(){ return location && location.href || ""; },
+ /**
+ *
+ */
+ get totalLoadTime(){ return totalLoadTime; },
+ set totalLoadTime(v){ totalLoadTime = v; },
+ /**
+ *
+ */
+ get startLoadTime(){ return startLoadTime; },
+ set startLoadTime(v){ startLoadTime = v; },
+ /**
+ *
+ */
+ get isReady(){ return isReady; },
+
+ _events: [
+ /**
+ * Fires when a javascript exception occurs.
+ * @event error
+ * @param {Object} e
+ * @param {String} e.oldpath
+ */
+ "error",
+ /**
+ * Fires when Cloud9 starts connecting to the workspace server.
+ * @event connecting
+ */
+ "connecting",
+ /**
+ * Fires when Cloud9 is connected to the workspace server.
+ * @event connect
+ */
+ "connect",
+ /**
+ * Fires when Cloud9 is permanently disconnected from the
+ * workspace server.
+ *
+ * @event disconnect
+ */
+ "disconnect",
+ /**
+ * Fires when Cloud9 receives a message from the workspace server.
+ * @event message
+ * @param {String} message the message that is received
+ */
+ "message",
+ /**
+ * Fires when Cloud9 is disconnected from the workspace server.
+ * Cloud9 will try to re-establish the connection with the server
+ * for a few minutes. When that doesn't happen the disconnect
+ * event is fired.
+ * @event away
+ */
+ "away",
+ /**
+ * Fires when Cloud9 is reconnected to a pre-existing session
+ * from which it was temporarily disconnected.
+ * @event back
+ */
+ "back",
+ /**
+ * Fires when there is a connection error
+ * @event showerrormessage
+ * @param {String} message the error message to display
+ */
+ "showerrormessage",
+ /**
+ * Fires when all plugins have loaded
+ * @event ready
+ */
+ "ready",
+ /**
+ * Fires just before exiting the application.
+ * @event beforequit
+ */
+ "beforequit"
+ ],
+
+ /**
+ * Send a message to the statefull server
+ * @param {Object} msg the JSON to send to the client
+ */
+ send: vfs.send,
+
+ /**
+ * Sets the availability of resources. Use bitwise operations to
+ * set availability of different resources. The default
+ * resources are {@link c9#NETWORK}, {@link c9#STORAGE},
+ * {@link c9#PROCESS}, {@link c9#LOCAL}
+ * @param {Number} status a bitwised & of {@link c9#NETWORK},
+ * {@link c9#STORAGE}, {@link c9#PROCESS}, {@link c9#LOCAL}
+ */
+ setStatus: setStatus,
+
+ /**
+ * Checks the availability of resources. Use the following constants
+ * {@link c9#NETWORK}, {@link c9#STORAGE}, {@link c9#PROCESS},
+ * {@link c9#LOCAL}
+ * @param {Number} test one of {@link c9#NETWORK}, {@link c9#STORAGE},
+ * {@link c9#PROCESS}, {@link c9#LOCAL}
+ */
+ has: has,
+
+ /**
+ * This method is called by the boot loader, it triggers the ready
+ * event.
+ *
+ * @private
+ */
+ ready: ready,
+
+ /**
+ * This method is called before exiting cloud9.
+ *
+ * @private
+ */
+ beforequit: beforequit,
+
+ /**
+ * This method is called to exit cloud9.
+ *
+ * @private
+ */
+ quit: quit,
+
+ /**
+ * Canonicalizes a path to its internal form.
+ * For example, turns C:\ into /C:/ on Windows.
+ */
+ toInternalPath: toInternalPath,
+
+ /**
+ * Canonicalizes a path to its external form.
+ * For example, turns /C:/ into C:\ on Windows.
+ */
+ toExternalPath: toExternalPath
+ });
+
+ register(null, {
+ c9: plugin
+ });
+ }
+});
diff --git a/plugins/c9.core/c9_test.js b/plugins/c9.core/c9_test.js
new file mode 100644
index 00000000..bdd0049b
--- /dev/null
+++ b/plugins/c9.core/c9_test.js
@@ -0,0 +1,94 @@
+/*global describe:false, it:false */
+
+"use client";
+
+require(["lib/architect/architect", "lib/chai/chai"], function (architect, chai) {
+ var expect = chai.expect;
+
+ expect.setupArchitectTest([
+ {
+ packagePath: "plugins/c9.core/c9",
+ startdate: new Date(),
+ debug: 2,
+ hosted: true,
+ local: false
+ },
+ "plugins/c9.vfs.client/vfs_client",
+ "plugins/c9.vfs.client/endpoint",
+ "plugins/c9.ide.auth/auth",
+ "plugins/c9.core/api",
+ "plugins/c9.core/ext",
+ "plugins/c9.core/http-xhr",
+ {
+ consumes: [],
+ provides: ["auth.bootstrap", "info", "dialog.error"],
+ setup: expect.html.mocked
+ },
+
+ {
+ consumes: ["c9", "vfs"],
+ provides: [],
+ setup: main
+ }
+ ], architect);
+
+ function main(options, imports, register) {
+ var c9 = imports.c9;
+ var vfs = imports.vfs;
+
+ describe('c9', function() {
+ this.timeout(30000);
+
+ it('should send proper events during connecting', function(done) {
+ // var count = 0;
+
+ // c9.on("connecting", function c1(){
+ // count++;
+
+ // expect(c9.connecting).to.equal(true);
+ // expect(c9.connected).to.equal(false);
+ // expect(c9.has(c9.NETWORK)).to.equal(false);
+
+ // c9.off("connecting", c1);
+ // });
+
+ expect(c9.connected).to.equal(false);
+
+ c9.once("connect", function c2(){
+ // expect(count, "Connecting event was not called").to.equal(1);
+ expect(c9.connected).to.equal(true);
+ expect(c9.has(c9.NETWORK)).to.equal(true);
+
+ done();
+ });
+
+ c9.enable();
+ });
+ it('check status settings and getting', function(done) {
+ c9.setStatus(c9.status & ~c9.STORAGE);
+ expect(c9.has(c9.STORAGE)).to.equal(false);
+ c9.setStatus(c9.status | c9.STORAGE);
+ expect(c9.has(c9.STORAGE)).to.equal(true);
+ done();
+ });
+ it('should send correct events during away', function(done) {
+ expect(c9.connected).to.equal(true);
+ expect(c9.has(c9.NETWORK)).to.equal(true);
+
+ c9.once("away", function c1(){
+ expect(c9.connected).to.equal(false);
+ expect(c9.has(c9.NETWORK)).to.equal(true);
+ });
+ c9.once("back", function c1(){
+ expect(c9.connected).to.equal(true);
+ expect(c9.has(c9.NETWORK)).to.equal(true);
+ done();
+ });
+
+ vfs.connection.socket.close();
+ });
+ });
+
+ onload && onload();
+ }
+});
\ No newline at end of file
diff --git a/plugins/c9.core/client.js b/plugins/c9.core/client.js
new file mode 100644
index 00000000..a6402cd7
--- /dev/null
+++ b/plugins/c9.core/client.js
@@ -0,0 +1,30 @@
+define(function(require, module, exports) {
+ "use strict";
+
+ plugin.consumes = ["auth"];
+ plugin.provides = [
+ "api.client"
+ ];
+
+ return plugin;
+
+ function plugin(options, imports, register) {
+ var assert = require("assert");
+ var createClient = require("frontdoor/lib/api-client");
+
+ assert(options.baseUrl, "Option 'baseUrl' is required");
+
+ var auth = imports.auth;
+
+ var baseUrl = options.baseUrl.replace(/\/$/, "");
+ var descriptionUrl = options.descriptionUrl || baseUrl + "/api.json";
+
+ createClient(descriptionUrl, {
+ request: auth.request
+ }, function(err, client) {
+ register(err, {
+ "api.client": client
+ });
+ });
+ }
+});
\ No newline at end of file
diff --git a/plugins/c9.core/ext.js b/plugins/c9.core/ext.js
new file mode 100644
index 00000000..f5b90e52
--- /dev/null
+++ b/plugins/c9.core/ext.js
@@ -0,0 +1,941 @@
+define(function(require, exports, module) {
+ main.consumes = [];
+ main.provides = ["ext", "Plugin"];
+ return main;
+
+ function main(options, imports, register) {
+ var Emitter = require("events").EventEmitter;
+
+ var plugins = [];
+ var lut = {};
+ var manuallyDisabled = {};
+ var dependencies = {};
+ var counters = {};
+
+ var $id = 1;
+
+ /***** Initialization *****/
+
+ var plugin = new Plugin("Ajax.org", main.consumes);
+ var emit = plugin.getEmitter();
+ var vfs, settings, api;
+
+ plugin.__defineSetter__("vfs", function(remote) {
+ vfs = remote;
+ delete plugin.vfs;
+ });
+
+ plugin.__defineSetter__("settings", function(remote) {
+ settings = remote;
+
+ settings.on("read", function(){
+ var s = settings.getNode("state/ext/counters");
+ for (var type in s) {
+ counters[type] = s[type.substr(1)];
+ }
+ });
+
+ delete plugin.settings;
+ });
+
+ plugin.__defineSetter__("api", function(remote) {
+ api = remote;
+ delete plugin.api;
+ });
+
+ var eventRegistry = Object.create(null);
+
+ /***** Methods *****/
+
+ function uid(type, name) {
+ while (!name || lut[name]) {
+ if (!counters[type]) counters[type] = 0;
+ name = type + counters[type]++;
+ }
+ if (settings && counters[type])
+ settings.set("state/ext/counters/@" + type, counters[type]);
+ return name;
+ }
+
+ function registerPlugin(plugin, loaded) {
+ if (plugins.indexOf(plugin) == -1)
+ plugins.push(plugin);
+ lut[plugin.name] = plugin;
+
+ loaded(true);
+
+ var deps = plugin.deps;
+ if (deps) {
+ deps.forEach(function(dep) {
+ // if (dep !== plugin.name) throw new Error(dep);
+ (dependencies[dep]
+ || (dependencies[dep] = {}))[plugin.name] = 1;
+ });
+ }
+
+ emit("register", {plugin: plugin});
+ }
+
+ function unregisterPlugin(plugin, loaded, ignoreDeps, keep) {
+ if (!plugin.registered)
+ return;
+
+ if (!ignoreDeps && getDependencies(plugin.name).length) {
+ //@todo this should be moved to whoever is calling this.
+ // if (!silent)
+ // util.alert(
+ // "Could not disable extension",
+ // "Extension is still in use",
+ // "This extension cannot be disabled, because it is still in use by the following plugins:
"
+ // + " - " + usedBy.join("
- ")
+ // + "
Please disable those plugins first.");
+ return false;
+ }
+
+ if (!keep)
+ plugins.splice(plugins.indexOf(plugin), 1);
+ delete lut[plugin.name];
+
+ loaded(false, 0);
+
+ var deps = plugin.deps;
+ if (deps && dependencies) {
+ deps.forEach(function(dep) {
+ delete dependencies[dep][plugin.name];
+ });
+ }
+
+ emit("unregister", {plugin: plugin});
+ }
+
+ function getDependencies(pluginName){
+ var usedBy = [];
+
+ // Check for dependencies needing this plugin
+ if (dependencies) {
+ var deps = dependencies[pluginName];
+ if (deps) {
+ Object.keys(deps).forEach(function(name) {
+ usedBy.push(name);
+ });
+ }
+ }
+
+ return usedBy;
+ }
+
+ function unloadAllPlugins(exclude) {
+ if (lut.settings)
+ lut.settings.unload(null, true);
+
+ function unload(plugin) {
+ if (!plugin || exclude && exclude[plugin.name])
+ return;
+
+ var deps = dependencies[plugin.name];
+ if (deps) {
+ Object.keys(deps).forEach(function(name) {
+ unload(lut[name]);
+ });
+ }
+
+ plugin.unload(null, true);
+ }
+
+ var list = plugins.slice(0);
+ for (var i = list.length - 1; i >= 0; i--) {
+ var plugin = list[i];
+ if (!plugin.loaded) continue;
+
+ if (plugin.unload)
+ unload(plugin);
+ else
+ console.warn("Ignoring not a plugin: " + plugin.name);
+ }
+ }
+
+ function loadRemotePlugin(id, options, callback) {
+ vfs.extend(id, options, function(err, meta) {
+ callback(err, meta && meta.api);
+ });
+ }
+
+ function fetchRemoteApi(id, callback) {
+ vfs.use(id, {}, function(err, meta) {
+ callback(err, meta && meta.api);
+ });
+ }
+
+ function unloadRemotePlugin(id, options, callback) {
+ if (typeof options == "function") {
+ callback = options;
+ options = {};
+ }
+ vfs.unextend(id, options, callback);
+ }
+
+ function enablePlugin(name){
+ if (!lut[name] && !manuallyDisabled[name])
+ throw new Error("Could not find plugin: " + name);
+ (lut[name] || manuallyDisabled[name]).load(name);
+ }
+
+ function disablePlugin(name){
+ if (!lut[name])
+ throw new Error("Could not find plugin: " + name);
+
+ var plugin = lut[name];
+ if (plugin.unload({ keep: true }) === false)
+ throw new Error("Failed unloading plugin: " + name);
+
+ manuallyDisabled[name] = plugin;
+ }
+
+ /***** Register and define API *****/
+
+ /**
+ * The Cloud9 Extension Manager
+ * @singleton
+ */
+ plugin.freezePublicAPI({
+ /**
+ *
+ */
+ get plugins(){ return plugins.slice(0); },
+
+ /**
+ *
+ */
+ get named(){
+ var named = Object.create(lut);
+ for (var name in manuallyDisabled) {
+ if (!lut[name])
+ lut[name] = manuallyDisabled[name];
+ }
+ return named;
+ },
+
+ _events: [
+ /**
+ * Fires when a plugin registers
+ * @event register
+ * @param {Object} e
+ * @param {Plugin} e.plugin the plugin that registers
+ */
+ "register",
+ /**
+ * Fires when a plugin unregisters
+ * @event unregister
+ * @param {Object} e
+ * @param {Plugin} e.plugin the plugin that unregisters
+ */
+ "unregister"
+ ],
+
+ /**
+ * Loads a plugin on the remote server. This plugin can be either
+ * source that we have local, or a path to a file that already
+ * exists on the server. The plugin provides an api that is returned
+ * in the callback. The remote plugin format is very simple. Here's
+ * an example of a Math module:
+ *
+ * #### Math Modules
+ *
+ * module.exports = function (vfs, options, register) {
+ * register(null, {
+ * add: function (a, b, callback) {
+ * callback(null, a + b);
+ * },
+ * multiply: function (a, b, callback) {
+ * callback(null, a * b);
+ * }
+ * });
+ * };
+ *
+ * @param {String} id A unique identifier for this module
+ * @param {Object} options Options to specify
+ * @param {String} [options.code] The implementation of a module, e.g. require("text!./my-service.js").
+ * @param {String} [options.file] An absolute path to a module on the remote disk
+ * @param {Boolean} [options.redefine] specifying whether to replace an existing module with the same `id`
+ * @param {Function} callback called when the code has been loaded.
+ * @param {Error} callback.err The error object if an error has occured.
+ * @param {Object} callback.api The api the code that loaded defined.
+ *
+ */
+ loadRemotePlugin: loadRemotePlugin,
+
+ /**
+ *
+ */
+ fetchRemoteApi: fetchRemoteApi,
+
+ /**
+ * Unloads a plugin loaded with loadRemotePlugin
+ * @param {String} id The unique identifier for this module
+ * @param {Function} callback
+ */
+ unloadRemotePlugin: unloadRemotePlugin,
+
+ /**
+ *
+ */
+ unloadAllPlugins: unloadAllPlugins,
+
+ /**
+ *
+ */
+ getDependencies: getDependencies,
+
+ /**
+ *
+ */
+ enablePlugin: enablePlugin,
+
+ /**
+ *
+ */
+ disablePlugin: disablePlugin
+ });
+
+ function Plugin(developer, deps) {
+ var elements = [];
+ var names = {};
+ var waiting = {};
+ var events = [];
+ var other = [];
+ var name = "";
+ var time = 0;
+ var registered = false;
+ var loaded = false;
+ var event = new Emitter();
+ var disabled = false;
+ var onNewEvents = {};
+ var declaredEvents = [];
+
+ this.deps = deps;
+ this.developer = developer;
+
+ event.on("newListener", function(type, listener) {
+ if (!(type in onNewEvents))
+ return;
+
+ var data = onNewEvents[type];
+ if (data === -1)
+ event.emit("$event." + type, listener);
+ else {
+ listener(onNewEvents[type]);
+
+ if (event.listeners(type).indexOf(listener) > -1)
+ console.trace("Used 'on' instead of 'once' to "
+ + "listen to sticky event " + name + "." + type);
+ }
+ });
+
+ function init(reg) {
+ registered = reg;
+ }
+
+ /***** Methods *****/
+
+ this.getEmitter = function(){
+ var emit = event.emit.bind(event);
+
+ var _self = this;
+ var sticky = function(name, e, plugin) {
+ if (plugin) {
+ _self.on("$event." + name, function(listener){
+ listener(e);
+ }, plugin);
+ onNewEvents[name] = -1;
+ }
+ else {
+ onNewEvents[name] = e;
+ }
+ return emit(name, e);
+ };
+
+ function unsticky(name, e) {
+ delete onNewEvents[name];
+ }
+
+ emit.listeners = event.listeners.bind(event);
+ emit.setMaxListeners = event.setMaxListeners.bind(event);
+ emit.sticky = sticky;
+ emit.unsticky = unsticky;
+
+ return emit;
+ };
+
+ this.freezePublicAPI = function(api) {
+ // Build a list of known events to warn users if they use a
+ // non-existent event.
+ if (api._events) {
+ api._events.forEach(function(name){
+ declaredEvents[name] = true;
+ });
+ delete api._events;
+ }
+
+ // Reverse prototyping of the API
+ // if (!this.__proto__) {
+ // modifying __proto__ is very slow on chrome!
+ Object.keys(api).forEach(function(key) {
+ var d = Object.getOwnPropertyDescriptor(api, key);
+ Object.defineProperty(this, key, d);
+ }, this);
+ // }
+ // else {
+ // api.__proto__ = this.__proto__;
+ // this.__proto__ = api;
+ // Object.freeze(api);
+ // }
+
+ if (!baseclass) {
+ delete this.baseclass;
+ delete this.freezePublicAPI.baseclass;
+ delete this.freezePublicAPI;
+ delete this.setAPIKey;
+ delete this.getEmitter;
+ Object.freeze(this);
+ }
+ baseclass = false;
+
+ return this;
+ };
+ var baseclass;
+ this.baseclass =
+ this.freezePublicAPI.baseclass = function(){ baseclass = true; };
+
+ function getElement(name, callback) {
+ // remove id's after storing them.
+ if (!callback) {
+ // If we run without APF, just return a simple object
+ if (typeof apf == "undefined")
+ return {};
+
+ if (!names[name]) {
+ throw new Error("Could not find AML element by name '"
+ + name + "'");
+ }
+
+ return names[name];
+ }
+ else {
+ if (names[name]) callback(names[name]);
+ else {
+ (waiting[name] || (waiting[name] = [])).push(callback);
+ }
+ }
+ }
+
+ function addElement() {
+ for (var i = 0; i < arguments.length; i++) {
+ var node = arguments[i];
+ elements.push(node);
+ recur(node);
+ }
+
+ function recur(node) {
+ (node.childNodes || []).forEach(recur);
+ var id = node.id;
+ if (!id)
+ return;
+ // Delete their global reference
+ delete window[id];
+ // delete apf.nameserver.lookup.all[node.id];
+
+ // Keep their original name in a lookup table
+ names[id] = node;
+
+ // Set a new unique id
+ if (node.localName != "page") { // Temp hack, should fix in tabs
+ node.id = "element" + node.$uniqueId;
+ apf.nameserver.lookup.all[node.id] = node;
+ }
+
+ // Call all callbacks waiting for this element
+ if (waiting[id]) {
+ waiting[id].forEach(function(callback) {
+ callback(node);
+ });
+ delete waiting[id];
+ }
+ }
+
+ return arguments[0];
+ }
+
+ function addEvent(emitter, type, listener) {
+ if (!listener.listenerId)
+ listener.listenerId = $id++;
+ events.push([emitter.name, type, listener.listenerId]);
+ }
+
+ function addOther(o) {
+ other.push(o);
+ }
+
+ function initLoad(type, listener) {
+ if (type == "load") listener();
+ }
+
+ function load(nm, type) {
+ var dt = Date.now();
+
+ if (type) nm = uid(type, nm);
+ if (nm && !name) name = nm;
+ event.name = name;
+ eventRegistry[name] = event;
+ registerPlugin(this, init);
+ loaded = true;
+
+ event.emit("load");
+ event.on("newListener", initLoad);
+
+ time = Date.now() - dt;
+ }
+
+ function enable() {
+ emit("enablePlugin", {plugin: this});
+ event.emit("enable");
+ disabled = false;
+ }
+
+ function disable() {
+ emit("disablePlugin", {plugin: this});
+ event.emit("disable");
+ disabled = true;
+ }
+
+ function unload(e, ignoreDeps) {
+ if (!loaded) return;
+
+ if (event.emit("beforeUnload", e) === false)
+ return false;
+
+ if (unregisterPlugin(this, init, ignoreDeps, e && e.keep) === false)
+ return false;
+
+ loaded = false;
+
+ event.emit("unload", e);
+
+ this.cleanUp();
+
+ event.off("newListener", initLoad);
+ setTimeout(function() {
+ if (eventRegistry[name] == event && !loaded) {
+ delete eventRegistry[name];
+ }
+ });
+ }
+
+ function cleanUp(keepElements) {
+ if (!keepElements) {
+ // Loop through elements
+ elements.forEach(function(element) {
+ element.destroy(true, true);
+ });
+ elements = [];
+ names = {};
+ waiting = [];
+ }
+
+ // Loop through events
+ events.forEach(function(eventRecord) {
+ var event = eventRegistry[eventRecord[0]];
+ if (!event) return; // this happens with mock plugins during testing
+ var type = eventRecord[1];
+ var id = eventRecord[2];
+ var _events = event._events;
+ var eventList = _events && _events[type];
+ if (typeof eventList == "function") {
+ if (eventList.listenerId == id)
+ event.off(type, eventList);
+ } else if (Array.isArray(eventList)) {
+ eventList.some(function(listener) {
+ if (listener.listenerId != id) return;
+ event.off(type, listener);
+ return true;
+ });
+ }
+ });
+ events = [];
+
+ // Loop through other
+ other.forEach(function(o) {
+ o();
+ });
+ other = [];
+
+ onNewEvents = {};
+ }
+
+ function setAPIKey(apikey){
+ // Validate Key
+ if (!apikey || !apikey.match(/[\w+]{27}=/))
+ throw new Error("Invalid API key");
+
+ return {
+ getPersistentData: getPersistentData.bind(this, apikey),
+ setPersistentData: setPersistentData.bind(this, apikey)
+ };
+ }
+
+ function getPersistentData(apiKey, context, callback){
+ var type;
+
+ if (!apiKey)
+ throw new Error("API Key not set. Please call plugin.setAPIKey(options.key);");
+
+ if (context == "user") type = "user";
+ else if (context == "workspace") type = "project";
+ else throw new Error("Unsupported context: " + context);
+
+ api[type].get("persistent/" + apiKey, function(err, data){
+ if (err) return callback(err);
+ try { callback(null, JSON.stringify(data)); }
+ catch(e){ return callback(e); }
+ });
+ }
+
+ function setPersistentData(apiKey, context, data, callback){
+ var type;
+
+ if (!apiKey)
+ throw new Error("API Key not set. Please call plugin.setAPIKey(options.key);");
+
+ if (context == "user") type = "user";
+ else if (context == "workspace") type = "project";
+ else throw new Error("Unsupported context: " + context);
+
+ api[type].put("persistent/" + apiKey, { data: JSON.stringify(data) }, callback);
+ }
+
+ /***** Register and define API *****/
+
+ this.baseclass();
+
+ /**
+ * Base class for all Plugins of Cloud9. A Cloud9 Plugin is
+ * an instance of the Plugin class. This class offers ways to
+ * describe the API it offers as well as ways to clean up the
+ * objects created during the lifetime of the plugin.
+ *
+ * Note that everything in Cloud9 is a plugin. This means that
+ * your plugins have the exact same possibilities as any other part
+ * of Cloud9. When building plugins you can simply create additional
+ * features or replace existing functionality, by turning off the
+ * core plugins and preference of your own.
+ *
+ * Check out the [template](http://example.org/template) for the
+ * recommended way of building a plugin. All Cloud9 Core Plugins
+ * are written in this way.
+ *
+ * Our goal has been to create an extensible system that works both
+ * in the browser as well as in Node.js. The Cloud9 CLI uses the
+ * exact same plugin structure as the Cloud9 in the browser. The
+ * same goes for many of the Cloud9 platform services. We focussed
+ * on making the system easy to use by making sure you can create
+ * plugins using simple javascript, html and css. The plugins you
+ * create will become available as services inside the Cloud9
+ * plugin system. This means that other plugins can consume your
+ * functionality and importantly you can replace existing services
+ * by giving your plugin the same service name.
+ *
+ * The plugin class will allow you to specify an API that is
+ * "frozen" upon definition (See {@link Object#freeze}. This means
+ * that once your plugin's API is defined the object's interface
+ * cannot be changed anymore. Property gettters and setters will
+ * still work and events can still be set/unset. Having immutable
+ * APIs will prevent users from common hacks that devs often use
+ * in the javascript community, adding new properties to objects.
+ * It is our aim that this will increase the stability of the system
+ * as a whole while introducing foreign plugins to it.
+ *
+ * The event flow of a basic plugin is as follows:
+ *
+ * * {@link #event-load} - *The plugin is loaded (this can happen multiple times to the same plugin instance)*
+ * * {@link #event-unload} - *The plugin is unloaded*
+ *
+ * #### User Actions:
+ *
+ * * {@link #event-disable} - *The plugin is disabled*
+ * * {@link #event-enable} - *The plugin is enabled*
+ *
+ * The following example shows how to implement a basic plugin:
+ *
+ * define(function(require, exports, module) {
+ * main.consumes = ["dependency"];
+ * main.provides = ["myplugin"];
+ * return main;
+ *
+ * function main(options, imports, register) {
+ * var dependency = imports.dependency;
+ *
+ * var plugin = new Plugin("(Company) Name", main.consumes);
+ * var emit = plugin.getEmitter();
+ *
+ * plugin.on("load", function(e) {
+ * // Create a command, menu item, etc
+ * });
+ * plugin.on("unload", function(e) {
+ * // Any custom unload code (most things are cleaned up automatically)
+ * });
+ *
+ * function doSomething(){
+ * }
+ *
+ * plugin.freezePublicAPI({
+ * doSomething : doSomething
+ * });
+ * }
+ * });
+ *
+ * @class Plugin
+ * @extends Object
+ */
+ /**
+ * @constructor
+ * Creates a new Plugin instance.
+ * @param {String} developer The name of the developer of the plugin
+ * @param {String[]} deps A list of dependencies for this
+ * plugin. In most cases it's a reference to main.consumes.
+ */
+ this.freezePublicAPI({
+ _events: [
+ /**
+ * Fires when the plugin is loaded
+ * @event load
+ */
+ "load",
+ /**
+ * Fires before the plugin is unloaded
+ * @event beforeUnload
+ */
+ "beforeUnload",
+ /**
+ * Fires when the plugin is unloaded
+ * @event unload
+ */
+ "unload",
+ /**
+ * Fires when the plugin is enabled
+ * @event enable
+ */
+ "enable",
+ /**
+ * Fires when the plugin is disabled
+ * @event disable
+ */
+ "disable",
+ /**
+ * Fires any time a new listener is added.
+ *
+ * plugin.on('newListener', function (event, listener) {
+ * // new listener added
+ * });
+ *
+ * @event newListener
+ */
+ "newListener",
+ /**
+ * Fires any time a listener is removed.
+ *
+ * plugin.on('removeListener', function (event, listener) {
+ * // listener is removed
+ * });
+ *
+ * @event removeListener
+ */
+ "removeListener"
+ ],
+
+ /**
+ * @property {Boolean} registered Specifies whether the plugin is registered
+ * @readonly
+ */
+ get registered(){ return registered; },
+ /**
+ * @property {Date} time The time when the plugin was registered
+ * @readonly
+ */
+ get time(){ return time; },
+ /**
+ * @property {Boolean} enabled Specifies whether the plugin is enabled
+ * @readonly
+ */
+ get enabled(){ return !disabled; },
+ /**
+ * @property {Boolean} loaded whether the plugin is loaded.
+ * This happens by calling load or setting the name.
+ * @readonly
+ */
+ get loaded(){ return loaded; },
+ /**
+ * @property {String} name The name of the plugin
+ */
+ get name(){ return name; },
+ set name(val) {
+ if (name == val)
+ return;
+
+ if (!name) {
+ name = val;
+ this.load();
+ }
+ else
+ throw new Error("Plugin Name Exception");
+ },
+
+ /**
+ * Copies all methods and properties from `api` and then freezes
+ * the plugin to prevent further changes to its API.
+ *
+ * @method freezePublicAPI
+ * @param {Object} api simple object that defines the API
+ **/
+
+ /**
+ * Fetches the event emitter for this plugin. After freezing the
+ * public API of this plugin, this method will no longer be
+ * available.
+ *
+ * Note that there is a limit to the amount of event listeners
+ * that can be placed on a plugin. This is to protect developers
+ * from leaking event listeners. When this limit is reached
+ * an error is thrown. Use the emit.setMaxListeners
+ * to change the amount of listeners a plugin can have:
+ *
+ * var emit = plugin.getEmitter();
+ * emit.setMaxListeners(100);
+ *
+ * @method getEmitter
+ * @return {Function}
+ * @return {String} return.eventName The name of the event to emit
+ * @return {Object} return.eventObject The data passed as the first argument to all event listeners
+ * @return {Boolean} return.immediateEmit Specifies whether
+ * to emit the event to event handlers that are set after
+ * emitting this event. This is useful for instance when you
+ * want to have a "load" event that others add listeners to
+ * after the load event is called.
+ **/
+
+ /**
+ * Fetches a UI element. You can use this method both sync and async.
+ * @param {String} name the id of the element to fetch
+ * @param {Function} [callback] the function to call when the
+ * element is available (could be immediately)
+ **/
+ getElement: getElement,
+
+ /**
+ * Register an element for destruction during the destroy phase of
+ * this plugin's lifecycle.
+ * @param {AMLElement} element the element to register
+ **/
+ addElement: addElement,
+
+ /**
+ * Register an event for destruction during the destroy phase of
+ * this plugin's lifecycle.
+ * @param {Array} ev Array containing three elements:
+ * object, event-name, callback
+ **/
+ addEvent: addEvent,
+
+ /**
+ * Register a function that is called during the destroy phase of
+ * this plugin's lifecycle.
+ * @param {Function} o function called during the destroy phase
+ * of this plugin's lifecycle
+ **/
+ addOther: addOther,
+
+ /**
+ * Loads this plugin into Cloud9
+ **/
+ load: load,
+
+ /**
+ * @ignore Enable this plugin
+ **/
+ enable: enable,
+
+ /**
+ * @ignore Disable this plugin
+ **/
+ disable: disable,
+
+ /**
+ * Unload this plugin from Cloud9
+ **/
+ unload: unload,
+
+ /**
+ * Removes all elements, events and other items registered for
+ * cleanup by this plugin
+ */
+ cleanUp: cleanUp,
+
+ /**
+ *
+ */
+ setAPIKey: setAPIKey,
+
+ /**
+ * Adds an event handler to this plugin. Note that unlike the
+ * event implementation you know from the browser, you are
+ * able to add the same listener multiple times to listen to
+ * the same event.
+ *
+ * @param {String} name The name of this event
+ * @param {Function} callback The function called when the event is fired
+ * @param {Plugin} plugin The plugin that is responsible
+ * for the event listener. Make sure to always add a reference
+ * to a plugin when adding events in order for the listeners
+ * to be cleaned up when the plugin unloads. If you forget
+ * this you will leak listeners into Cloud9.
+ * @fires newListener
+ **/
+ on: function(eventName, callback, plugin){
+ // if (!declaredEvents[eventName])
+ // console.warn("Missing event description or unknown event '" + eventName + "' for plugin '" + name + "'", new Error().stack);
+
+ event.on(eventName, callback, plugin);
+ },
+
+ /**
+ * Adds an event handler to this plugin and removes it after executing it once
+ * @param {String} name the name of this event
+ * @param {Function} callback the function called when the event is fired
+ **/
+ once: function(eventName, callback){
+ // if (!declaredEvents[eventName])
+ // console.warn("Missing event description or unknown event '" + eventName + "' for plugin '" + name + "'");
+
+ event.once(eventName, callback);
+ },
+
+ /**
+ * Removes an event handler from this plugin
+ * @param {String} name the name of this event
+ * @param {Function} callback the function previously registered as event handler
+ * @fires removeListener
+ **/
+ off: event.removeListener.bind(event),
+
+ /**
+ * Returns an array of listeners for an event specified by `name`
+ * @param {String} name the name of this event
+ */
+ listeners: event.listeners.bind(event)
+ });
+ }
+
+ register(null, {
+ ext: plugin,
+ Plugin: Plugin
+ });
+ }
+});
diff --git a/plugins/c9.core/ext_test.js b/plugins/c9.core/ext_test.js
new file mode 100644
index 00000000..89d325a1
--- /dev/null
+++ b/plugins/c9.core/ext_test.js
@@ -0,0 +1,218 @@
+/*global describe:false, it:false */
+
+"use client";
+
+require(["lib/architect/architect", "lib/chai/chai"], function (architect, chai) {
+ var expect = chai.expect;
+
+ expect.setupArchitectTest([
+ "plugins/c9.core/ext",
+ {
+ consumes: ["ext", "Plugin"],
+ provides: [],
+ setup: main
+ }
+ ], architect);
+
+ function main(options, imports, register) {
+ var ext = imports.ext;
+ var Plugin = imports.Plugin;
+
+ describe('plugin', function() {
+ this.timeout(1000);
+
+ it('should expose the constructor arguments', function(done) {
+ var deps = [1,2];
+ var plugin = new Plugin("Ajax.org", deps);
+
+ expect(plugin.developer).to.equal("Ajax.org");
+ expect(plugin.deps).to.equal(deps);
+
+ done();
+ });
+ it('should only allow setting the api once', function(done) {
+ var plugin = new Plugin("Ajax.org", []);
+
+ var func = function(a) {};
+ plugin.freezePublicAPI({
+ test: func
+ });
+
+ plugin.test = "nothing";
+ expect(plugin.test).to.equal(func);
+
+ done();
+ });
+ it('should give access to the event emitter before freezing the api', function(done) {
+ var plugin = new Plugin("Ajax.org", []);
+ var emit = plugin.getEmitter();
+ plugin.freezePublicAPI({});
+ plugin.on("test", function(){ done(); })
+ emit("test");
+ });
+ it('should not give access to the event emitter after freezing the api', function(done) {
+ var plugin = new Plugin("Ajax.org", []);
+ plugin.freezePublicAPI({});
+ expect(plugin.getEmitter).to.not.ok
+ done();
+ });
+ it('should call load event when name is set', function(done) {
+ var plugin = new Plugin("Ajax.org", []);
+ plugin.on("load", function(){ done() });
+ plugin.name = "test";
+ });
+ it('should only allow the name to be set once', function(done) {
+ var plugin = new Plugin("Ajax.org", []);
+ plugin.name = "test";
+ expect(function(){ plugin.name = "test2";}).to.throw("Plugin Name Exception");
+ done();
+ });
+ it('should call sticky event when adding handler later', function(done) {
+ var plugin = new Plugin("Ajax.org", []);
+ var emit = plugin.getEmitter();
+ plugin.name = "test";
+ emit.sticky("ready");
+ plugin.on("ready", done);
+ });
+ it('should call sticky event for each plugin added', function(done) {
+ var plugin = new Plugin("Ajax.org", []);
+ var plugin2 = new Plugin("Ajax.org", []);
+ var plugin3 = new Plugin("Ajax.org", []);
+ var emit = plugin.getEmitter();
+ plugin.name = "test";
+ plugin2.name = "test2";
+ plugin3.name = "test3";
+ emit.sticky("create", {}, plugin2);
+ emit.sticky("create", {}, plugin3);
+
+ var z = 0;
+ plugin.on("create", function(){
+ if (++z == 2) done();
+ else if (z > 2)
+ throw new Error("Called too often initially");
+ });
+ });
+ it('should call sticky event only for the non-unloaded plugins', function(done) {
+ var plugin = new Plugin("Ajax.org", []);
+ var plugin2 = new Plugin("Ajax.org", []);
+ var plugin3 = new Plugin("Ajax.org", []);
+ var emit = plugin.getEmitter();
+ plugin.name = "test";
+ plugin2.name = "test2";
+ plugin3.name = "test3";
+ emit.sticky("create", {}, plugin2);
+ emit.sticky("create", {}, plugin3);
+
+ var z = 0, q = 0, timer;
+ plugin.on("create", function(){
+ if (++z == 2) {
+ plugin3.unload();
+
+ plugin.on("create", function(){
+ ++q;
+ clearTimeout(timer);
+ timer = setTimeout(function(){
+ if (q == 1) done();
+ else throw new Error("Called too often after unload");
+ })
+ });
+ }
+ else if (z > 2) {
+ throw new Error("Called too often initially");
+ }
+ });
+ });
+ it('should call unload event when unload() is called', function(done) {
+ var plugin = new Plugin("Ajax.org", []);
+ var loaded = false;
+ plugin.on("unload", function error(){
+ if (!loaded)
+ throw new Error("shouldn't call unload");
+ done();
+ });
+ plugin.unload();
+ loaded = true;
+ plugin.load();
+ plugin.unload();
+ });
+ it('should call disable event when disable() is called', function(done) {
+ var plugin = new Plugin("Ajax.org", []);
+ plugin.on("disable", function(){ done() });
+ plugin.enable();
+ plugin.disable();
+ });
+ it('should call enable event when enable() is called', function(done) {
+ var plugin = new Plugin("Ajax.org", []);
+ plugin.on("enable", function(){ done() });
+ plugin.enable();
+ });
+ it('should destroy all assets when it\'s unloaded', function(done) {
+ var plugin = new Plugin("Ajax.org", []);
+
+ var count = 0;
+ function check(){
+ if (++count == 4)
+ done();
+ }
+
+ var el1 = {destroy: check, childNodes: []};
+ var el2 = {destroy: check, childNodes: []};
+
+ plugin.load();
+
+ plugin.on("load", check);
+ expect(plugin.listeners("load").length).to.equal(1);
+
+ plugin.addElement(el1, el2);
+ plugin.addEvent(plugin, "load", check);
+ plugin.addOther(check);
+
+ plugin.unload();
+
+ if (!plugin.listeners("load").length)
+ check();
+ });
+
+ //@todo haven't tested getElement
+ });
+
+ describe('ext', function() {
+ it('should register a plugin when the plugin\'s name is set', function(done) {
+ var plugin = new Plugin("Ajax.org", []);
+ expect(plugin.registered).to.equal(false);
+
+ ext.on("register", function reg(){
+ expect(plugin.registered).to.equal(true);
+ done();
+ ext.off("register", reg);
+ })
+
+ plugin.name = "test";
+ });
+ it('should call the unregister event when the plugin is unloaded', function(done) {
+ var plugin = new Plugin("Ajax.org", []);
+ plugin.name = "test";
+
+ ext.on("unregister", function unreg(){
+ expect(plugin.registered).to.equal(false);
+ done();
+ ext.off("register", unreg);
+ })
+
+ plugin.unload();
+ });
+ it('should return false on unload() when the dependency tree is not in check', function(done) {
+ var plugin = new Plugin("Ajax.org", []);
+ plugin.name = "test";
+ var plugin2 = new Plugin("Ajax.org", ["test"]);
+ plugin2.name = "test2";
+
+ expect(plugin.unload()).to.equal(false);
+
+ done();
+ });
+ });
+
+ onload && onload();
+ }
+});
\ No newline at end of file
diff --git a/plugins/c9.core/http-node.js b/plugins/c9.core/http-node.js
new file mode 100644
index 00000000..c2ae4a60
--- /dev/null
+++ b/plugins/c9.core/http-node.js
@@ -0,0 +1,55 @@
+"use strict";
+
+main.consumes = [];
+main.provides = ["http"];
+
+module.exports = main;
+
+function main(options, imports, register) {
+ var request = require("frontdoor/lib/http_node");
+
+ /**
+ * Simple API for performing HTTP requests.
+ *
+ * Example:
+ *
+ * http.request("http://www.c9.io", function(err, data){
+ * if (err) throw err;
+ * console.log(data);
+ * });
+ *
+ * @singleton
+ */
+ var plugin = {
+ /**
+ * Performs an HTTP request
+ *
+ * @param {String} url Target URL for the HTTP request
+ * @param {Object} [options] Request options
+ * @param {String} [options.method] HTTP method (default=GET)
+ * @param {Object} [options.query] URL query parameters as an object
+ * @param {String} [options.body] HTTP body for PUT and POST
+ * @param {Object} [options.headers] Request headers
+ * @param {Object} [options.username] Basic auth username
+ * @param {Object} [options.password] Basic auth password
+ * @param {Number} [options.timeout] Timeout in ms (default=10000)
+ * @param {String} [options.contentType='application/x-www-form-urlencoded; charset=UTF-8'] Content type of sent data
+ * @param {String} [options.overrideMimeType] Overrides the MIME type returned by the server
+ * @param {Function} [options.progress] Progress event handler
+ * @param {Function} [options.progress.loaded] The amount of bytes downloaded/uploaded.
+ * @param {Function} [options.progress.total] The total amount of bytes to download/upload.
+ * @param {Function} callback Called when the request returns.
+ * @param {Error} callback.err Error object if an error occured.
+ * @param {String} callback.data The data received.
+ * @param {Object} callback.res
+ * @param {String} callback.res.body The body of the response message.
+ * @param {Number} callback.res.status The status of the response message.
+ * @param {Object} callback.res.headers The headers of the response message.
+ */
+ request: request
+ };
+
+ register(null, {
+ http: plugin
+ });
+}
\ No newline at end of file
diff --git a/plugins/c9.core/http-xhr.js b/plugins/c9.core/http-xhr.js
new file mode 100644
index 00000000..3144aaff
--- /dev/null
+++ b/plugins/c9.core/http-xhr.js
@@ -0,0 +1,268 @@
+define(function(require, module, exports) {
+ "use strict";
+
+ var XHR = XMLHttpRequest; // Grab constructor early so it can't be spoofed
+
+ main.consumes = ["Plugin"];
+ main.provides = ["http"];
+ return main;
+
+ function main(options, imports, register) {
+ var URL = require("url");
+ var qs = require("querystring");
+
+ var plugin = new imports.Plugin("Ajax.org", main.consumes);
+ var debug = options.debug !== false;
+
+ function request(url, options, callback) {
+ if (!callback)
+ return request(url, {}, options);
+
+ if (typeof options == "string")
+ return request(url, {method: options}, callback);
+
+ var method = options.method || "GET";
+ var headers = options.headers || {};
+ var body = options.body || "";
+ var contentType = options.contentType
+ || "application/x-www-form-urlencoded; charset=UTF-8";
+ var timeout = options.hasOwnProperty("timeout") ? options.timeout : 10000;
+ var async = options.sync !== true;
+ var parsedUrl = parseUrl(url, options.query);
+ if (contentType === "application/json")
+ headers.Accept = headers.Accept || "application/json";
+
+ if (options.username) {
+ headers.Authorization = "Basic " + btoa(options.username + ":" + options.password);
+ }
+
+ var xhr = new XHR();
+
+ if (options.overrideMimeType)
+ xhr.overrideMimeType = options.overrideMimeType;
+
+ // From MDN: Note: You need to add the event listeners before
+ // calling open() on the request. Otherwise the progress events
+ // will not fire.
+ if (options.progress) {
+ var obj = method == "PUT" ? xhr.upload : xhr;
+ obj.onprogress = function(e) {
+ if (e.lengthComputable)
+ options.progress(e.loaded, e.total);
+ };
+ }
+
+ xhr.open(method, URL.format(parsedUrl), async);
+ headers["Content-Type"] = contentType;
+ for (var header in headers)
+ xhr.setRequestHeader(header, headers[header]);
+
+ // encode body
+ if (typeof body == "object") {
+ if (contentType.indexOf("application/json") === 0) {
+ try {
+ body = JSON.stringify(body);
+ } catch (e) {
+ return done(new Error("Could not serialize body as json"));
+ }
+ }
+ else if (contentType.indexOf("application/x-www-form-urlencoded") === 0) {
+ body = qs.stringify(body);
+ }
+ else if (["[object File]", "[object Blob]"].indexOf(Object.prototype.toString.call(body)) > -1) {
+ // pass as is
+ xhr.overrideMimeType = body.type;
+ }
+ else {
+ body = body.toString();
+ }
+ }
+
+ var timer;
+ var timedout = false;
+ if (timeout) {
+ timer = setTimeout(function() {
+ timedout = true;
+ xhr.abort();
+ var err = new Error("Timeout");
+ err.code = "ETIMEOUT";
+ done(err);
+ }, timeout);
+ }
+
+ xhr.send(body || "");
+
+ var abort = xhr.abort;
+ xhr.abort = function(){
+ clearTimeout(timer);
+ abort.call(xhr);
+ };
+
+ xhr.onload = function(e) {
+ var res = {
+ body: xhr.responseText,
+ status: xhr.status,
+ headers: parseHeaders(xhr.getAllResponseHeaders())
+ };
+
+ var data = xhr.responseText;
+ var contentType = options.overrideMimeType || res.headers["content-type"] || "";
+ if (contentType.indexOf("application/json") === 0) {
+ try {
+ data = JSON.parse(data);
+ } catch (e) {
+ return done(e);
+ }
+ }
+
+ if (this.status > 299) {
+ var err = new Error(xhr.responseText);
+ err.code = xhr.status;
+ if (debug)
+ console.error("HTTP error " + this.status + ": " + xhr.responseText);
+ return done(err, data, res);
+ }
+
+ done(null, data, res);
+ };
+
+ xhr.onerror = function(e) {
+ if (typeof XMLHttpRequestProgressEvent == "function" && e instanceof XMLHttpRequestProgressEvent) {
+ // No useful information in this object.
+ // Possibly CORS error if code was 0.
+ var err = new Error("Failed to retrieve resource from " + parsedUrl.host);
+ err.cause = e;
+ err.code = e.target.status;
+ return done(err);
+ }
+ e.code = e.target.status;
+ done(e);
+ };
+
+ var called = false;
+ function done(err, data, res) {
+ timer && clearTimeout(timer);
+ if (called) return;
+ called = true;
+ callback(err, data, res);
+ }
+
+ return xhr;
+ }
+
+ var callbackId = 1;
+ function jsonP(url, options, callback) {
+ if (!callback) return jsonP(url, {}, options);
+
+ var cbName = "__josnpcallback" + callbackId++;
+ var callbackParam = options.callbackParam || "callback";
+
+ var parsedUrl = parseUrl(url, options.query);
+ parsedUrl.query[callbackParam] = cbName;
+
+ window[cbName] = function(json) {
+ delete window.cbName;
+ callback(json);
+ };
+
+ var head = document.getElementsByTagName("head")[0] || document.documentElement;
+ var s = document.createElement('script');
+
+ s.src = URL.format(parsedUrl);
+ head.appendChild(s);
+
+ s.onload = s.onreadystatechange = function(_, isAbort) {
+ if (isAbort || !s.readyState || s.readyState == "loaded" || s.readyState == "complete") {
+ head.removeChild(s);
+ s = s.onload = s.onreadystatechange = null;
+ }
+ };
+ }
+
+ function parseUrl(url, query) {
+ query = query || {};
+ var parsedUrl = URL.parse(url, true);
+ for (var key in query)
+ parsedUrl.query[key] = query[key];
+
+ delete parsedUrl.search;
+
+ return parsedUrl;
+ }
+
+ function parseHeaders(headerString) {
+ return headerString
+ .split('\u000d\u000a')
+ .reduce(function(headers, headerPair) {
+ var index = headerPair.indexOf('\u003a\u0020');
+ if (index > 0) {
+ var key = headerPair.substring(0, index).toLowerCase();
+ var val = headerPair.substring(index + 2);
+ headers[key] = val;
+ }
+ return headers;
+ }, {});
+ }
+
+ /**
+ * Simple API for performing HTTP requests.
+ *
+ * Example:
+ *
+ * http.request("http://www.c9.io", function(err, data) {
+ * if (err) throw err;
+ * console.log(data);
+ * });
+ *
+ * @singleton
+ */
+ plugin.freezePublicAPI({
+ /**
+ * Performs an HTTP request
+ *
+ * @param {String} url Target URL for the HTTP request
+ * @param {Object} [options] Request options
+ * @param {String} [options.method] HTTP method (default=GET)
+ * @param {Object} [options.query] URL query parameters as an object
+ * @param {String|Object} [options.body] HTTP body for PUT and POST
+ * @param {Object} [options.headers] Request headers
+ * @param {Object} [options.username] Basic auth username
+ * @param {Object} [options.password] Basic auth password
+ * @param {Number} [options.timeout] Timeout in ms (default=10000)
+ * @param {String} [options.contentType='application/x-www-form-urlencoded; charset=UTF-8'] Content type of sent data
+ * @param {String} [options.overrideMimeType] Overrides the MIME type returned by the server
+ * @param {Function} [options.progress] Progress event handler
+ * @param {Function} [options.progress.loaded] The amount of bytes downloaded/uploaded.
+ * @param {Function} [options.progress.total] The total amount of bytes to download/upload.
+ * @param {Function} callback Called when the request returns.
+ * @param {Error} callback.err Error object if an error occured.
+ * @param {String} callback.data The data received.
+ * @param {Object} callback.res
+ * @param {String} callback.res.body The body of the response message.
+ * @param {Number} callback.res.status The status of the response message.
+ * @param {Object} callback.res.headers The headers of the response message.
+ */
+ request: request,
+
+ /**
+ * Performs a JSONP request
+ *
+ * @param {String} url Target URL for the JSONP request
+ * @param {Object} [options] Request options
+ * @param {String} [options.callbackParam="callback"] name of the callback query parameter
+ * @param {Object} [options.query] URL query parameters as an object
+ * @param {Function} callback Called when the request returns.
+ * @param {Error} callback.err Error object if an error occured.
+ * @param {String} callback.data The data received.
+ * @param {Object} callback.res
+ * @param {String} callback.res.body The body of the response message.
+ * @param {Number} callback.res.status The status of the response message.
+ */
+ jsonP: jsonP
+ });
+
+ register(null, {
+ http: plugin
+ });
+ }
+});
\ No newline at end of file
diff --git a/plugins/c9.core/http-xhr_test.js b/plugins/c9.core/http-xhr_test.js
new file mode 100644
index 00000000..8b925c2f
--- /dev/null
+++ b/plugins/c9.core/http-xhr_test.js
@@ -0,0 +1,36 @@
+/*global describe, it */
+
+"use client";
+
+require(["lib/architect/architect", "lib/chai/chai"], function (architect, chai) {
+ var expect = chai.expect;
+
+ expect.setupArchitectTest([
+ "plugins/c9.core/ext",
+ "plugins/c9.core/http-xhr",
+ {
+ consumes: ["http"],
+ provides: [],
+ setup: main
+ }
+ ], architect);
+
+ function main(options, imports, register) {
+ var http = imports.http;
+
+ describe('http', function() {
+ it('should request a url via XHR', function(done) {
+ http.request("plugins/c9.core/http-xhr.js", function(err, data, res) {
+ if (err) throw (err.message || err);
+
+ expect(res.status).to.equal(200);
+ expect(data.indexOf("define(function(require, module, exports) {"))
+ .to.equal(0);
+ done();
+ });
+ });
+ });
+
+ onload && onload();
+ }
+});
\ No newline at end of file
diff --git a/plugins/c9.core/node_defaults.js b/plugins/c9.core/node_defaults.js
new file mode 100644
index 00000000..4dab3cf5
--- /dev/null
+++ b/plugins/c9.core/node_defaults.js
@@ -0,0 +1,31 @@
+"use strict";
+
+var debug = require("debug")("node:defaults");
+
+main.consumes = [];
+main.provides = ["node.defaults"];
+
+module.exports = main;
+
+/**
+ * initialize gloabl nodejs settings
+ */
+
+function main(options, imports, register) {
+
+ if ("tls_reject_unauthorized" in options) {
+ debug("setting NODE_TLS_REJECT_UNAUTHORIZED to %s", options.tls_reject_unauthorized);
+ process.env.NODE_TLS_REJECT_UNAUTHORIZED = options.tls_reject_unauthorized;
+
+ }
+
+ if (options.maxSockets) {
+ debug("setting maxSockets to %s", options.maxSockets);
+ require("http").globalAgent.maxSockets = options.maxSockets;
+ require("https").globalAgent.maxSockets = options.maxSockets;
+ }
+
+ register(null, {
+ "node.defaults": {}
+ });
+}
diff --git a/plugins/c9.core/settings.js b/plugins/c9.core/settings.js
new file mode 100644
index 00000000..8c0882b4
--- /dev/null
+++ b/plugins/c9.core/settings.js
@@ -0,0 +1,693 @@
+define(function(require, exports, module) {
+ main.consumes = [
+ "c9", "ui", "Plugin", "fs", "proc", "api", "info", "ext", "util"
+ ];
+ main.provides = ["settings"];
+ return main;
+
+ function main(options, imports, register) {
+ var c9 = imports.c9;
+ var ext = imports.ext;
+ var ui = imports.ui;
+ var Plugin = imports.Plugin;
+ var fs = imports.fs;
+ var proc = imports.proc;
+ var api = imports.api;
+ var info = imports.info;
+ var util = imports.util;
+ var _ = require("lodash");
+
+ var join = require("path").join;
+
+ /***** Initialization *****/
+
+ var plugin = new Plugin("Ajax.org", main.consumes);
+ var emit = plugin.getEmitter();
+
+ // Give the info, ext plugin a reference to settings
+ info.settings = plugin;
+ ext.settings = plugin;
+
+ // We'll have a lot of listeners, so upping the limit
+ emit.setMaxListeners(10000);
+
+ var resetSettings = options.reset || c9.location.match(/reset=([\w\|]*)/) && RegExp.$1;
+ var develMode = c9.location.indexOf("devel=1") > -1;
+ var debugMode = c9.location.indexOf("debug=2") > -1;
+ var testing = options.testing;
+ var debug = options.debug;
+
+ // do not leave reset= in url
+ if (resetSettings && window.history)
+ window.history.pushState(null, null, location.href.replace(/reset=([\w\|]*)/,""));
+
+ var TEMPLATE = options.template || { user: {}, project: {}, state: {} };
+ var INTERVAL = 1000;
+ var PATH = {
+ "project" : c9.toInternalPath(options.projectConfigPath || "/.c9") + "/project.settings",
+ "user" : c9.toInternalPath(options.userConfigPath || "~/.c9") + "/user.settings",
+ "state" : c9.toInternalPath(options.stateConfigFilePath || (options.stateConfigPath || "/.c9") + "/state.settings")
+ };
+ var KEYS = Object.keys(PATH);
+
+ var saveToCloud = {};
+ var model = {};
+ var cache = {};
+ var diff = 0; // TODO should we allow this to be undefined and get NaN in timestamps?
+ var userData;
+
+ var inited = false;
+ function loadSettings(json) {
+ if (!json) {
+ // Load from TEMPLATE
+ if (options.settings == "defaults" || testing)
+ json = TEMPLATE;
+ // Load from parsed settings in the index file
+ else if (options.settings) {
+ json = options.settings;
+
+ if (debugMode)
+ json.state = localStorage["debugState" + c9.projectName];
+
+ for (var type in json) {
+ if (typeof json[type] == "string") {
+ if (json[type].charAt(0) == "<") {
+ json[type] = TEMPLATE[type];
+ }
+ else {
+ try {
+ json[type] = JSON.parse(json[type]);
+ } catch (e) {
+ json[type] = TEMPLATE[type];
+ }
+ }
+ }
+ }
+ }
+
+ if (!json) {
+ var info = {};
+ var count = KEYS.length;
+
+ KEYS.forEach(function(type) {
+ fs.readFile(PATH[type], function(err, data) {
+ try {
+ info[type] = err ? {} : JSON.parse(data);
+ } catch (e) {
+ console.error("Invalid Settings Read for ",
+ type, ": ", data);
+ info[type] = {};
+ }
+
+ if (--count === 0)
+ loadSettings(info);
+ });
+ });
+ return;
+ }
+ }
+
+ read(json);
+ events();
+
+ if (resetSettings)
+ saveToFile();
+
+ KEYS.forEach(function(type) {
+ var node = model[type];
+ if (node)
+ cache[type] = JSON.stringify(node);
+ });
+
+ model.loaded = true;
+ }
+
+ /***** Methods *****/
+
+ var dirty, timer;
+ function checkSave(){
+ if (dirty)
+ saveToFile();
+ }
+
+ function startTimer(){
+ if (c9.readonly) return;
+
+ clearInterval(timer);
+ timer = setInterval(checkSave, INTERVAL);
+ }
+
+ function save(force, sync) {
+ dirty = true;
+
+ if (force) {
+ saveToFile();
+ startTimer();
+ }
+ }
+
+ function saveToFile(sync) {
+ if (c9.readonly || !plugin.loaded)
+ return;
+
+ if (c9.debug)
+ console.log("Saving Settings...");
+
+ emit("write", { model : model });
+
+ model.time = new Date().getTime();
+
+ if (develMode) {
+ dirty = false;
+ return;
+ }
+
+ saveModel(sync);
+ }
+
+ function saveModel(forceSync) {
+ if (c9.readonly || !c9.has(c9.NETWORK)) return;
+
+ if (model.loaded && !testing) {
+ KEYS.forEach(function(type) {
+ var node = model[type];
+ if (!node) return;
+
+ // Get XML string
+ var json = util.stableStringify(node, 0, " ");
+ if (cache[type] == json) return; // Ignore if same as cache
+
+ // Set Cache
+ cache[type] = json;
+
+ // Debug mode
+ if (debugMode && type == "state") {
+ localStorage["debugState" + c9.projectName] = json;
+ return;
+ }
+
+ // Detect whether we're in standalone mode
+ var standalone = !options.hosted;
+
+ if (standalone || type == "project") {
+ fs.writeFile(PATH[type], json, forceSync, function(err){});
+
+ if (standalone && !saveToCloud[type])
+ return; // We're done
+ }
+
+ var addPid = type !== "user"
+ ? "/" + info.getWorkspace().id
+ : "";
+
+ // Save settings in persistent API
+ api.settings.put(type + addPid, {
+ body: { settings: json },
+ sync: forceSync
+ }, function (err) {});
+ });
+ }
+
+ dirty = false;
+ }
+
+ function read(json, isReset) {
+ try {
+ if (testing) throw "testing";
+
+ KEYS.forEach(function(type) {
+ if (json[type])
+ model[type] = json[type];
+ });
+
+ if (resetSettings) {
+ var query = (resetSettings == 1
+ ? "user|state" : resetSettings).split("|");
+ query.forEach(function(type) {
+ model[type] = TEMPLATE[type];
+ });
+ }
+
+ } catch (e) {
+ KEYS.forEach(function(type) {
+ model[type] = TEMPLATE[type];
+ });
+ }
+
+ if (!c9.debug) {
+ try {
+ emit("read", {
+ model: model,
+ ext: plugin,
+ reset: isReset
+ });
+ } catch (e) {
+ fs.writeFile(PATH.project
+ + ".broken", JSON.stringify(json), function(){});
+
+ KEYS.forEach(function(type) {
+ model[type] = TEMPLATE[type];
+ });
+
+ emit("read", {
+ model: model,
+ ext: plugin,
+ reset: isReset
+ });
+ }
+ }
+ else {
+ emit("read", {
+ model: model,
+ ext: plugin,
+ reset: isReset
+ });
+ }
+
+ if (inited)
+ return;
+
+ inited = true;
+
+ plugin.on("newListener", function(type, cb) {
+ if (type != "read") return;
+
+ if (c9.debug || debug) {
+ cb({model : model, ext : plugin});
+ }
+ else {
+ try {
+ cb({ model : model, ext : plugin });
+ }
+ catch (e) {
+ console.error(e.message, e.stack);
+ }
+ }
+ });
+ }
+
+ var hasSetEvents;
+ function events() {
+ if (hasSetEvents) return;
+ hasSetEvents = true;
+
+ startTimer();
+
+ c9.on("beforequit", function(){
+ emit("write", { model: model, unload: true });
+ saveModel(true); //Forcing sync xhr works in chrome
+ }, plugin);
+
+ c9.on("stateChange", function(e) {
+ if (e.state | c9.NETWORK && e.last | c9.NETWORK)
+ saveToFile(); //Save to file
+ }, plugin);
+ }
+
+ function migrate(pathFrom, pathTo) {
+ var idx = pathFrom.lastIndexOf("/");
+ var nodeParent = getNode(pathFrom.substr(0, idx));
+ var name = pathFrom.substr(idx + 1);
+ var nodeFrom = nodeParent && nodeParent[name];
+ if (!nodeFrom) return;
+
+ // Remove node
+ delete nodeParent[name];
+
+ // Create new node
+ var nodeTo = getNode(pathTo) || setNode(pathTo, {}) && getNode(pathTo);
+
+ // Move attributes
+ for (var prop in nodeFrom) {
+ nodeTo[prop] = nodeFrom[prop];
+ }
+ }
+
+ function setDefaults(path, attr) {
+ var node = getNode(path) || set(path, {}, true, true) && getNode(path);
+ var changed;
+
+ attr.forEach(function(a) {
+ var name = "@" + a[0];
+ if (!node.hasOwnProperty(name)) {
+ node[name] = a[1];
+ emit(path + "/" + name, a[1]);
+ changed = true;
+ }
+ });
+
+ if (changed)
+ emit(path);
+ }
+
+ function update(type, json, ud){
+ // Do nothing if they are the same
+ if (_.isEqual(model[type], json))
+ return;
+
+ userData = ud;
+
+ // Compare key/values (assume source has same keys as target)
+ (function recur(source, target, base){
+ for (var prop in source) {
+ if (prop == "json()") {
+ setJson(base, source[prop]);
+ }
+ else if (typeof source[prop] == "object") {
+ if (!target[prop]) target[prop] = {};
+ recur(source[prop], target[prop], join(base, prop));
+ }
+ else if (source[prop] != target[prop]) {
+ set(join(base, prop), source[prop]);
+ }
+ }
+ })(json, model[type], type);
+
+ userData = null;
+ }
+
+ function setNode(query, value) {
+ return set(query, value, true);
+ }
+
+ function set(query, value, isNode, isDefault, checkDefined) {
+ if (!inited && !isDefault) return false;
+
+ var parts = query.split("/");
+ var key = parts.pop();
+ if (!isNode && key.charAt(0) !== "@") {
+ parts.push(key);
+ key = "json()";
+ }
+
+ var hash = model;
+ if (!parts.every(function(part) {
+ if (!hash[part] && checkDefined) return false;
+ hash = hash[part] || (hash[part] = {});
+ return hash;
+ })) {
+ console.warn("Setting non defined query: ", query);
+ return false;
+ }
+ if (hash[key] === value)
+ return;
+
+ hash[key] = value;
+
+ // Tell everyone this property changed
+ emit(parts.join("/"));
+ // Tell everyone it's parent changed
+ emit(query, value);
+
+ // Tell everyone the root type changed (user, project, state)
+ scheduleAnnounce(parts[0], userData);
+
+ dirty = true; //Prevent recursion
+
+ return true;
+ }
+
+ var timers = {};
+ function scheduleAnnounce(type, userData){
+ clearTimeout(timers[type]);
+ timers[type] = setTimeout(function(){
+ emit("change:" + type, { data: model[type], userData: userData });
+ });
+ }
+
+ function setJson(query, value) {
+ return set(query, value);
+ }
+
+ function getJson(query) {
+ var json = get(query, true);
+
+ if (query.indexOf("json()") == -1)
+ json = json["json()"];
+
+ if (typeof json === "object")
+ return JSON.parse(JSON.stringify(json));
+
+ if (typeof json === "string") {
+ try {
+ return JSON.parse(json);
+ } catch (e) {}
+ }
+ // do not return null or undefined so that getJson(query).foo never throws
+ return false;
+ }
+
+ function getBool(query) {
+ var bool = get(query);
+ return ui.isTrue(bool) || (ui.isFalse(bool) ? false : undefined);
+ }
+
+ function getNumber(query) {
+ var double = get(query);
+ return parseFloat(double, 10);
+ }
+
+ function getNode(query) {
+ return get(query, true);
+ }
+
+ function get(query, isNode) {
+ var parts = query.split("/");
+ if (!isNode && parts[parts.length - 1].charAt(0) !== "@")
+ parts.push("json()");
+
+ var hash = model;
+ parts.every(function(part) {
+ hash = hash[part];
+ return hash;
+ });
+
+ return hash === undefined ? "" : hash;
+ }
+
+ function exist(query) {
+ var parts = query.split("/");
+ var hash = model;
+ return parts.every(function(part) {
+ hash = hash[part];
+ return hash;
+ });
+ }
+
+ function reset(query) {
+ if (!query) query = "user|state";
+
+ var info = {};
+ query.split("|").forEach(function(type) {
+ info[type] = TEMPLATE[type];
+ });
+
+ read(model, true);
+ saveToFile();
+ }
+
+ /***** Lifecycle *****/
+
+ plugin.on("load", function(){
+ // Give ui a reference to settings
+ ui.settings = plugin;
+
+ // Get the Time
+ proc.execFile("node", {
+ args: ["-e", "console.log(Date.now())"]
+ }, function(err, stdout, stderr) {
+ if (err || stderr)
+ return;
+
+ var time = parseInt(stdout, 10);
+ diff = Date.now() - time;
+ });
+ });
+ plugin.on("enable", function(){
+ });
+ plugin.on("disable", function(){
+ });
+ plugin.on("unload", function(){
+ dirty = false;
+ diff = 0;
+ userData = null;
+ inited = false;
+ clearInterval(timer);
+ });
+
+ /***** Register and define API *****/
+
+ /**
+ * Settings for Cloud9. Settings are stored based on a path pointing
+ * to leaves. Each leaf can be accessed using the "@" char.
+ *
+ * Example:
+ *
+ * settings.set("user/tree/@width", "200");
+ *
+ * Example:
+ *
+ * settings.getNumber("user/tree/@width");
+ * @singleton
+ */
+ plugin.freezePublicAPI({
+ /**
+ * Exposes the model object that stores the XML used to store the
+ * settings. This property is here for backwards compatibility only
+ * and will be removed in the next version.
+ * @property model
+ * @deprecated
+ * @private
+ */
+ model: model, //Backwards compatibility, should be removed in a later version
+
+ /**
+ * @property {Boolean} inited whether the settings have been loaded
+ */
+ get inited(){ return inited; },
+
+ /**
+ * The offset between the server time and the client time in
+ * milliseconds. A positive number means the client is ahead of the
+ * server.
+ * @property timeOffset
+ * @readonly
+ */
+ get timeOffset(){ return diff; },
+
+ /**
+ *
+ */
+ get paths(){ return PATH; },
+
+ /**
+ *
+ */
+ get saveToCloud(){ return saveToCloud; },
+
+ _events: [
+ /**
+ * @event read Fires when settings are read
+ */
+ "read",
+ /**
+ * @event write Fires when settings are written
+ * @param {Object} e
+ * @param {Boolean} e.unload specifies whether the application
+ * is being unloaded. During an unload there is not much time
+ * and only the highly urgent information should be saved in a
+ * way that the browser still allows (socket is gone, etc).
+ **/
+ "write"
+ ],
+
+ /**
+ * Saves the most current settings after a timeout
+ * @param {Boolean} force forces the settings to be saved immediately
+ */
+ save: save,
+
+ /**
+ * Loads the xml settings into the application
+ * @param {XMLElement} xml The settings xml
+ */
+ read: read,
+
+ /**
+ * Sets a value in the settings tree
+ * @param {String} path the path specifying the key for the value
+ * @param {String} value the value to store in the specified location
+ */
+ "set" : set,
+
+ /**
+ * Sets a value in the settings tree and serializes it as JSON
+ * @param {String} path the path specifying the key for the value
+ * @param {String} value the value to store in the specified location
+ */
+ "setJson" : setJson,
+
+ /**
+ * Gets a value from the settings tree
+ * @param {String} path the path specifying the key for the value
+ */
+ "get" : get,
+
+ /**
+ * Gets a value from the settings tree and interprets it as JSON
+ * @param {String} path the path specifying the key for the value
+ */
+ "getJson" : getJson,
+
+ /**
+ * Gets a value from the settings tree and interprets it as Boolean
+ * @param {String} path the path specifying the key for the value
+ */
+ "getBool" : getBool,
+
+ /**
+ * Gets a value from the settings tree and interprets it as Boolean
+ * @param {String} path the path specifying the key for the value
+ */
+ "getNumber" : getNumber,
+
+ /**
+ * Gets an object from the settings tree and returns it
+ * @param {String} path the path specifying the key for the value
+ */
+ "getNode" : getNode,
+
+ /**
+ * Checks to see if a node exists
+ * @param {String} path the path specifying the key for the value
+ */
+ "exist" : exist,
+
+ /**
+ * Sets the default attributes of a settings tree node.
+ *
+ * Example:
+ *
+ * settings.setDefaults("user/myplugin", [
+ * ["width", 200],
+ * ["show", true]
+ * ])
+ *
+ * @param {String} path the path specifying the key for the value
+ * @param {Array} attr two dimensional array with name
+ * values of the attributes for which the defaults are set
+ */
+ setDefaults: setDefaults,
+
+ /**
+ * Moves and renames attributes from one path to another path
+ * @param {String} fromPath the path specifying where key for the value
+ * @param {String} toPath the path specifying where key for the value
+ * @param {Array} attr two dimensional array with name
+ * values of the attributes for which the defaults are set
+ */
+ migrate: migrate,
+
+ /**
+ * Resets the settings to their defaults
+ */
+ reset: reset,
+
+ /**
+ * Update user, project or state settings incrementally
+ * @param {String} type
+ * @param {Object} settings
+ */
+ update: update
+ });
+
+ if (c9.connected || options.settings)
+ loadSettings();
+ else
+ c9.once("connect", loadSettings);
+
+ register(null, {
+ settings: plugin
+ });
+ }
+});
diff --git a/plugins/c9.core/settings_test.js b/plugins/c9.core/settings_test.js
new file mode 100644
index 00000000..1dec1b9b
--- /dev/null
+++ b/plugins/c9.core/settings_test.js
@@ -0,0 +1,119 @@
+/*global describe:false, it:false */
+
+"use client";
+
+require(["lib/architect/architect", "lib/chai/chai"], function (architect, chai) {
+ var expect = chai.expect;
+
+ var defSettings = { user: { "@bar": "foo", "bar": {"json()": "test"} }, state: { oi : { hi : 10 } }, project: { hi: 0 } };
+ var copySettings = JSON.parse(JSON.stringify(defSettings));
+
+ expect.setupArchitectTest([
+ {
+ packagePath: "plugins/c9.core/c9",
+
+ workspaceId: "user/javruben/dev",
+ env: "test",
+ },
+ "plugins/c9.vfs.client/vfs_client",
+ "plugins/c9.vfs.client/endpoint",
+ "plugins/c9.ide.auth/auth",
+ "plugins/c9.core/ext",
+ "plugins/c9.core/http-xhr",
+ "plugins/c9.ide.ui/lib_apf",
+ {
+ packagePath: "plugins/c9.core/settings",
+ settings: defSettings,
+ debug: true
+ },
+ "plugins/c9.ide.ui/ui",
+ "plugins/c9.core/api",
+ // Mock plugins
+ {
+ consumes: [],
+ provides: ["fs", "auth.bootstrap", "info", "proc", "dialog.error"],
+ setup: expect.html.mocked
+ },
+ {
+ consumes: ["settings"],
+ provides: [],
+ setup: main
+ }
+ ], architect);
+
+ function main(options, imports, register) {
+ var settings = imports.settings;
+
+ describe('settings', function() {
+ it('should expose the settings in it\'s model', function(done) {
+ expect(settings.model.project).to.deep.equal(defSettings.project);
+ expect(settings.model.user).to.deep.equal(defSettings.user);
+ expect(settings.model.state).to.deep.equal(defSettings.state);
+ done();
+ });
+ it('should expose the tree via the get method', function(done) {
+ expect(settings.get('user/@bar')).to.equal("foo");
+ expect(settings.get('user/bar')).to.equal("test");
+ done();
+ });
+ it('should allow altering the tree via the set method', function(done) {
+ var v = Math.random().toString();
+ settings.set('user/@bar', v)
+ expect(settings.get('user/@bar')).to.equal(v);
+
+ v = Math.random().toString();
+ settings.set('user/bar', v)
+ expect(settings.get('user/bar')).to.equal(v);
+
+ done();
+ });
+ it('should allow new settings to be read from json', function(done) {
+ settings.read(copySettings);
+ settings.once("read", function(){
+ expect(settings.get("user/@bar")).to.equal("foo");
+ done();
+ });
+ });
+ it('should call event listener on tree node', function(done) {
+ settings.once("user", function(){
+ expect(settings.get("user/@bar")).to.equal("test");
+ done();
+ });
+ settings.set("user/@bar", "test");
+ });
+ it('should allow type conversion for JSON and Booleans', function(done) {
+ settings.set('user/@bar', "true")
+ expect(settings.getBool('user/@bar')).to.equal(true);
+
+ settings.setJson('user/bar', {test:1})
+ expect(settings.getJson('user/bar')).property("test").to.equal(1);
+
+ done();
+ });
+ it('should set default values only when they are not set already', function(done) {
+ settings.setDefaults('user', [
+ ["bar", "10"],
+ ["test", "15"]
+ ]);
+ expect(settings.exist('user')).to.equal(true);
+ expect(settings.get('user/@bar')).to.not.equal("10");
+ expect(settings.get('user/@test')).to.equal("15");
+
+ done();
+ });
+ it('should set default values the node doesn\'t exist yet', function(done) {
+ settings.setDefaults('new', [
+ ["bar", "10"],
+ ["test", "15"]
+ ]);
+ expect(settings.exist('new')).to.equal(true);
+ expect(settings.get('new/@bar')).to.equal("10");
+ expect(settings.get('new/@test')).to.equal("15");
+
+ done();
+ });
+ });
+
+ onload && onload();
+ }
+});
\ No newline at end of file
diff --git a/plugins/c9.core/util.js b/plugins/c9.core/util.js
new file mode 100644
index 00000000..cec28160
--- /dev/null
+++ b/plugins/c9.core/util.js
@@ -0,0 +1,383 @@
+/**
+ * Utilities for the Ajax.org Cloud IDE
+ *
+ * @copyright 2013, Ajax.org B.V.
+ */
+define(function(require, exports, module) {
+ main.consumes = ["c9", "Plugin"];
+ main.provides = ["util"];
+ return main;
+
+ function main(options, imports, register) {
+ var c9 = imports.c9;
+ var Plugin = imports.Plugin;
+ var normalize = require("path").normalize;
+
+ var plugin = new Plugin("Ajax.org", main.consumes);
+
+ plugin.escapeXpathString = function(name) {
+ if (!name)
+ return "";
+
+ if (name.indexOf('"') > -1) {
+ var out = [];
+ var parts = name.split('"');
+ parts.each(function(part) {
+ out.push(part === "" ? "'\"'" : '"' + part + '"');
+ });
+ return "concat(" + out.join(", ") + ")";
+ }
+ return '"' + name + '"';
+ };
+
+ var SupportedIcons = {
+ "application/xhtml+xml":"html",
+ "text/css": "css",
+ "text/x-scss": "css",
+ "text/x-sass": "css",
+ "text/html":"html",
+ "application/pdf":"page_white_acrobat",
+ "image":"image",
+ "application/xml":"page_white_code_red",
+ "image/svg+xml": "page_white_picture",
+ "text/plain": "page_white_text",
+ "application/javascript": "page_white_code",
+ "application/json": "page_white_code",
+ "text/x-script.python": "page_white_code",
+ "text/x-script.ocaml": "page_white_code",
+ "text/x-script.clojure": "page_white_code",
+ "application/x-httpd-php": "page_white_php",
+ "application/x-sh": "page_white_wrench",
+ "text/x-coldfusion": "page_white_coldfusion",
+ "text/x-script.ruby": "page_white_ruby",
+ "text/x-script.coffeescript": "page_white_cup",
+ "text/cpp": "page_white_cplusplus",
+ "text/x-c": "page_white_c",
+ "text/x-logiql": "logiql",
+ "text/x-csharp": "page_white_csharp",
+ "text/x-java-source": "page_white_cup",
+ "text/x-markdown": "page_white_text",
+ "text/x-xquery": "page_white_code"
+ };
+
+ var contentTypes = {
+ "c9search": "text/x-c9search",
+
+ "js": "application/javascript",
+ "json": "application/json",
+ "run": "application/javascript",
+ "build": "application/javascript",
+ "css": "text/css",
+ "scss": "text/x-scss",
+ "sass": "text/x-sass",
+
+ "xml": "application/xml",
+ "rdf": "application/rdf+xml",
+ "rss": "application/rss+xml",
+ "svg": "image/svg+xml",
+ "wsdl": "application/wsdl+xml",
+ "xslt": "application/xslt+xml",
+ "atom": "application/atom+xml",
+ "mathml": "application/mathml+xml",
+ "mml": "application/mathml+xml",
+
+ "php": "application/x-httpd-php",
+ "phtml": "application/x-httpd-php",
+ "html": "text/html",
+ "xhtml": "application/xhtml+xml",
+ "coffee": "text/x-script.coffeescript",
+ "py": "text/x-script.python",
+ "java": "text/x-java-source",
+ "logic": "text/x-logiql",
+
+ "ru": "text/x-script.ruby",
+ "gemspec": "text/x-script.ruby",
+ "rake": "text/x-script.ruby",
+ "rb": "text/x-script.ruby",
+
+ "c": "text/x-c",
+ "cc": "text/x-c",
+ "cpp": "text/x-c",
+ "cxx": "text/x-c",
+ "h": "text/x-c",
+ "hh": "text/x-c",
+ "hpp": "text/x-c",
+
+ "bmp": "image",
+ "djv": "image",
+ "djvu": "image",
+ "gif": "image",
+ "ico": "image",
+ "jpeg": "image",
+ "jpg": "image",
+ "pbm": "image",
+ "pgm": "image",
+ "png": "image",
+ "pnm": "image",
+ "ppm": "image",
+ "psd": "image",
+ "svgz": "image",
+ "tif": "image",
+ "tiff": "image",
+ "xbm": "image",
+ "xpm": "image",
+
+ "clj": "text/x-script.clojure",
+ "ml": "text/x-script.ocaml",
+ "mli": "text/x-script.ocaml",
+ "cfm": "text/x-coldfusion",
+ "sql": "text/x-sql",
+
+ "sh": "application/x-sh",
+ "bash": "application/x-sh",
+
+ "xq": "text/x-xquery",
+
+ "terminal": "terminal"
+ };
+
+ plugin.getFileIcon = function(name) {
+ var icon = "page_white_text";
+ var ext;
+
+ if (name) {
+ ext = name.split(".").pop().toLowerCase();
+ icon = SupportedIcons[contentTypes[ext]] || "page_white_text";
+ }
+ return icon;
+ };
+
+ plugin.getFileIconCss = function(staticPrefix) {
+ function iconCss(name, icon) {
+ return ".filetree-icon." + name + "{background-image:"
+ +"url(\"" + staticPrefix + "/icons/" + (icon || name) + ".png\")}";
+ }
+ var css = "";
+ var added = {};
+ for (var i in SupportedIcons) {
+ var icon = SupportedIcons[i];
+ if (!added[icon]) {
+ css += iconCss(icon) + "\n";
+ added[icon] = true;
+ }
+ }
+ return css;
+ };
+
+ plugin.getContentType = function(filename) {
+ var type = filename.split(".").pop().split("!").pop().toLowerCase() || "";
+ return contentTypes[type] || "text/plain";
+ };
+
+ // taken from http://xregexp.com/
+ plugin.escapeRegExp = function(str) {
+ return str.replace(/[-[\]{}()*+?.,\\^$|#\s"']/g, "\\$&");
+ };
+
+ plugin.escapeXml = window.apf
+ ? apf.escapeXML
+ : function() { alert("oops! apf needed for this") };
+
+ plugin.replaceStaticPrefix = function (string) {
+ return string.replace(new RegExp("{c9.staticPrefix}", "g"), c9.staticUrl);
+ };
+
+ /*
+ * JavaScript Linkify - v0.3 - 6/27/2009
+ * http://benalman.com/projects/javascript-linkify/
+ *
+ * Copyright (c) 2009 "Cowboy" Ben Alman
+ * Dual licensed under the MIT and GPL licenses.
+ * http://benalman.com/about/license/
+ *
+ * Some regexps adapted from http://userscripts.org/scripts/review/7122
+ */
+ plugin.linkify = function(){var k="[a-z\\d.-]+://",h="(?:(?:[0-9]|[1-9]\\d|1\\d{2}|2[0-4]\\d|25[0-5])\\.){3}(?:[0-9]|[1-9]\\d|1\\d{2}|2[0-4]\\d|25[0-5])",c="(?:(?:[^\\s!@#$%^&*()_=+[\\]{}\\\\|;:'\",.<>/?]+)\\.)+",n="(?:ac|ad|aero|ae|af|ag|ai|al|am|an|ao|aq|arpa|ar|asia|as|at|au|aw|ax|az|ba|bb|bd|be|bf|bg|bh|biz|bi|bj|bm|bn|bo|br|bs|bt|bv|bw|by|bz|cat|ca|cc|cd|cf|cg|ch|ci|ck|cl|cm|cn|coop|com|co|cr|cu|cv|cx|cy|cz|de|dj|dk|dm|do|dz|ec|edu|ee|eg|er|es|et|eu|fi|fj|fk|fm|fo|fr|ga|gb|gd|ge|gf|gg|gh|gi|gl|gm|gn|gov|gp|gq|gr|gs|gt|gu|gw|gy|hk|hm|hn|hr|ht|hu|id|ie|il|im|info|int|in|io|iq|ir|is|it|je|jm|jobs|jo|jp|ke|kg|kh|ki|km|kn|kp|kr|kw|ky|kz|la|lb|lc|li|lk|lr|ls|lt|lu|lv|ly|ma|mc|md|me|mg|mh|mil|mk|ml|mm|mn|mobi|mo|mp|mq|mr|ms|mt|museum|mu|mv|mw|mx|my|mz|name|na|nc|net|ne|nf|ng|ni|nl|no|np|nr|nu|nz|om|org|pa|pe|pf|pg|ph|pk|pl|pm|pn|pro|pr|ps|pt|pw|py|qa|re|ro|rs|ru|rw|sa|sb|sc|sd|se|sg|sh|si|sj|sk|sl|sm|sn|so|sr|st|su|sv|sy|sz|tc|td|tel|tf|tg|th|tj|tk|tl|tm|tn|to|tp|travel|tr|tt|tv|tw|tz|ua|ug|uk|um|us|uy|uz|va|vc|ve|vg|vi|vn|vu|wf|ws|xn--0zwm56d|xn--11b5bs3a9aj6g|xn--80akhbyknj4f|xn--9t4b11yi5a|xn--deba0ad|xn--g6w251d|xn--hgbk6aj7f53bba|xn--hlcj6aya9esc7a|xn--jxalpdlp|xn--kgbechtv|xn--zckzah|ye|yt|yu|za|zm|zw)",f="(?:"+c+n+"|"+h+")",o="(?:[;/][^#?<>\\s]*)?",e="(?:\\?[^#<>\\s]*)?(?:#[^<>\\s]*)?",d="\\b"+k+"[^<>\\s]+",a="\\b"+f+o+e+"(?!\\w)",m="mailto:",j="(?:"+m+")?[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@"+f+e+"(?!\\w)",l=new RegExp("(?:"+d+"|"+a+"|"+j+")","ig"),g=new RegExp("^"+k,"i"),b={"'":"`",">":"<",")":"(","]":"[","}":"{","B;":"B+","b:":"b9"},i={callback:function(q,p){return p?''+q+"":q},punct_regexp:/(?:[!?.,:;'"]|(?:&|&)(?:lt|gt|quot|apos|raquo|laquo|rsaquo|lsaquo);)$/};return function(u,z){z=z||{};var w,v,A,p,x="",t=[],s,E,C,y,q,D,B,r;for(v in i){if(z[v]===undefined){z[v]=i[v]}}while(w=l.exec(u)){A=w[0];E=l.lastIndex;C=E-A.length;if(/[\/:]/.test(u.charAt(C-1))){continue}do{y=A;r=A.substr(-1);B=b[r];if(B){q=A.match(new RegExp("\\"+B+"(?!$)","g"));D=A.match(new RegExp("\\"+r,"g"));if((q?q.length:0)<(D?D.length:0)){A=A.substr(0,A.length-1);E--}}if(z.punct_regexp){A=A.replace(z.punct_regexp,function(F){E-=F.length;return""})}}while(A.length&&A!==y);p=A;if(!g.test(p)){p=(p.indexOf("@")!==-1?(!p.indexOf(m)?"":m):!p.indexOf("irc.")?"irc://":!p.indexOf("ftp.")?"ftp://":"http://")+p}if(s!=C){t.push([u.slice(s,C)]);s=E}t.push([A,p])}t.push([u.substr(s)]);for(v=0;v" : " />");
+ };
+
+ /**
+ * Returns the gravatar url for this user
+ * @param {Number} size the size of the image
+ */
+ plugin.getGravatarUrl = function getGravatarUrl(email, size, defaultImage) {
+ var md5Email = apf.crypto.MD5.hex_md5((email || "").trim().toLowerCase());
+ return "https://secure.gravatar.com/avatar/"
+ + md5Email + "?s=" + size + "&d=" + (defaultImage || "retro");
+ };
+
+ var reHome = new RegExp("^" + plugin.escapeRegExp(c9.home || "/home/ubuntu"));
+ plugin.normalizePath = function(path){
+ return path && normalize(path.replace(reHome, "~"));
+ };
+
+ /**
+ * Converts a map of name-value pairs to XML properties.
+ *
+ * @param {Object} obj Map of name-value pairs of XML properties
+ * @type {String}
+ */
+ plugin.toXmlAttributes = function(obj) {
+ var xml = Object.keys(obj)
+ .map(function (k) {
+ return k + '="' + apf.escapeXML(obj[k]) + '"';
+ })
+ .join(" ");
+
+ return xml;
+ };
+
+ plugin.shadeColor = function(base, factor) {
+ var m = base.match(/(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/);
+ if (!m) {
+ m = base.match(/(\w\w)(\w\w)(\w\w)/);
+ if (!m) {
+ m = base.match(/(\w)(\w)(\w)/);
+ if (!m)
+ return base; // not a color
+ m = [0, m[1] + m[1], m[2] + m[2], m[3] + m[3]];
+ }
+ m = [0, parseInt(m[1], 16), parseInt(m[2], 16), parseInt(m[3], 16)];
+ }
+
+ var R = m[1], G = m[2], B = m[3];
+
+ return {
+ isLight: (0.2126 * R + 0.7152 * G + 0.0722 * B) > 150,
+ color: "rgb(" + parseInt(R * factor, 10) + ", "
+ + parseInt(G * factor, 10) + ", "
+ + parseInt(B * factor, 10) + ")"
+ };
+ };
+
+ plugin.getBox = function(value, base) {
+ if (!base) base = 0;
+
+ if (value === null || (!parseInt(value, 10) && parseInt(value, 10) !== 0))
+ return [0, 0, 0, 0];
+
+ var x = String(value).split(/\s* \s*/);
+ for (var i = 0; i < x.length; i++)
+ x[i] = parseInt(x[i], 10) || 0;
+ switch (x.length) {
+ case 1:
+ x[1] = x[0];
+ x[2] = x[0];
+ x[3] = x[0];
+ break;
+ case 2:
+ x[2] = x[0];
+ x[3] = x[1];
+ break;
+ case 3:
+ x[3] = x[1];
+ break;
+ }
+
+ return x;
+ };
+
+ plugin.escapeShell = function(cmd) {
+ var re = /([\#\&\;\`\|\*\?<>\^\(\)\[\]\{\}\$\,\x0A\xFF\' \"\\])/g;
+ return cmd.replace(re, "\\$1");//.replace(/^~/, "\\~");
+ };
+
+ var cloneObject = plugin.cloneObject = function(obj) {
+ if (obj === null || typeof obj !== "object")
+ return obj;
+ var copy = Array.isArray(obj) ? [] : {};
+ Object.keys(obj).forEach(function(k) {
+ copy[k] = cloneObject(obj[k]);
+ });
+ return copy;
+ };
+
+ plugin.nextFrame = window.requestAnimationFrame ||
+ window.mozRequestAnimationFrame ||
+ window.webkitRequestAnimationFrame ||
+ window.msRequestAnimationFrame ||
+ window.oRequestAnimationFrame;
+
+ if (plugin.nextFrame)
+ plugin.nextFrame = plugin.nextFrame.bind(window);
+ else
+ plugin.nextFrame = function(callback) {
+ setTimeout(callback, 17);
+ };
+
+ plugin.freezePublicAPI({});
+
+ register(null, {
+ util: plugin
+ });
+ }
+});
\ No newline at end of file
diff --git a/plugins/c9.core/util_test.js b/plugins/c9.core/util_test.js
new file mode 100644
index 00000000..afe54df5
--- /dev/null
+++ b/plugins/c9.core/util_test.js
@@ -0,0 +1,40 @@
+/*global describe:false, it:false */
+
+"use client";
+
+require(["lib/architect/architect", "lib/chai/chai"], function (architect, chai) {
+ var expect = chai.expect;
+
+ expect.setupArchitectTest([
+ "plugins/c9.core/ext",
+ "plugins/c9.core/util",
+ // Mock plugins
+ {
+ consumes: [],
+ provides: ["c9"],
+ setup: expect.html.mocked
+ },
+ {
+ consumes: ["util"],
+ provides: [],
+ setup: main
+ }
+ ], architect);
+
+ function main(options, imports, register) {
+ var util = imports.util;
+
+ describe('getContentType, getFileIcon', function() {
+ it('should retrieve the content type based on a filename', function() {
+ expect(util.getContentType("test.js")).to.equal("application/javascript");
+ expect(util.getContentType("test.html")).to.equal("text/html");
+ });
+ it('should retrieve the icon class name based on a filename', function() {
+ expect(util.getFileIcon("test.js")).to.equal("page_white_code");
+ expect(util.getFileIcon("test.html")).to.equal("html");
+ });
+ });
+
+ onload && onload();
+ }
+});
\ No newline at end of file
diff --git a/plugins/c9.vfs.client/endpoint.js b/plugins/c9.vfs.client/endpoint.js
index 9ff97c77..bc35ebf4 100644
--- a/plugins/c9.vfs.client/endpoint.js
+++ b/plugins/c9.vfs.client/endpoint.js
@@ -43,7 +43,8 @@ define(function(require, exports, module) {
if (query.vfs)
options.updateServers = false;
- var region = query.region || options.region;
+ var strictRegion = query.region || options.strictRegion;
+ var region = strictRegion || options.region;
var servers;
var pendingServerReqs = [];
@@ -170,8 +171,8 @@ define(function(require, exports, module) {
// check for version
if (vfsServers.length && !servers.length) {
- if (region === "beta")
- return callback(fatalError("Staging VFS server(s) not working", "reload"));
+ if (strictRegion)
+ return callback(fatalError("No VFS server(s) found for region " + strictRegion, "reload"));
return onProtocolChange(callback);
}
@@ -304,11 +305,11 @@ define(function(require, exports, module) {
function shuffleServers(version, servers) {
servers = servers.slice();
- var isBetaClient = region === "beta";
- servers = servers.filter(function(s) {
- var isBetaServer = s.region === "beta";
- return isBetaServer === isBetaClient;
- });
+ if (strictRegion) {
+ servers = servers.filter(function(s) {
+ return s.region === strictRegion;
+ });
+ }
servers = servers.filter(function(s) {
return s.version == undefined || s.version == version;
});