1 // Copyright (c) 2009-2012 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(function (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(function (a) a.type != addon.type || a.id != addon.id);
68 if (!this.remaining.length)
71 ? _("addon.installingUpdates", this.upgrade.map(function (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;
121 return function (addon) !addon.userDisabled &&
122 !(addon.operationsRequiringRestart & (AddonManager.OP_NEEDS_RESTART_ENABLE | AddonManager.OP_NEEDS_RESTART_DISABLE));
128 description: "Toggle an extension's enabled status",
129 action: function (addon) { addon.userDisabled = !addon.userDisabled; }
133 description: "Update an extension",
134 actions: updateAddons,
139 var Addon = Class("Addon", {
140 init: function init(addon, list) {
142 this.instance = this;
149 ["tr", { highlight: "Addon", key: "row" },
150 ["td", { highlight: "AddonName", key: "name" }],
151 ["td", { highlight: "AddonVersion", key: "version" }],
152 ["td", { highlight: "AddonButtons Buttons" },
153 ["a", { highlight: "Button", href: "javascript:0", key: "enable" }, _("addon.action.On")],
154 ["a", { highlight: "Button", href: "javascript:0", key: "disable" }, _("addon.action.Off")],
155 ["a", { highlight: "Button", href: "javascript:0", key: "delete" }, _("addon.action.Delete")],
156 ["a", { highlight: "Button", href: "javascript:0", key: "update" }, _("addon.action.Update")],
157 ["a", { highlight: "Button", href: "javascript:0", key: "options" }, _("addon.action.Options")]],
158 ["td", { highlight: "AddonStatus", key: "status" }],
159 ["td", { highlight: "AddonDescription", key: "description" }]],
160 this.list.document, this.nodes);
165 commandAllowed: function commandAllowed(cmd) {
166 util.assert(Set.has(actions, cmd), _("addon.unknownCommand"));
168 let action = actions[cmd];
169 if ("perm" in action && !(this.permissions & AddonManager["PERM_CAN_" + action.perm.toUpperCase()]))
171 if ("filter" in action && !action.filter(this))
176 command: function command(cmd) {
177 util.assert(this.commandAllowed(cmd), _("addon.commandNotAllowed"));
179 let action = actions[cmd];
181 action.action.call(this.list.modules, this, true);
183 action.actions([this], this.list.modules);
186 compare: function compare(other) String.localeCompare(this.name, other.name),
189 let info = this.isActive ? ["span", { highlight: "Enabled" }, "enabled"]
190 : ["span", { highlight: "Disabled" }, "disabled"];
193 if (this.pendingOperations & AddonManager.PENDING_UNINSTALL)
194 pending = ["Disabled", "uninstalled"];
195 else if (this.pendingOperations & AddonManager.PENDING_DISABLE)
196 pending = ["Disabled", "disabled"];
197 else if (this.pendingOperations & AddonManager.PENDING_INSTALL)
198 pending = ["Enabled", "installed"];
199 else if (this.pendingOperations & AddonManager.PENDING_ENABLE)
200 pending = ["Enabled", "enabled"];
201 else if (this.pendingOperations & AddonManager.PENDING_UPGRADE)
202 pending = ["Enabled", "upgraded"];
205 ["span", { highlight: pending[0] }, pending[1]],
207 ["a", { href: "#", "dactyl:command": "dactyl.restart" }, "restart"],
212 update: function callee() {
213 let update = (key, xml) => {
214 let node = this.nodes[key];
215 while (node.firstChild)
216 node.removeChild(node.firstChild);
218 DOM(node).append(isArray(xml) ? xml : DOM.DOMString(xml));
221 update("name", template.icon({ icon: this.iconURL }, this.name));
222 this.nodes.version.textContent = this.version;
223 update("status", this.statusInfo);
224 this.nodes.description.textContent = this.description;
225 DOM(this.nodes.row).attr("active", this.isActive || null);
227 for (let node in values(this.nodes))
228 if (node.update && node.update !== callee)
231 let event = this.list.document.createEvent("Events");
232 event.initEvent("dactyl-commandupdate", true, false);
233 this.list.document.dispatchEvent(event);
237 ["cancelUninstall", "findUpdates", "getResourceURI", "hasResource", "isCompatibleWith",
238 "uninstall"].forEach(function (prop) {
239 Addon.prototype[prop] = function proxy() this.addon[prop].apply(this.addon, arguments);
242 ["aboutURL", "appDisabled", "applyBackgroundUpdates", "blocklistState", "contributors", "creator",
243 "description", "developers", "homepageURL", "iconURL", "id", "install", "installDate", "isActive",
244 "isCompatible", "isPlatformCompatible", "name", "operationsRequiringRestart", "optionsURL",
245 "pendingOperations", "pendingUpgrade", "permissions", "providesUpdatesSecurely", "releaseNotesURI",
246 "scope", "screenshots", "size", "sourceURI", "translators", "type", "updateDate", "userDisabled",
247 "version"].forEach(function (prop) {
248 Object.defineProperty(Addon.prototype, prop, {
249 get: function get_proxy() this.addon[prop],
250 set: function set_proxy(val) this.addon[prop] = val
254 var AddonList = Class("AddonList", {
255 init: function init(modules, types, filter) {
256 this.modules = modules;
257 this.filter = filter && filter.toLowerCase();
262 AddonManager.getAddonsByTypes(types, this.closure(function (addons) {
263 this._addons = addons;
267 AddonManager.addAddonListener(this);
269 cleanup: function cleanup() {
270 AddonManager.removeAddonListener(this);
273 _init: function _init() {
274 this._addons.forEach(this.closure.addAddon);
279 message: Class.Memoize(function () {
280 DOM.fromJSON(["table", { highlight: "Addons", key: "list" },
281 ["tr", { highlight: "AddonHead" },
282 ["td", {}, _("title.Name")],
283 ["td", {}, _("title.Version")],
285 ["td", {}, _("title.Status")],
286 ["td", {}, _("title.Description")]]],
287 this.document, this.nodes);
292 return this.nodes.list;
295 addAddon: function addAddon(addon) {
296 if (addon.id in this.addons)
299 if (this.filter && addon.name.toLowerCase().indexOf(this.filter) === -1)
302 addon = Addon(addon, this);
303 this.addons[addon.id] = addon;
305 let index = values(this.addons).sort(function (a, b) a.compare(b))
308 this.nodes.list.insertBefore(addon.nodes.row,
309 this.nodes.list.childNodes[index + 1]);
313 removeAddon: function removeAddon(addon) {
314 if (addon.id in this.addons) {
315 this.nodes.list.removeChild(this.addons[addon.id].nodes.row);
316 delete this.addons[addon.id];
321 leave: function leave(stack) {
326 update: function update(addon) {
327 if (addon && addon.id in this.addons)
328 this.addons[addon.id].update();
330 this.modules.mow.resize(false);
333 onDisabled: function (addon) { this.update(addon); },
334 onDisabling: function (addon) { this.update(addon); },
335 onEnabled: function (addon) { this.update(addon); },
336 onEnabling: function (addon) { this.update(addon); },
337 onInstalled: function (addon) { this.addAddon(addon); },
338 onInstalling: function (addon) { this.update(addon); },
339 onUninstalled: function (addon) { this.removeAddon(addon); },
340 onUninstalling: function (addon) { this.update(addon); },
341 onOperationCancelled: function (addon) { this.update(addon); },
342 onPropertyChanged: function onPropertyChanged(addon, properties) {}
345 var Addons = Module("addons", {
346 errors: Class.Memoize(function ()
347 array(["ERROR_NETWORK_FAILURE", "ERROR_INCORRECT_HASH",
348 "ERROR_CORRUPT_FILE", "ERROR_FILE_ACCESS"])
349 .map(function (e) [AddonManager[e], _("AddonManager." + e)])
353 commands: function initCommands(dactyl, modules, window) {
354 const { CommandOption, commands, completion, io } = modules;
356 commands.add(["addo[ns]", "ao"],
357 "List installed extensions",
359 let addons = AddonList(modules, args["-types"], args[0]);
360 modules.commandline.echo(addons);
362 if (modules.commandline.savingOutput)
363 util.waitFor(function () addons.ready);
369 names: ["-types", "-type", "-t"],
370 description: "The add-on types to list",
371 default: ["extension"],
372 completer: function (context, args) completion.addonType(context),
373 type: CommandOption.LIST
378 let addonListener = AddonListener(modules);
380 commands.add(["exta[dd]"],
381 "Install an extension",
384 let file = io.File(url);
385 function install(addonInstall) {
386 addonInstall.addListener(addonListener);
387 addonInstall.install();
391 AddonManager.getInstallForURL(url, install, "application/x-xpinstall");
392 else if (file.isReadable() && file.isFile())
393 AddonManager.getInstallForFile(file.file, install, "application/x-xpinstall");
394 else if (file.isDirectory())
395 dactyl.echoerr(_("addon.cantInstallDir", file.path.quote()));
397 dactyl.echoerr(_("io.notReadable", file.path));
400 completer: function (context) {
401 context.filters.push(function ({ isdir, text }) isdir || /\.xpi$/.test(text));
402 completion.file(context);
407 // TODO: handle extension dependencies
408 values(actions).forEach(function (command) {
409 let perm = command.perm && AddonManager["PERM_CAN_" + command.perm.toUpperCase()];
410 function ok(addon) (!perm || addon.permissions & perm) && (!command.filter || command.filter(addon));
412 commands.add(Array.concat(command.name),
416 if (args.bang && !command.bang)
417 dactyl.assert(!name, _("error.trailingCharacters"));
419 dactyl.assert(name, _("error.argumentRequired"));
421 AddonManager.getAddonsByTypes(args["-types"], dactyl.wrapCallback(function (list) {
422 if (!args.bang || command.bang) {
423 list = list.filter(function (addon) addon.id == name || addon.name == name);
424 dactyl.assert(list.length, _("error.invalidArgument", name));
425 dactyl.assert(list.some(ok), _("error.invalidOperation"));
426 list = list.filter(ok);
428 dactyl.assert(list.every(ok));
430 command.actions(list, this.modules);
432 list.forEach(function (addon) command.action.call(this.modules, addon, args.bang), this);
435 argCount: "?", // FIXME: should be "1"
437 completer: function (context, args) {
438 completion.addon(context, args["-types"]);
439 context.filters.push(function ({ item }) ok(item));
444 names: ["-types", "-type", "-t"],
445 description: "The add-on types to operate on",
446 default: ["extension"],
447 completer: function (context, args) completion.addonType(context),
448 type: CommandOption.LIST
454 completion: function initCompletion(dactyl, modules, window) {
455 completion.addonType = function addonType(context) {
456 let base = ["extension", "theme"];
457 function update(types) {
458 context.completions = types.map(function (t) [t, util.capitalize(t)]);
461 context.generate = function generate() {
463 if (AddonManager.getAllAddons) {
464 context.incomplete = true;
465 AddonManager.getAllAddons(function (addons) {
466 context.incomplete = false;
467 update(array.uniq(base.concat(addons.map(function (a) a.type)),
474 completion.addon = function addon(context, types) {
475 context.title = ["Add-on"];
476 context.anchored = false;
478 text: function (addon) [addon.name, addon.id],
479 description: "description",
482 context.generate = function () {
483 context.incomplete = true;
484 AddonManager.getAddonsByTypes(types || ["extension"], function (addons) {
485 context.incomplete = false;
486 context.completions = addons;
493 Components.utils.import("resource://gre/modules/AddonManager.jsm", this);
497 } catch(e){ if (isString(e)) e = Error(e); dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack); }
499 // vim: set fdm=marker sw=4 sts=4 ts=8 et ft=javascript: