diff options
Diffstat (limited to 'src/static/js/pluginfw')
-rw-r--r-- | src/static/js/pluginfw/hooks.js | 26 | ||||
-rw-r--r-- | src/static/js/pluginfw/installer.js | 76 | ||||
-rw-r--r-- | src/static/js/pluginfw/parent_require.js | 37 | ||||
-rw-r--r-- | src/static/js/pluginfw/plugins.js | 15 | ||||
-rw-r--r-- | src/static/js/pluginfw/read-installed.js | 324 |
5 files changed, 466 insertions, 12 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..058f1351 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"); @@ -12,12 +12,12 @@ if (!exports.isClient) { var fs = require("fs"); var tsort = require("./tsort"); var util = require("util"); + var extend = require("node.extend"); _ = require("underscore"); }else{ var $, jQuery $ = jQuery = require("ep_etherpad-lite/static/js/rjquery").$; _ = require("ep_etherpad-lite/static/js/underscore"); - } exports.prefix = 'ep_'; @@ -123,14 +123,19 @@ exports.getPackages = function (cb) { function flatten(deps) { _.chain(deps).keys().each(function (name) { if (name.indexOf(exports.prefix) == 0) { - packages[name] = deps[name]; + packages[name] = extend({}, 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); - delete deps[name].dependencies; }); } - flatten([data]); + var tmp = {}; + tmp[data.name] = data; + flatten(tmp); cb(null, packages); }); } 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 + } +} |