]> git.donarmstrong.com Git - dactyl.git/blob - common/modules/addons.jsm
finalize changelog for 7904
[dactyl.git] / common / modules / addons.jsm
1 // Copyright (c) 2009-2014 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(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(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(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.flushCache();
114             util.timeout(function () {
115                 addon.userDisabled = true;
116                 addon.userDisabled = false;
117             });
118         },
119         get filter() {
120             return addon => (
121                 !addon.userDisabled &&
122                 !(addon.operationsRequiringRestart & (AddonManager.OP_NEEDS_RESTART_ENABLE
123                                                      | AddonManager.OP_NEEDS_RESTART_DISABLE)));
124         },
125         perm: "disable"
126     },
127     toggle: {
128         name: "extt[oggle]",
129         description: "Toggle an extension's enabled status",
130         action: function (addon) { addon.userDisabled = !addon.userDisabled; }
131     },
132     update: {
133         name: "extu[pdate]",
134         description: "Update an extension",
135         actions: updateAddons,
136         perm: "upgrade"
137     }
138 };
139
140 var Addon = Class("Addon", {
141     init: function init(addon, list) {
142         this.addon = addon;
143         this.instance = this;
144         this.list = list;
145
146         this.nodes = {
147             commandTarget: this
148         };
149         DOM.fromJSON(
150             ["tr", { highlight: "Addon", key: "row" },
151                 ["td", { highlight: "AddonName", key: "name" }],
152                 ["td", { highlight: "AddonVersion", key: "version" }],
153                 ["td", { highlight: "AddonButtons Buttons" },
154                     ["a", { highlight: "Button", href: "javascript:0", key: "enable" }, _("addon.action.On")],
155                     ["a", { highlight: "Button", href: "javascript:0", key: "disable" }, _("addon.action.Off")],
156                     ["a", { highlight: "Button", href: "javascript:0", key: "delete" }, _("addon.action.Delete")],
157                     ["a", { highlight: "Button", href: "javascript:0", key: "update" }, _("addon.action.Update")],
158                     ["a", { highlight: "Button", href: "javascript:0", key: "options" }, _("addon.action.Options")]],
159                 ["td", { highlight: "AddonStatus", key: "status" }],
160                 ["td", { highlight: "AddonDescription", key: "description" }]],
161             this.list.document, this.nodes);
162
163         this.update();
164     },
165
166     commandAllowed: function commandAllowed(cmd) {
167         util.assert(hasOwnProperty(actions, cmd),
168                     _("addon.unknownCommand"));
169
170         let action = actions[cmd];
171         if ("perm" in action && !(this.permissions & AddonManager["PERM_CAN_" + action.perm.toUpperCase()]))
172             return false;
173         if ("filter" in action && !action.filter(this))
174             return false;
175         return true;
176     },
177
178     command: function command(cmd) {
179         util.assert(this.commandAllowed(cmd), _("addon.commandNotAllowed"));
180
181         let action = actions[cmd];
182         if (action.action)
183             action.action.call(this.list.modules, this, true);
184         else
185             action.actions([this], this.list.modules);
186     },
187
188     compare: function compare(other) String.localeCompare(this.name, other.name),
189
190     get statusInfo() {
191         let info = this.isActive ? ["span", { highlight: "Enabled" }, "enabled"]
192                                  : ["span", { highlight: "Disabled" }, "disabled"];
193
194         let pending;
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"];
205         if (pending)
206             return [info, " (",
207                     ["span", { highlight: pending[0] }, pending[1]],
208                     " on ",
209                     ["a", { href: "#", "dactyl:command": "dactyl.restart" }, "restart"],
210                     ")"];
211         return info;
212     },
213
214     update: function callee() {
215         let update = (key, xml) => {
216             let node = this.nodes[key];
217             while (node.firstChild)
218                 node.removeChild(node.firstChild);
219
220             DOM(node).append(isArray(xml) ? xml : DOM.DOMString(xml));
221         }
222
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);
228
229         for (let node in values(this.nodes))
230             if (node.update && node.update !== callee)
231                 node.update();
232
233         let event = this.list.document.createEvent("Events");
234         event.initEvent("dactyl-commandupdate", true, false);
235         this.list.document.dispatchEvent(event);
236     }
237 });
238
239 ["cancelUninstall", "findUpdates", "getResourceURI", "hasResource", "isCompatibleWith",
240  "uninstall"].forEach(function (prop) {
241      Addon.prototype[prop] = function proxy() this.addon[prop].apply(this.addon, arguments);
242 });
243
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
253     });
254 });
255
256 var AddonList = Class("AddonList", {
257     init: function init(modules, types, filter) {
258         this.modules = modules;
259         this.filter = filter && filter.toLowerCase();
260         this.nodes = {};
261         this.addons = {};
262         this.ready = false;
263
264         AddonManager.getAddonsByTypes(types, addons => {
265             this._addons = addons;
266             if (this.document)
267                 this._init();
268         });
269         AddonManager.addAddonListener(this);
270     },
271     cleanup: function cleanup() {
272         AddonManager.removeAddonListener(this);
273     },
274
275     _init: function _init() {
276         this._addons.forEach(this.bound.addAddon);
277         this.ready = true;
278         this.update();
279     },
280
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")],
286                             ["td"],
287                             ["td", {}, _("title.Status")],
288                             ["td", {}, _("title.Description")]]],
289                       this.document, this.nodes);
290
291         if (this._addons)
292             this._init();
293
294         return this.nodes.list;
295     }),
296
297     addAddon: function addAddon(addon) {
298         if (addon.id in this.addons)
299             this.update(addon);
300         else {
301             if (this.filter && addon.name.toLowerCase().indexOf(this.filter) === -1)
302                 return;
303
304             addon = Addon(addon, this);
305             this.addons[addon.id] = addon;
306
307             let index = values(this.addons).sort((a, b) => a.compare(b))
308                                            .indexOf(addon);
309
310             this.nodes.list.insertBefore(addon.nodes.row,
311                                          this.nodes.list.childNodes[index + 1]);
312             this.update();
313         }
314     },
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];
319             this.update();
320         }
321     },
322
323     leave: function leave(stack) {
324         if (stack.pop)
325             this.cleanup();
326     },
327
328     update: function update(addon) {
329         if (addon && addon.id in this.addons)
330             this.addons[addon.id].update();
331         if (this.ready)
332             this.modules.mow.resize(false);
333     },
334
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) {}
345 });
346
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)])
352                 .toObject())
353 }, {
354 }, {
355     commands: function initCommands(dactyl, modules, window) {
356         const { CommandOption, commands, completion, io } = modules;
357
358         commands.add(["addo[ns]", "ao"],
359             "List installed extensions",
360             function (args) {
361                 let addons = AddonList(modules, args["-types"], args[0]);
362                 modules.commandline.echo(addons);
363
364                 if (modules.commandline.savingOutput)
365                     util.waitFor(() => addons.ready);
366             },
367             {
368                 argCount: "?",
369                 options: [
370                     {
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
376                     }
377                 ]
378             });
379
380         let addonListener = AddonListener(modules);
381
382         commands.add(["exta[dd]"],
383             "Install an extension",
384             function (args) {
385                 let url  = args[0];
386                 let file = io.File(url);
387                 function install(addonInstall) {
388                     addonInstall.addListener(addonListener);
389                     addonInstall.install();
390                 }
391
392                 if (!file.exists())
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()));
398                 else
399                     dactyl.echoerr(_("io.notReadable", file.path));
400             }, {
401                 argCount: "1",
402                 completer: function (context) {
403                     context.filters.push(({ isdir, text }) => isdir || /\.xpi$/.test(text));
404                     completion.file(context);
405                 },
406                 literal: 0
407             });
408
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));
413
414             commands.add(Array.concat(command.name),
415                 command.description,
416                 function (args) {
417                     let name = args[0];
418                     if (args.bang && !command.bang)
419                         dactyl.assert(!name, _("error.trailingCharacters"));
420                     else
421                         dactyl.assert(name, _("error.argumentRequired"));
422
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);
429                         }
430                         dactyl.assert(list.every(ok));
431                         if (command.actions)
432                             command.actions(list, this.modules);
433                         else
434                             list.forEach(addon => { command.action.call(this.modules, addon, args.bang) });
435                     }));
436                 }, {
437                     argCount: "?", // FIXME: should be "1"
438                     bang: true,
439                     completer: function (context, args) {
440                         completion.addon(context, args["-types"]);
441                         context.filters.push(({ item }) => ok(item));
442                     },
443                     literal: 0,
444                     options: [
445                         {
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
451                         }
452                     ]
453                 });
454         });
455     },
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)]);
461             }
462
463             context.generate = function generate() {
464                 update(base);
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)),
470                                           true));
471                     });
472                 }
473             };
474         };
475
476         completion.addon = function addon(context, types) {
477             context.title = ["Add-on"];
478             context.anchored = false;
479             context.keys = {
480                 text: addon => [addon.name, addon.id],
481                 description: "description",
482                 icon: "iconURL"
483             };
484             context.generate = function () {
485                 context.incomplete = true;
486                 AddonManager.getAddonsByTypes(types || ["extension"], function (addons) {
487                     context.incomplete = false;
488                     context.completions = addons;
489                 });
490             };
491         };
492     }
493 });
494
495 Components.utils.import("resource://gre/modules/AddonManager.jsm", this);
496
497 endModule();
498
499 } catch(e){ if (isString(e)) e = Error(e); dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack); }
500
501 // vim: set fdm=marker sw=4 sts=4 ts=8 et ft=javascript: