kopia lustrzana https://github.com/c9/core
Add aliasing and flushing
rodzic
f591d815c8
commit
7545b361f0
|
@ -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
|
||||
});
|
||||
}
|
||||
});
|
|
@ -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
|
||||
});
|
||||
}
|
||||
});
|
|
@ -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();
|
||||
}
|
||||
});
|
|
@ -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
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
|
@ -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:<br /><br />"
|
||||
// + " - " + usedBy.join("<br /> - ")
|
||||
// + "<br /><br /> 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
|
||||
});
|
||||
}
|
||||
});
|
|
@ -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();
|
||||
}
|
||||
});
|
|
@ -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
|
||||
});
|
||||
}
|
|
@ -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
|
||||
});
|
||||
}
|
||||
});
|
|
@ -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();
|
||||
}
|
||||
});
|
|
@ -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": {}
|
||||
});
|
||||
}
|
|
@ -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
|
||||
});
|
||||
}
|
||||
});
|
|
@ -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();
|
||||
}
|
||||
});
|
|
@ -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?'<a href="'+p+'" title="'+p+'">'+q+"</a>":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<t.length;v++){x+=z.callback.apply(window,t[v])}return x||u}}();
|
||||
|
||||
plugin.stableStringify = function(obj, replacer, spaces) {
|
||||
var sortByKeys = function(obj) {
|
||||
if (!obj || typeof obj != "object" || Array.isArray(obj)) {
|
||||
return obj;
|
||||
}
|
||||
var sorted = {};
|
||||
Object.keys(obj).sort().forEach(function(key) {
|
||||
sorted[key] = sortByKeys(obj[key]);
|
||||
});
|
||||
// assert(_.isEqual(obj, sorted));
|
||||
return sorted;
|
||||
};
|
||||
return JSON.stringify(sortByKeys(obj), replacer, spaces);
|
||||
};
|
||||
|
||||
plugin.safeParseJson = function(strJson, cb){
|
||||
// Remove comments
|
||||
var data = strJson.replace(/(^|\n)\s*\/\/.*/g, "");
|
||||
|
||||
try { return JSON.parse(data); }
|
||||
catch (e) { cb(e); return false; }
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
plugin.extend = function(dest, src) {
|
||||
var prop, i, x = !dest.notNull;
|
||||
if (arguments.length == 2) {
|
||||
for (prop in src) {
|
||||
if (x || src[prop])
|
||||
dest[prop] = src[prop];
|
||||
}
|
||||
return dest;
|
||||
}
|
||||
|
||||
for (i = 1; i < arguments.length; i++) {
|
||||
src = arguments[i];
|
||||
for (prop in src) {
|
||||
if (x || src[prop])
|
||||
dest[prop] = src[prop];
|
||||
}
|
||||
}
|
||||
return dest;
|
||||
};
|
||||
|
||||
plugin.isEqual = function(o1, o2) {
|
||||
for (var prop in o1)
|
||||
if (o1[prop] != o2[prop])
|
||||
return false;
|
||||
|
||||
for (var prop in o2)
|
||||
if (o1[prop] != o2[prop])
|
||||
return false;
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate an XML tag that contains properties according to a property-map defined
|
||||
* in `attrs`.
|
||||
*
|
||||
* @param {String} tag Name of the XML tag
|
||||
* @param {Object} attrs Map of name-value pairs of XML properties
|
||||
* @param {Boolean} noclose If TRUE, the XML tag will be returned UNclosed. Defaults to FALSE.
|
||||
* @type {String}
|
||||
*/
|
||||
plugin.toXmlTag = function (tag, attrs, noclose) {
|
||||
return "<" + tag + " " + plugin.toXmlAttributes(attrs) + (noclose ? ">" : " />");
|
||||
};
|
||||
|
||||
/**
|
||||
* 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
|
||||
});
|
||||
}
|
||||
});
|
|
@ -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();
|
||||
}
|
||||
});
|
|
@ -36,6 +36,7 @@ function plugin(options, imports, register) {
|
|||
var connect = imports.connect;
|
||||
var render = imports["connect.render"];
|
||||
var analytics = imports["analytics"];
|
||||
var async = require("async");
|
||||
|
||||
var Types = require("frontdoor").Types;
|
||||
var error = require("http-error");
|
||||
|
@ -281,7 +282,7 @@ function plugin(options, imports, register) {
|
|||
}
|
||||
// TODO: use an interval to make sure this fires
|
||||
// even when this REST api is not used for a day
|
||||
trackActivity(entry.user);
|
||||
trackActivity(entry.user, req.cookies);
|
||||
entry.vfs.handleRest(scope, path, req, res, next);
|
||||
}
|
||||
]);
|
||||
|
@ -315,12 +316,23 @@ function plugin(options, imports, register) {
|
|||
}
|
||||
]);
|
||||
|
||||
function trackActivity(user) {
|
||||
function trackActivity(user, cookies) {
|
||||
if (user.id === -1)
|
||||
return;
|
||||
|
||||
if (new Date(user.lastVfsAccess).getDate() != new Date().getDate() ||
|
||||
Date.now() > user.lastVfsAccess + VFS_ACTIVITY_WINDOW) {
|
||||
|
||||
analytics.identifyClean(user);
|
||||
analytics.trackClean(user, "VFS is active", { uid: user.id });
|
||||
// Alias anonymous id, identify, and track activity;
|
||||
// wait for a flush between each step; see
|
||||
// https://segment.com/docs/integrations/mixpanel/#server-side
|
||||
async.series([
|
||||
analytics.aliasClean.bind(null, cookies.mixpanelAnonymousId, user.id),
|
||||
analytics.identifyClean.bind(user, {}),
|
||||
analytics.trackClean(user, "VFS is active", { uid: user.id }),
|
||||
], function(err) {
|
||||
if (err) return console.log("Error logging activity", err.stack || err);
|
||||
});
|
||||
|
||||
user.lastVfsAccess = Date.now();
|
||||
user.save(function() {});
|
||||
|
|
Ładowanie…
Reference in New Issue