summaryrefslogtreecommitdiff
path: root/src/static/js/pluginfw
diff options
context:
space:
mode:
Diffstat (limited to 'src/static/js/pluginfw')
-rw-r--r--src/static/js/pluginfw/hooks.js26
-rw-r--r--src/static/js/pluginfw/installer.js76
-rw-r--r--src/static/js/pluginfw/parent_require.js37
-rw-r--r--src/static/js/pluginfw/plugins.js87
-rw-r--r--src/static/js/pluginfw/read-installed.js324
5 files changed, 505 insertions, 45 deletions
diff --git a/src/static/js/pluginfw/hooks.js b/src/static/js/pluginfw/hooks.js
index 9c04023f..c4cd5aeb 100644
--- a/src/static/js/pluginfw/hooks.js
+++ b/src/static/js/pluginfw/hooks.js
@@ -10,12 +10,18 @@ if (plugins.isClient) {
_ = require("underscore");
}
+exports.bubbleExceptions = true
+
var hookCallWrapper = function (hook, hook_name, args, cb) {
if (cb === undefined) cb = function (x) { return x; };
- try {
+ if (exports.bubbleExceptions) {
return hook.hook_fn(hook_name, args, cb);
- } catch (ex) {
- console.error([hook_name, hook.part.full_name, ex.stack || ex]);
+ } else {
+ try {
+ return hook.hook_fn(hook_name, args, cb);
+ } catch (ex) {
+ console.error([hook_name, hook.part.full_name, ex.stack || ex]);
+ }
}
}
@@ -36,6 +42,7 @@ exports.flatten = function (lst) {
}
exports.callAll = function (hook_name, args) {
+ if (!args) args = {};
if (plugins.hooks[hook_name] === undefined) return [];
return exports.flatten(_.map(plugins.hooks[hook_name], function (hook) {
return hookCallWrapper(hook, hook_name, args);
@@ -43,26 +50,31 @@ exports.callAll = function (hook_name, args) {
}
exports.aCallAll = function (hook_name, args, cb) {
- if (plugins.hooks[hook_name] === undefined) cb([]);
+ if (!args) args = {};
+ if (!cb) cb = function () {};
+ if (plugins.hooks[hook_name] === undefined) return cb(null, []);
async.map(
plugins.hooks[hook_name],
function (hook, cb) {
hookCallWrapper(hook, hook_name, args, function (res) { cb(null, res); });
},
function (err, res) {
- cb(exports.flatten(res));
+ cb(null, exports.flatten(res));
}
);
}
exports.callFirst = function (hook_name, args) {
+ if (!args) args = {};
if (plugins.hooks[hook_name][0] === undefined) return [];
return exports.flatten(hookCallWrapper(plugins.hooks[hook_name][0], hook_name, args));
}
exports.aCallFirst = function (hook_name, args, cb) {
- if (plugins.hooks[hook_name][0] === undefined) cb([]);
- hookCallWrapper(plugins.hooks[hook_name][0], hook_name, args, function (res) { cb(exports.flatten(res)); });
+ if (!args) args = {};
+ if (!cb) cb = function () {};
+ if (plugins.hooks[hook_name][0] === undefined) return cb(null, []);
+ hookCallWrapper(plugins.hooks[hook_name][0], hook_name, args, function (res) { cb(null, exports.flatten(res)); });
}
exports.callAllStr = function(hook_name, args, sep, pre, post) {
diff --git a/src/static/js/pluginfw/installer.js b/src/static/js/pluginfw/installer.js
new file mode 100644
index 00000000..127a95aa
--- /dev/null
+++ b/src/static/js/pluginfw/installer.js
@@ -0,0 +1,76 @@
+var plugins = require("ep_etherpad-lite/static/js/pluginfw/plugins");
+var hooks = require("ep_etherpad-lite/static/js/pluginfw/hooks");
+var npm = require("npm");
+var registry = require("npm/lib/utils/npm-registry-client/index.js");
+
+var withNpm = function (npmfn, cb) {
+ npm.load({}, function (er) {
+ if (er) return cb({progress:1, error:er});
+ npm.on("log", function (message) {
+ cb({progress: 0.5, message:message.msg + ": " + message.pref});
+ });
+ npmfn(function (er, data) {
+ if (er) return cb({progress:1, error:er.code + ": " + er.path});
+ if (!data) data = {};
+ data.progress = 1;
+ data.message = "Done.";
+ cb(data);
+ });
+ });
+}
+
+// All these functions call their callback multiple times with
+// {progress:[0,1], message:STRING, error:object}. They will call it
+// with progress = 1 at least once, and at all times will either
+// message or error be present, not both. It can be called multiple
+// times for all values of propgress except for 1.
+
+exports.uninstall = function(plugin_name, cb) {
+ withNpm(
+ function (cb) {
+ npm.commands.uninstall([plugin_name], function (er) {
+ if (er) return cb(er);
+ hooks.aCallAll("pluginUninstall", {plugin_name: plugin_name}, function (er, data) {
+ if (er) return cb(er);
+ plugins.update(cb);
+ });
+ });
+ },
+ cb
+ );
+};
+
+exports.install = function(plugin_name, cb) {
+ withNpm(
+ function (cb) {
+ npm.commands.install([plugin_name], function (er) {
+ if (er) return cb(er);
+ hooks.aCallAll("pluginInstall", {plugin_name: plugin_name}, function (er, data) {
+ if (er) return cb(er);
+ plugins.update(cb);
+ });
+ });
+ },
+ cb
+ );
+};
+
+exports.search = function(pattern, cb) {
+ withNpm(
+ function (cb) {
+ registry.get(
+ "/-/all", null, 600, false, true,
+ function (er, data) {
+ if (er) return cb(er);
+ var res = {};
+ for (key in data) {
+ if (key.indexOf(plugins.prefix) == 0 && key.indexOf(pattern) != -1)
+ res[key] = data[key];
+ }
+ cb(null, {results:res});
+ }
+ );
+ },
+ cb
+ );
+};
diff --git a/src/static/js/pluginfw/parent_require.js b/src/static/js/pluginfw/parent_require.js
new file mode 100644
index 00000000..d7f6190d
--- /dev/null
+++ b/src/static/js/pluginfw/parent_require.js
@@ -0,0 +1,37 @@
+/**
+ * This module allows passing require modules instances to
+ * embedded iframes in a page.
+ * For example, if a page has the "plugins" module initialized,
+ * it is important to use exactly the same "plugins" instance
+ * inside iframes as well. Otherwise, plugins cannot save any
+ * state.
+ */
+
+
+/**
+ * Instructs the require object that when a reqModuleName module
+ * needs to be loaded, that it iterates through the parents of the
+ * current window until it finds one who can execute "require"
+ * statements and asks it to perform require on reqModuleName.
+ *
+ * @params requireDefObj Require object which supports define
+ * statements. This object is accessible after loading require-kernel.
+ * @params reqModuleName Module name e.g. (ep_etherpad-lite/static/js/plugins)
+ */
+exports.getRequirementFromParent = function(requireDefObj, reqModuleName) {
+ requireDefObj.define(reqModuleName, function(require, exports, module) {
+ var t = parent;
+ var max = 0; // make sure I don't go up more than 10 times
+ while (typeof(t) != "undefined") {
+ max++;
+ if (max==10)
+ break;
+ if (typeof(t.require) != "undefined") {
+ module.exports = t.require(reqModuleName);
+ return;
+ }
+ t = t.parent;
+ }
+ });
+
+}
diff --git a/src/static/js/pluginfw/plugins.js b/src/static/js/pluginfw/plugins.js
index aa2dfafb..db0b92e4 100644
--- a/src/static/js/pluginfw/plugins.js
+++ b/src/static/js/pluginfw/plugins.js
@@ -4,7 +4,7 @@ var _;
if (!exports.isClient) {
var npm = require("npm/lib/npm.js");
- var readInstalled = require("npm/lib/utils/read-installed.js");
+ var readInstalled = require("./read-installed.js");
var relativize = require("npm/lib/utils/relativize.js");
var readJson = require("npm/lib/utils/read-json.js");
var path = require("path");
@@ -14,10 +14,9 @@ if (!exports.isClient) {
var util = require("util");
_ = require("underscore");
}else{
- var $, jQuery
+ var $, jQuery;
$ = jQuery = require("ep_etherpad-lite/static/js/rjquery").$;
_ = require("ep_etherpad-lite/static/js/underscore");
-
}
exports.prefix = 'ep_';
@@ -31,15 +30,15 @@ exports.ensure = function (cb) {
exports.update(cb);
else
cb();
-}
+};
exports.formatPlugins = function () {
return _.keys(exports.plugins).join(", ");
-}
+};
exports.formatParts = function () {
return _.map(exports.parts, function (part) { return part.full_name; }).join("\n");
-}
+};
exports.formatHooks = function () {
var res = [];
@@ -49,33 +48,39 @@ exports.formatHooks = function () {
});
});
return res.join("\n");
-}
+};
-exports.loadFn = function (path) {
+exports.loadFn = function (path, hookName) {
var x = path.split(":");
var fn = require(x[0]);
- _.each(x[1].split("."), function (name) {
+ var functionName = x[1] ? x[1] : hookName;
+
+ _.each(functionName.split("."), function (name) {
fn = fn[name];
});
return fn;
-}
+};
exports.extractHooks = function (parts, hook_set_name) {
var hooks = {};
_.each(parts,function (part) {
- _.chain(part[hook_set_name] || {}).keys().each(function (hook_name) {
+ _.chain(part[hook_set_name] || {})
+ .keys()
+ .each(function (hook_name) {
if (hooks[hook_name] === undefined) hooks[hook_name] = [];
+
+
var hook_fn_name = part[hook_set_name][hook_name];
- var hook_fn = exports.loadFn(part[hook_set_name][hook_name]);
+ var hook_fn = exports.loadFn(hook_fn_name, hook_name);
if (hook_fn) {
hooks[hook_name].push({"hook_name": hook_name, "hook_fn": hook_fn, "hook_fn_name": hook_fn_name, "part": part});
} else {
- console.error("Unable to load hook function for " + part.full_name + " for hook " + hook_name + ": " + part.hooks[hook_name]);
+ console.error("Unable to load hook function for " + part.full_name + " for hook " + hook_name + ": " + part.hooks[hook_name]);
}
});
});
return hooks;
-}
+};
if (exports.isClient) {
@@ -90,7 +95,7 @@ if (exports.isClient) {
console.error("Failed to load plugin-definitions: " + err);
cb();
});
- }
+ };
} else {
exports.update = function (cb) {
@@ -104,15 +109,15 @@ exports.update = function (cb) {
exports.loadPlugin(packages, plugin_name, plugins, parts, cb);
},
function (err) {
- exports.plugins = plugins;
+ exports.plugins = plugins;
exports.parts = exports.sortParts(parts);
exports.hooks = exports.extractHooks(exports.parts, "hooks");
- exports.loaded = true;
+ exports.loaded = true;
cb(err);
}
);
});
-}
+ };
exports.getPackages = function (cb) {
// Load list of installed NPM packages, flatten it to a list, and filter out only packages with names that
@@ -122,44 +127,50 @@ exports.getPackages = function (cb) {
var packages = {};
function flatten(deps) {
_.chain(deps).keys().each(function (name) {
- if (name.indexOf(exports.prefix) == 0) {
- packages[name] = deps[name];
- }
- if (deps[name].dependencies !== undefined)
- flatten(deps[name].dependencies);
- delete deps[name].dependencies;
+ if (name.indexOf(exports.prefix) === 0) {
+ packages[name] = _.clone(deps[name]);
+ // Delete anything that creates loops so that the plugin
+ // list can be sent as JSON to the web client
+ delete packages[name].dependencies;
+ delete packages[name].parent;
+ }
+
+ if (deps[name].dependencies !== undefined) flatten(deps[name].dependencies);
});
}
- flatten([data]);
+
+ var tmp = {};
+ tmp[data.name] = data;
+ flatten(tmp);
cb(null, packages);
});
-}
+ };
-exports.loadPlugin = function (packages, plugin_name, plugins, parts, cb) {
+ exports.loadPlugin = function (packages, plugin_name, plugins, parts, cb) {
var plugin_path = path.resolve(packages[plugin_name].path, "ep.json");
fs.readFile(
plugin_path,
function (er, data) {
if (er) {
- console.error("Unable to load plugin definition file " + plugin_path);
+ console.error("Unable to load plugin definition file " + plugin_path);
return cb();
}
try {
var plugin = JSON.parse(data);
- plugin.package = packages[plugin_name];
- plugins[plugin_name] = plugin;
- _.each(plugin.parts, function (part) {
- part.plugin = plugin_name;
- part.full_name = plugin_name + "/" + part.name;
- parts[part.full_name] = part;
- });
+ plugin['package'] = packages[plugin_name];
+ plugins[plugin_name] = plugin;
+ _.each(plugin.parts, function (part) {
+ part.plugin = plugin_name;
+ part.full_name = plugin_name + "/" + part.name;
+ parts[part.full_name] = part;
+ });
} catch (ex) {
- console.error("Unable to parse plugin definition file " + plugin_path + ": " + ex.toString());
+ console.error("Unable to parse plugin definition file " + plugin_path + ": " + ex.toString());
}
cb();
}
);
-}
+ };
exports.partsToParentChildList = function (parts) {
var res = [];
@@ -175,7 +186,7 @@ exports.partsToParentChildList = function (parts) {
}
});
return res;
-}
+};
// Used only in Node, so no need for _
diff --git a/src/static/js/pluginfw/read-installed.js b/src/static/js/pluginfw/read-installed.js
new file mode 100644
index 00000000..cc03b357
--- /dev/null
+++ b/src/static/js/pluginfw/read-installed.js
@@ -0,0 +1,324 @@
+// A copy of npm/lib/utils/read-installed.js
+// that is hacked to not cache everything :)
+
+// Walk through the file-system "database" of installed
+// packages, and create a data object related to the
+// installed versions of each package.
+
+/*
+This will traverse through all node_modules folders,
+resolving the dependencies object to the object corresponding to
+the package that meets that dep, or just the version/range if
+unmet.
+
+Assuming that you had this folder structure:
+
+/path/to
++-- package.json { name = "root" }
+`-- node_modules
+ +-- foo {bar, baz, asdf}
+ | +-- node_modules
+ | +-- bar { baz }
+ | `-- baz
+ `-- asdf
+
+where "foo" depends on bar, baz, and asdf, bar depends on baz,
+and bar and baz are bundled with foo, whereas "asdf" is at
+the higher level (sibling to foo), you'd get this object structure:
+
+{ <package.json data>
+, path: "/path/to"
+, parent: null
+, dependencies:
+ { foo :
+ { version: "1.2.3"
+ , path: "/path/to/node_modules/foo"
+ , parent: <Circular: root>
+ , dependencies:
+ { bar:
+ { parent: <Circular: foo>
+ , path: "/path/to/node_modules/foo/node_modules/bar"
+ , version: "2.3.4"
+ , dependencies: { baz: <Circular: foo.dependencies.baz> }
+ }
+ , baz: { ... }
+ , asdf: <Circular: asdf>
+ }
+ }
+ , asdf: { ... }
+ }
+}
+
+Unmet deps are left as strings.
+Extraneous deps are marked with extraneous:true
+deps that don't meet a requirement are marked with invalid:true
+
+to READ(packagefolder, parentobj, name, reqver)
+obj = read package.json
+installed = ./node_modules/*
+if parentobj is null, and no package.json
+ obj = {dependencies:{<installed>:"*"}}
+deps = Object.keys(obj.dependencies)
+obj.path = packagefolder
+obj.parent = parentobj
+if name, && obj.name !== name, obj.invalid = true
+if reqver, && obj.version !satisfies reqver, obj.invalid = true
+if !reqver && parentobj, obj.extraneous = true
+for each folder in installed
+ obj.dependencies[folder] = READ(packagefolder+node_modules+folder,
+ obj, folder, obj.dependencies[folder])
+# walk tree to find unmet deps
+for each dep in obj.dependencies not in installed
+ r = obj.parent
+ while r
+ if r.dependencies[dep]
+ if r.dependencies[dep].verion !satisfies obj.dependencies[dep]
+ WARN
+ r.dependencies[dep].invalid = true
+ obj.dependencies[dep] = r.dependencies[dep]
+ r = null
+ else r = r.parent
+return obj
+
+
+TODO:
+1. Find unmet deps in parent directories, searching as node does up
+as far as the left-most node_modules folder.
+2. Ignore anything in node_modules that isn't a package folder.
+
+*/
+
+
+var npm = require("npm/lib/npm.js")
+ , fs = require("graceful-fs")
+ , path = require("path")
+ , asyncMap = require("slide").asyncMap
+ , semver = require("semver")
+ , readJson = require("npm/lib/utils/read-json.js")
+ , log = require("npm/lib/utils/log.js")
+
+module.exports = readInstalled
+
+function readInstalled (folder, cb) {
+ /* This is where we clear the cache, these three lines are all the
+ * new code there is */
+ rpSeen = {};
+ riSeen = [];
+ var fuSeen = [];
+
+ var d = npm.config.get("depth")
+ readInstalled_(folder, null, null, null, 0, d, function (er, obj) {
+ if (er) return cb(er)
+ // now obj has all the installed things, where they're installed
+ // figure out the inheritance links, now that the object is built.
+ resolveInheritance(obj)
+ cb(null, obj)
+ })
+}
+
+var rpSeen = {}
+function readInstalled_ (folder, parent, name, reqver, depth, maxDepth, cb) {
+ //console.error(folder, name)
+
+ var installed
+ , obj
+ , real
+ , link
+
+ fs.readdir(path.resolve(folder, "node_modules"), function (er, i) {
+ // error indicates that nothing is installed here
+ if (er) i = []
+ installed = i.filter(function (f) { return f.charAt(0) !== "." })
+ next()
+ })
+
+ readJson(path.resolve(folder, "package.json"), function (er, data) {
+ obj = copy(data)
+
+ if (!parent) {
+ obj = obj || true
+ er = null
+ }
+ return next(er)
+ })
+
+ fs.lstat(folder, function (er, st) {
+ if (er) {
+ if (!parent) real = true
+ return next(er)
+ }
+ fs.realpath(folder, function (er, rp) {
+ //console.error("realpath(%j) = %j", folder, rp)
+ real = rp
+ if (st.isSymbolicLink()) link = rp
+ next(er)
+ })
+ })
+
+ var errState = null
+ , called = false
+ function next (er) {
+ if (errState) return
+ if (er) {
+ errState = er
+ return cb(null, [])
+ }
+ //console.error('next', installed, obj && typeof obj, name, real)
+ if (!installed || !obj || !real || called) return
+ called = true
+ if (rpSeen[real]) return cb(null, rpSeen[real])
+ if (obj === true) {
+ obj = {dependencies:{}, path:folder}
+ installed.forEach(function (i) { obj.dependencies[i] = "*" })
+ }
+ if (name && obj.name !== name) obj.invalid = true
+ obj.realName = name || obj.name
+ obj.dependencies = obj.dependencies || {}
+
+ // "foo":"http://blah" is always presumed valid
+ if (reqver
+ && semver.validRange(reqver)
+ && !semver.satisfies(obj.version, reqver)) {
+ obj.invalid = true
+ }
+
+ if (parent
+ && !(name in parent.dependencies)
+ && !(name in (parent.devDependencies || {}))) {
+ obj.extraneous = true
+ }
+ obj.path = obj.path || folder
+ obj.realPath = real
+ obj.link = link
+ if (parent && !obj.link) obj.parent = parent
+ rpSeen[real] = obj
+ obj.depth = depth
+ if (depth >= maxDepth) return cb(null, obj)
+ asyncMap(installed, function (pkg, cb) {
+ var rv = obj.dependencies[pkg]
+ if (!rv && obj.devDependencies) rv = obj.devDependencies[pkg]
+ readInstalled_( path.resolve(folder, "node_modules/"+pkg)
+ , obj, pkg, obj.dependencies[pkg], depth + 1, maxDepth
+ , cb )
+ }, function (er, installedData) {
+ if (er) return cb(er)
+ installedData.forEach(function (dep) {
+ obj.dependencies[dep.realName] = dep
+ })
+
+ // any strings here are unmet things. however, if it's
+ // optional, then that's fine, so just delete it.
+ if (obj.optionalDependencies) {
+ Object.keys(obj.optionalDependencies).forEach(function (dep) {
+ if (typeof obj.dependencies[dep] === "string") {
+ delete obj.dependencies[dep]
+ }
+ })
+ }
+ return cb(null, obj)
+ })
+ }
+}
+
+// starting from a root object, call findUnmet on each layer of children
+var riSeen = []
+function resolveInheritance (obj) {
+ if (typeof obj !== "object") return
+ if (riSeen.indexOf(obj) !== -1) return
+ riSeen.push(obj)
+ if (typeof obj.dependencies !== "object") {
+ obj.dependencies = {}
+ }
+ Object.keys(obj.dependencies).forEach(function (dep) {
+ findUnmet(obj.dependencies[dep])
+ })
+ Object.keys(obj.dependencies).forEach(function (dep) {
+ resolveInheritance(obj.dependencies[dep])
+ })
+}
+
+// find unmet deps by walking up the tree object.
+// No I/O
+var fuSeen = []
+function findUnmet (obj) {
+ if (fuSeen.indexOf(obj) !== -1) return
+ fuSeen.push(obj)
+ //console.error("find unmet", obj.name, obj.parent && obj.parent.name)
+ var deps = obj.dependencies = obj.dependencies || {}
+ //console.error(deps)
+ Object.keys(deps)
+ .filter(function (d) { return typeof deps[d] === "string" })
+ .forEach(function (d) {
+ //console.error("find unmet", obj.name, d, deps[d])
+ var r = obj.parent
+ , found = null
+ while (r && !found && typeof deps[d] === "string") {
+ // if r is a valid choice, then use that.
+ found = r.dependencies[d]
+ if (!found && r.realName === d) found = r
+
+ if (!found) {
+ r = r.link ? null : r.parent
+ continue
+ }
+ if ( typeof deps[d] === "string"
+ && !semver.satisfies(found.version, deps[d])) {
+ // the bad thing will happen
+ log.warn(obj.path + " requires "+d+"@'"+deps[d]
+ +"' but will load\n"
+ +found.path+",\nwhich is version "+found.version
+ ,"unmet dependency")
+ found.invalid = true
+ }
+ deps[d] = found
+ }
+
+ })
+ log.verbose([obj._id], "returning")
+ return obj
+}
+
+function copy (obj) {
+ if (!obj || typeof obj !== 'object') return obj
+ if (Array.isArray(obj)) return obj.map(copy)
+
+ var o = {}
+ for (var i in obj) o[i] = copy(obj[i])
+ return o
+}
+
+if (module === require.main) {
+ var util = require("util")
+ console.error("testing")
+
+ var called = 0
+ readInstalled(process.cwd(), function (er, map) {
+ console.error(called ++)
+ if (er) return console.error(er.stack || er.message)
+ cleanup(map)
+ console.error(util.inspect(map, true, 10, true))
+ })
+
+ var seen = []
+ function cleanup (map) {
+ if (seen.indexOf(map) !== -1) return
+ seen.push(map)
+ for (var i in map) switch (i) {
+ case "_id":
+ case "path":
+ case "extraneous": case "invalid":
+ case "dependencies": case "name":
+ continue
+ default: delete map[i]
+ }
+ var dep = map.dependencies
+// delete map.dependencies
+ if (dep) {
+// map.dependencies = dep
+ for (var i in dep) if (typeof dep[i] === "object") {
+ cleanup(dep[i])
+ }
+ }
+ return map
+ }
+}