1 // Copyright (c) 2009-2013 Kris Maglione <maglione.k@gmail.com>
2 // Copyright (c) 2009-2010 by Doug Kearns <dougkearns@gmail.com>
4 // This work is licensed for reuse under an MIT license. Details are
5 // given in the LICENSE.txt file included with this file.
10 defineModule("addons", {
11 exports: ["AddonManager", "Addons", "Addon", "addons"],
12 require: ["services", "util"]
15 this.lazyRequire("completion", ["completion"]);
16 lazyRequire("template", ["template"]);
18 var callResult = function callResult(method, ...args) {
19 return function (result) { result[method].apply(result, args); };
22 var listener = function listener(action, event)
23 function addonListener(install) {
24 this.dactyl[install.error ? "echoerr" : "echomsg"](
25 _("addon.error", action, event, (install.name || install.sourceURI.spec) +
26 (install.error ? ": " + addons.errors[install.error] : "")));
29 var AddonListener = Class("AddonListener", {
30 init: function init(modules) {
31 this.dactyl = modules.dactyl;
34 onNewInstall: function (install) {},
35 onExternalInstall: function (addon, existingAddon, needsRestart) {},
36 onDownloadStarted: listener("download", "started"),
37 onDownloadEnded: listener("download", "complete"),
38 onDownloadCancelled: listener("download", "canceled"),
39 onDownloadFailed: listener("download", "failed"),
40 onDownloadProgress: function (install) {},
41 onInstallStarted: function (install) {},
42 onInstallEnded: listener("installation", "complete"),
43 onInstallCancelled: listener("installation", "canceled"),
44 onInstallFailed: listener("installation", "failed")
47 var updateAddons = Class("UpgradeListener", AddonListener, {
48 init: function init(addons, modules) {
49 init.supercall(this, modules);
51 util.assert(!addons.length || addons[0].findUpdates,
52 _("error.unavailable", config.host, services.runtime.version));
54 this.remaining = addons;
56 this.dactyl.echomsg(_("addon.check", addons.map(a => a.name).join(", ")));
57 for (let addon in values(addons))
58 addon.findUpdates(this, AddonManager.UPDATE_WHEN_USER_REQUESTED, null, null);
61 onUpdateAvailable: function (addon, install) {
62 this.upgrade.push(addon);
63 install.addListener(this);
66 onUpdateFinished: function (addon, error) {
67 this.remaining = this.remaining.filter(a => (a.type != addon.type || a.id != addon.id));
68 if (!this.remaining.length)
71 ? _("addon.installingUpdates", this.upgrade.map(i => i.name).join(", "))
72 : _("addon.noUpdates"));
78 name: ["extde[lete]", "extrm"],
79 description: "Uninstall an extension",
80 action: callResult("uninstall"),
85 description: "Enable an extension",
86 action: function (addon) { addon.userDisabled = false; },
87 filter: function (addon) addon.userDisabled,
92 description: "Disable an extension",
93 action: function (addon) { addon.userDisabled = true; },
94 filter: function (addon) !addon.userDisabled,
98 name: ["exto[ptions]", "extp[references]"],
99 description: "Open an extension's preference dialog",
101 action: function (addon, bang) {
103 this.window.openDialog(addon.optionsURL, "_blank", "chrome");
105 this.dactyl.open(addon.optionsURL, { from: "extoptions" });
107 filter: function (addon) addon.isActive && addon.optionsURL
111 description: "Reload an extension",
112 action: function (addon) {
113 util.assert(config.haveGecko("2b"), _("command.notUseful", config.host));
115 util.timeout(function () {
116 addon.userDisabled = true;
117 addon.userDisabled = false;
122 !addon.userDisabled &&
123 !(addon.operationsRequiringRestart & (AddonManager.OP_NEEDS_RESTART_ENABLE
124 | AddonManager.OP_NEEDS_RESTART_DISABLE)));
130 description: "Toggle an extension's enabled status",
131 action: function (addon) { addon.userDisabled = !addon.userDisabled; }
135 description: "Update an extension",
136 actions: updateAddons,
141 var Addon = Class("Addon", {
142 init: function init(addon, list) {
144 this.instance = this;
151 ["tr", { highlight: "Addon", key: "row" },
152 ["td", { highlight: "AddonName", key: "name" }],
153 ["td", { highlight: "AddonVersion", key: "version" }],
154 ["td", { highlight: "AddonButtons Buttons" },
155 ["a", { highlight: "Button", href: "javascript:0", key: "enable" }, _("addon.action.On")],
156 ["a", { highlight: "Button", href: "javascript:0", key: "disable" }, _("addon.action.Off")],
157 ["a", { highlight: "Button", href: "javascript:0", key: "delete" }, _("addon.action.Delete")],
158 ["a", { highlight: "Button", href: "javascript:0", key: "update" }, _("addon.action.Update")],
159 ["a", { highlight: "Button", href: "javascript:0", key: "options" }, _("addon.action.Options")]],
160 ["td", { highlight: "AddonStatus", key: "status" }],
161 ["td", { highlight: "AddonDescription", key: "description" }]],
162 this.list.document, this.nodes);
167 commandAllowed: function commandAllowed(cmd) {
168 util.assert(Set.has(actions, cmd), _("addon.unknownCommand"));
170 let action = actions[cmd];
171 if ("perm" in action && !(this.permissions & AddonManager["PERM_CAN_" + action.perm.toUpperCase()]))
173 if ("filter" in action && !action.filter(this))
178 command: function command(cmd) {
179 util.assert(this.commandAllowed(cmd), _("addon.commandNotAllowed"));
181 let action = actions[cmd];
183 action.action.call(this.list.modules, this, true);
185 action.actions([this], this.list.modules);
188 compare: function compare(other) String.localeCompare(this.name, other.name),
191 let info = this.isActive ? ["span", { highlight: "Enabled" }, "enabled"]
192 : ["span", { highlight: "Disabled" }, "disabled"];
195 if (this.pendingOperations & AddonManager.PENDING_UNINSTALL)
196 pending = ["Disabled", "uninstalled"];
197 else if (this.pendingOperations & AddonManager.PENDING_DISABLE)
198 pending = ["Disabled", "disabled"];
199 else if (this.pendingOperations & AddonManager.PENDING_INSTALL)
200 pending = ["Enabled", "installed"];
201 else if (this.pendingOperations & AddonManager.PENDING_ENABLE)
202 pending = ["Enabled", "enabled"];
203 else if (this.pendingOperations & AddonManager.PENDING_UPGRADE)
204 pending = ["Enabled", "upgraded"];
207 ["span", { highlight: pending[0] }, pending[1]],
209 ["a", { href: "#", "dactyl:command": "dactyl.restart" }, "restart"],
214 update: function callee() {
215 let update = (key, xml) => {
216 let node = this.nodes[key];
217 while (node.firstChild)
218 node.removeChild(node.firstChild);
220 DOM(node).append(isArray(xml) ? xml : DOM.DOMString(xml));
223 update("name", template.icon({ icon: this.iconURL }, this.name));
224 this.nodes.version.textContent = this.version;
225 update("status", this.statusInfo);
226 this.nodes.description.textContent = this.description;
227 DOM(this.nodes.row).attr("active", this.isActive || null);
229 for (let node in values(this.nodes))
230 if (node.update && node.update !== callee)
233 let event = this.list.document.createEvent("Events");
234 event.initEvent("dactyl-commandupdate", true, false);
235 this.list.document.dispatchEvent(event);
239 ["cancelUninstall", "findUpdates", "getResourceURI", "hasResource", "isCompatibleWith",
240 "uninstall"].forEach(function (prop) {
241 Addon.prototype[prop] = function proxy() this.addon[prop].apply(this.addon, arguments);
244 ["aboutURL", "appDisabled", "applyBackgroundUpdates", "blocklistState", "contributors", "creator",
245 "description", "developers", "homepageURL", "iconURL", "id", "install", "installDate", "isActive",
246 "isCompatible", "isPlatformCompatible", "name", "operationsRequiringRestart", "optionsURL",
247 "pendingOperations", "pendingUpgrade", "permissions", "providesUpdatesSecurely", "releaseNotesURI",
248 "scope", "screenshots", "size", "sourceURI", "translators", "type", "updateDate", "userDisabled",
249 "version"].forEach(function (prop) {
250 Object.defineProperty(Addon.prototype, prop, {
251 get: function get_proxy() this.addon[prop],
252 set: function set_proxy(val) this.addon[prop] = val
256 var AddonList = Class("AddonList", {
257 init: function init(modules, types, filter) {
258 this.modules = modules;
259 this.filter = filter && filter.toLowerCase();
264 AddonManager.getAddonsByTypes(types, this.closure(function (addons) {
265 this._addons = addons;
269 AddonManager.addAddonListener(this);
271 cleanup: function cleanup() {
272 AddonManager.removeAddonListener(this);
275 _init: function _init() {
276 this._addons.forEach(this.closure.addAddon);
281 message: Class.Memoize(function () {
282 DOM.fromJSON(["table", { highlight: "Addons", key: "list" },
283 ["tr", { highlight: "AddonHead" },
284 ["td", {}, _("title.Name")],
285 ["td", {}, _("title.Version")],
287 ["td", {}, _("title.Status")],
288 ["td", {}, _("title.Description")]]],
289 this.document, this.nodes);
294 return this.nodes.list;
297 addAddon: function addAddon(addon) {
298 if (addon.id in this.addons)
301 if (this.filter && addon.name.toLowerCase().indexOf(this.filter) === -1)
304 addon = Addon(addon, this);
305 this.addons[addon.id] = addon;
307 let index = values(this.addons).sort((a, b) => a.compare(b))
310 this.nodes.list.insertBefore(addon.nodes.row,
311 this.nodes.list.childNodes[index + 1]);
315 removeAddon: function removeAddon(addon) {
316 if (addon.id in this.addons) {
317 this.nodes.list.removeChild(this.addons[addon.id].nodes.row);
318 delete this.addons[addon.id];
323 leave: function leave(stack) {
328 update: function update(addon) {
329 if (addon && addon.id in this.addons)
330 this.addons[addon.id].update();
332 this.modules.mow.resize(false);
335 onDisabled: function (addon) { this.update(addon); },
336 onDisabling: function (addon) { this.update(addon); },
337 onEnabled: function (addon) { this.update(addon); },
338 onEnabling: function (addon) { this.update(addon); },
339 onInstalled: function (addon) { this.addAddon(addon); },
340 onInstalling: function (addon) { this.update(addon); },
341 onUninstalled: function (addon) { this.removeAddon(addon); },
342 onUninstalling: function (addon) { this.update(addon); },
343 onOperationCancelled: function (addon) { this.update(addon); },
344 onPropertyChanged: function onPropertyChanged(addon, properties) {}
347 var Addons = Module("addons", {
348 errors: Class.Memoize(() =>
349 array(["ERROR_NETWORK_FAILURE", "ERROR_INCORRECT_HASH",
350 "ERROR_CORRUPT_FILE", "ERROR_FILE_ACCESS"])
351 .map(e => [AddonManager[e], _("AddonManager." + e)])
355 commands: function initCommands(dactyl, modules, window) {
356 const { CommandOption, commands, completion, io } = modules;
358 commands.add(["addo[ns]", "ao"],
359 "List installed extensions",
361 let addons = AddonList(modules, args["-types"], args[0]);
362 modules.commandline.echo(addons);
364 if (modules.commandline.savingOutput)
365 util.waitFor(() => addons.ready);
371 names: ["-types", "-type", "-t"],
372 description: "The add-on types to list",
373 default: ["extension"],
374 completer: function (context, args) completion.addonType(context),
375 type: CommandOption.LIST
380 let addonListener = AddonListener(modules);
382 commands.add(["exta[dd]"],
383 "Install an extension",
386 let file = io.File(url);
387 function install(addonInstall) {
388 addonInstall.addListener(addonListener);
389 addonInstall.install();
393 AddonManager.getInstallForURL(url, install, "application/x-xpinstall");
394 else if (file.isReadable() && file.isFile())
395 AddonManager.getInstallForFile(file.file, install, "application/x-xpinstall");
396 else if (file.isDirectory())
397 dactyl.echoerr(_("addon.cantInstallDir", file.path.quote()));
399 dactyl.echoerr(_("io.notReadable", file.path));
402 completer: function (context) {
403 context.filters.push(({ isdir, text }) => isdir || /\.xpi$/.test(text));
404 completion.file(context);
409 // TODO: handle extension dependencies
410 values(actions).forEach(function (command) {
411 let perm = command.perm && AddonManager["PERM_CAN_" + command.perm.toUpperCase()];
412 function ok(addon) (!perm || addon.permissions & perm) && (!command.filter || command.filter(addon));
414 commands.add(Array.concat(command.name),
418 if (args.bang && !command.bang)
419 dactyl.assert(!name, _("error.trailingCharacters"));
421 dactyl.assert(name, _("error.argumentRequired"));
423 AddonManager.getAddonsByTypes(args["-types"], dactyl.wrapCallback(function (list) {
424 if (!args.bang || command.bang) {
425 list = list.filter(addon => (addon.id == name || addon.name == name));
426 dactyl.assert(list.length, _("error.invalidArgument", name));
427 dactyl.assert(list.some(ok), _("error.invalidOperation"));
428 list = list.filter(ok);
430 dactyl.assert(list.every(ok));
432 command.actions(list, this.modules);
434 list.forEach(addon => { command.action.call(this.modules, addon, args.bang) });
437 argCount: "?", // FIXME: should be "1"
439 completer: function (context, args) {
440 completion.addon(context, args["-types"]);
441 context.filters.push(({ item }) => ok(item));
446 names: ["-types", "-type", "-t"],
447 description: "The add-on types to operate on",
448 default: ["extension"],
449 completer: function (context, args) completion.addonType(context),
450 type: CommandOption.LIST
456 completion: function initCompletion(dactyl, modules, window) {
457 completion.addonType = function addonType(context) {
458 let base = ["extension", "theme"];
459 function update(types) {
460 context.completions = types.map(t => [t, util.capitalize(t)]);
463 context.generate = function generate() {
465 if (AddonManager.getAllAddons) {
466 context.incomplete = true;
467 AddonManager.getAllAddons(function (addons) {
468 context.incomplete = false;
469 update(array.uniq(base.concat(addons.map(a => a.type)),
476 completion.addon = function addon(context, types) {
477 context.title = ["Add-on"];
478 context.anchored = false;
480 text: addon => [addon.name, addon.id],
481 description: "description",
484 context.generate = function () {
485 context.incomplete = true;
486 AddonManager.getAddonsByTypes(types || ["extension"], function (addons) {
487 context.incomplete = false;
488 context.completions = addons;
495 Components.utils.import("resource://gre/modules/AddonManager.jsm", this);
499 } catch(e){ if (isString(e)) e = Error(e); dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack); }
501 // vim: set fdm=marker sw=4 sts=4 ts=8 et ft=javascript: