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