]> git.donarmstrong.com Git - dactyl.git/blob - common/modules/addons.jsm
Import r6948 from upstream hg supporting Firefox up to 24.*
[dactyl.git] / common / modules / addons.jsm
1 // Copyright (c) 2009-2012 Kris Maglione <maglione.k@gmail.com>
2 // Copyright (c) 2009-2010 by Doug Kearns <dougkearns@gmail.com>
3 //
4 // This work is licensed for reuse under an MIT license. Details are
5 // given in the LICENSE.txt file included with this file.
6 "use strict";
7
8 try {
9
10 defineModule("addons", {
11     exports: ["AddonManager", "Addons", "Addon", "addons"],
12     require: ["services", "util"]
13 });
14
15 this.lazyRequire("completion", ["completion"]);
16 lazyRequire("template", ["template"]);
17
18 var callResult = function callResult(method, ...args) {
19     return function (result) { result[method].apply(result, args); };
20 }
21
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] : "")));
27     };
28
29 var AddonListener = Class("AddonListener", {
30     init: function init(modules) {
31         this.dactyl = modules.dactyl;
32     },
33
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")
45 });
46
47 var updateAddons = Class("UpgradeListener", AddonListener, {
48     init: function init(addons, modules) {
49         init.supercall(this, modules);
50
51         util.assert(!addons.length || addons[0].findUpdates,
52                     _("error.unavailable", config.host, services.runtime.version));
53
54         this.remaining = addons;
55         this.upgrade = [];
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);
59
60     },
61     onUpdateAvailable: function (addon, install) {
62         this.upgrade.push(addon);
63         install.addListener(this);
64         install.install();
65     },
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)
69             this.dactyl.echomsg(
70                 this.upgrade.length
71                     ? _("addon.installingUpdates", this.upgrade.map(function (i) i.name).join(", "))
72                     : _("addon.noUpdates"));
73     }
74 });
75
76 var actions = {
77     delete: {
78         name: ["extde[lete]", "extrm"],
79         description: "Uninstall an extension",
80         action: callResult("uninstall"),
81         perm: "uninstall"
82     },
83     enable: {
84         name: "exte[nable]",
85         description: "Enable an extension",
86         action: function (addon) { addon.userDisabled = false; },
87         filter: function (addon) addon.userDisabled,
88         perm: "enable"
89     },
90     disable: {
91         name: "extd[isable]",
92         description: "Disable an extension",
93         action: function (addon) { addon.userDisabled = true; },
94         filter: function (addon) !addon.userDisabled,
95         perm: "disable"
96     },
97     options: {
98         name: ["exto[ptions]", "extp[references]"],
99         description: "Open an extension's preference dialog",
100         bang: true,
101         action: function (addon, bang) {
102             if (bang)
103                 this.window.openDialog(addon.optionsURL, "_blank", "chrome");
104             else
105                 this.dactyl.open(addon.optionsURL, { from: "extoptions" });
106         },
107         filter: function (addon) addon.isActive && addon.optionsURL
108     },
109     rehash: {
110         name: "extr[ehash]",
111         description: "Reload an extension",
112         action: function (addon) {
113             util.assert(config.haveGecko("2b"), _("command.notUseful", config.host));
114             util.flushCache();
115             util.timeout(function () {
116                 addon.userDisabled = true;
117                 addon.userDisabled = false;
118             });
119         },
120         get filter() {
121             return function (addon) !addon.userDisabled &&
122                 !(addon.operationsRequiringRestart & (AddonManager.OP_NEEDS_RESTART_ENABLE | AddonManager.OP_NEEDS_RESTART_DISABLE));
123         },
124         perm: "disable"
125     },
126     toggle: {
127         name: "extt[oggle]",
128         description: "Toggle an extension's enabled status",
129         action: function (addon) { addon.userDisabled = !addon.userDisabled; }
130     },
131     update: {
132         name: "extu[pdate]",
133         description: "Update an extension",
134         actions: updateAddons,
135         perm: "upgrade"
136     }
137 };
138
139 var Addon = Class("Addon", {
140     init: function init(addon, list) {
141         this.addon = addon;
142         this.instance = this;
143         this.list = list;
144
145         this.nodes = {
146             commandTarget: this
147         };
148         DOM.fromJSON(
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);
161
162         this.update();
163     },
164
165     commandAllowed: function commandAllowed(cmd) {
166         util.assert(Set.has(actions, cmd), _("addon.unknownCommand"));
167
168         let action = actions[cmd];
169         if ("perm" in action && !(this.permissions & AddonManager["PERM_CAN_" + action.perm.toUpperCase()]))
170             return false;
171         if ("filter" in action && !action.filter(this))
172             return false;
173         return true;
174     },
175
176     command: function command(cmd) {
177         util.assert(this.commandAllowed(cmd), _("addon.commandNotAllowed"));
178
179         let action = actions[cmd];
180         if (action.action)
181             action.action.call(this.list.modules, this, true);
182         else
183             action.actions([this], this.list.modules);
184     },
185
186     compare: function compare(other) String.localeCompare(this.name, other.name),
187
188     get statusInfo() {
189         let info = this.isActive ? ["span", { highlight: "Enabled" }, "enabled"]
190                                  : ["span", { highlight: "Disabled" }, "disabled"];
191
192         let pending;
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"];
203         if (pending)
204             return [info, " (",
205                     ["span", { highlight: pending[0] }, pending[1]],
206                     " on ",
207                     ["a", { href: "#", "dactyl:command": "dactyl.restart" }, "restart"],
208                     ")"];
209         return info;
210     },
211
212     update: function callee() {
213         let update = (key, xml) => {
214             let node = this.nodes[key];
215             while (node.firstChild)
216                 node.removeChild(node.firstChild);
217
218             DOM(node).append(isArray(xml) ? xml : DOM.DOMString(xml));
219         }
220
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);
226
227         for (let node in values(this.nodes))
228             if (node.update && node.update !== callee)
229                 node.update();
230
231         let event = this.list.document.createEvent("Events");
232         event.initEvent("dactyl-commandupdate", true, false);
233         this.list.document.dispatchEvent(event);
234     }
235 });
236
237 ["cancelUninstall", "findUpdates", "getResourceURI", "hasResource", "isCompatibleWith",
238  "uninstall"].forEach(function (prop) {
239      Addon.prototype[prop] = function proxy() this.addon[prop].apply(this.addon, arguments);
240 });
241
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
251     });
252 });
253
254 var AddonList = Class("AddonList", {
255     init: function init(modules, types, filter) {
256         this.modules = modules;
257         this.filter = filter && filter.toLowerCase();
258         this.nodes = {};
259         this.addons = [];
260         this.ready = false;
261
262         AddonManager.getAddonsByTypes(types, this.closure(function (addons) {
263             this._addons = addons;
264             if (this.document)
265                 this._init();
266         }));
267         AddonManager.addAddonListener(this);
268     },
269     cleanup: function cleanup() {
270         AddonManager.removeAddonListener(this);
271     },
272
273     _init: function _init() {
274         this._addons.forEach(this.closure.addAddon);
275         this.ready = true;
276         this.update();
277     },
278
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")],
284                             ["td"],
285                             ["td", {}, _("title.Status")],
286                             ["td", {}, _("title.Description")]]],
287                       this.document, this.nodes);
288
289         if (this._addons)
290             this._init();
291
292         return this.nodes.list;
293     }),
294
295     addAddon: function addAddon(addon) {
296         if (addon.id in this.addons)
297             this.update(addon);
298         else {
299             if (this.filter && addon.name.toLowerCase().indexOf(this.filter) === -1)
300                 return;
301
302             addon = Addon(addon, this);
303             this.addons[addon.id] = addon;
304
305             let index = values(this.addons).sort(function (a, b) a.compare(b))
306                                            .indexOf(addon);
307
308             this.nodes.list.insertBefore(addon.nodes.row,
309                                          this.nodes.list.childNodes[index + 1]);
310             this.update();
311         }
312     },
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];
317             this.update();
318         }
319     },
320
321     leave: function leave(stack) {
322         if (stack.pop)
323             this.cleanup();
324     },
325
326     update: function update(addon) {
327         if (addon && addon.id in this.addons)
328             this.addons[addon.id].update();
329         if (this.ready)
330             this.modules.mow.resize(false);
331     },
332
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) {}
343 });
344
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)])
350                 .toObject())
351 }, {
352 }, {
353     commands: function initCommands(dactyl, modules, window) {
354         const { CommandOption, commands, completion, io } = modules;
355
356         commands.add(["addo[ns]", "ao"],
357             "List installed extensions",
358             function (args) {
359                 let addons = AddonList(modules, args["-types"], args[0]);
360                 modules.commandline.echo(addons);
361
362                 if (modules.commandline.savingOutput)
363                     util.waitFor(function () addons.ready);
364             },
365             {
366                 argCount: "?",
367                 options: [
368                     {
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
374                     }
375                 ]
376             });
377
378         let addonListener = AddonListener(modules);
379
380         commands.add(["exta[dd]"],
381             "Install an extension",
382             function (args) {
383                 let url  = args[0];
384                 let file = io.File(url);
385                 function install(addonInstall) {
386                     addonInstall.addListener(addonListener);
387                     addonInstall.install();
388                 }
389
390                 if (!file.exists())
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()));
396                 else
397                     dactyl.echoerr(_("io.notReadable", file.path));
398             }, {
399                 argCount: "1",
400                 completer: function (context) {
401                     context.filters.push(function ({ isdir, text }) isdir || /\.xpi$/.test(text));
402                     completion.file(context);
403                 },
404                 literal: 0
405             });
406
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));
411
412             commands.add(Array.concat(command.name),
413                 command.description,
414                 function (args) {
415                     let name = args[0];
416                     if (args.bang && !command.bang)
417                         dactyl.assert(!name, _("error.trailingCharacters"));
418                     else
419                         dactyl.assert(name, _("error.argumentRequired"));
420
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);
427                         }
428                         dactyl.assert(list.every(ok));
429                         if (command.actions)
430                             command.actions(list, this.modules);
431                         else
432                             list.forEach(function (addon) command.action.call(this.modules, addon, args.bang), this);
433                     }));
434                 }, {
435                     argCount: "?", // FIXME: should be "1"
436                     bang: true,
437                     completer: function (context, args) {
438                         completion.addon(context, args["-types"]);
439                         context.filters.push(function ({ item }) ok(item));
440                     },
441                     literal: 0,
442                     options: [
443                         {
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
449                         }
450                     ]
451                 });
452         });
453     },
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)]);
459             }
460
461             context.generate = function generate() {
462                 update(base);
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)),
468                                           true));
469                     });
470                 }
471             };
472         };
473
474         completion.addon = function addon(context, types) {
475             context.title = ["Add-on"];
476             context.anchored = false;
477             context.keys = {
478                 text: function (addon) [addon.name, addon.id],
479                 description: "description",
480                 icon: "iconURL"
481             };
482             context.generate = function () {
483                 context.incomplete = true;
484                 AddonManager.getAddonsByTypes(types || ["extension"], function (addons) {
485                     context.incomplete = false;
486                     context.completions = addons;
487                 });
488             };
489         };
490     }
491 });
492
493 Components.utils.import("resource://gre/modules/AddonManager.jsm", this);
494
495 endModule();
496
497 } catch(e){ if (isString(e)) e = Error(e); dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack); }
498
499 // vim: set fdm=marker sw=4 sts=4 ts=8 et ft=javascript: