]> git.donarmstrong.com Git - dactyl.git/blob - common/modules/addons.jsm
03c4e0fb9bf18ff2a4920dac0cdd5706b69fd975
[dactyl.git] / common / modules / addons.jsm
1 // Copyright (c) 2009-2011 by 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 Components.utils.import("resource://dactyl/bootstrap.jsm");
11 defineModule("addons", {
12     exports: ["AddonManager", "Addons", "Addon", "addons"],
13     require: ["services"],
14     use: ["completion", "config", "io", "messages", "prefs", "template", "util"]
15 }, this);
16
17 var callResult = function callResult(method) {
18     let args = Array.slice(arguments, 1);
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             "Add-on " + action + " " + event + ": " + (install.name || install.sourceURI.spec) +
26             (install.error ? ": " + addonErrors[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 != addon);
68         if (!this.remaining.length)
69             this.dactyl.echomsg(
70                 this.upgrade.length
71                     ? "Installing updates for addons: " + this.upgrade.map(function (i) i.name).join(", ")
72                     : "No addon updates found");
73     }
74 });
75
76 var actions = {
77     delete: {
78         name: "extde[lete]",
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 ({ item }) item.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 ({ item }) !item.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 ({ item }) item.isActive && item.optionsURL
108     },
109     rehash: {
110         name: "extr[ehash]",
111         description: "Reload an extension",
112         action: function (addon) {
113             util.assert(util.haveGecko("2b"), _("error.notUseful", config.host));
114             util.timeout(function () {
115                 addon.userDisabled = true;
116                 addon.userDisabled = false;
117             });
118         },
119         get filter() {
120             let ids = set(keys(JSON.parse(prefs.get("extensions.bootstrappedAddons", "{}"))));
121             return function ({ item }) !item.userDisabled && set.has(ids, item.id);
122         },
123         perm: "disable"
124     },
125     toggle: {
126         name: "extt[oggle]",
127         description: "Toggle an extension's enabled status",
128         action: function (addon) { addon.userDisabled = !addon.userDisabled; }
129     },
130     update: {
131         name: "extu[pdate]",
132         description: "Update an extension",
133         actions: updateAddons,
134         perm: "upgrade"
135     }
136 };
137
138 var Addon = Class("Addon", {
139     init: function init(addon, list) {
140         this.addon = addon;
141         this.instance = this;
142         this.list = list;
143
144         this.nodes = {
145             commandTarget: this
146         };
147         XML.ignoreWhitespace = true;
148         util.xmlToDom(
149             <tr highlight="Addon" key="row" xmlns:dactyl={NS} xmlns={XHTML}>
150                 <td highlight="AddonName" key="name"/>
151                 <td highlight="AddonVersion" key="version"/>
152                 <td highlight="AddonStatus" key="status"/>
153                 <td highlight="AddonButtons Buttons">
154                     <a highlight="Button" key="enable">On&#xa0;</a>
155                     <a highlight="Button" key="disable">Off</a>
156                     <a highlight="Button" key="delete">Del</a>
157                     <a highlight="Button" key="update">Upd</a>
158                     <a highlight="Button" key="options">Opt</a>
159                 </td>
160                 <td highlight="AddonDescription" key="description"/>
161             </tr>,
162             this.list.document, this.nodes);
163
164         this.update();
165     },
166
167     commandAllowed: function commandAllowed(cmd) {
168         util.assert(set.has(actions, cmd), "Unknown command");
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({ item: this }))
174             return false;
175         return true;
176     },
177
178     command: function command(cmd) {
179         util.assert(this.commandAllowed(cmd), "Command not allowed");
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         XML.ignoreWhitespace = XML.prettyPrinting = false;
192         default xml namespace = XHTML;
193
194         let info = this.isActive ? <span highlight="Enabled">enabled</span>
195                                  : <span highlight="Disabled">disabled</span>;
196
197         let pending;
198         if (this.pendingOperations & AddonManager.PENDING_UNINSTALL)
199             pending = ["Disabled", "uninstalled"];
200         else if (this.pendingOperations & AddonManager.PENDING_DISABLE)
201             pending = ["Disabled", "disabled"];
202         else if (this.pendingOperations & AddonManager.PENDING_INSTALL)
203             pending = ["Enabled", "installed"];
204         else if (this.pendingOperations & AddonManager.PENDING_ENABLE)
205             pending = ["Enabled", "enabled"];
206         else if (this.pendingOperations & AddonManager.PENDING_UPGRADE)
207             pending = ["Enabled", "upgraded"];
208         if (pending)
209             return <>{info}&#xa0;(<span highlight={pending[0]}>{pending[1]}</span>
210                                   &#xa0;on <a href="#" dactyl:command="dactyl.restart" xmlns:dactyl={NS}>restart</a>)</>;
211         return info;
212     },
213
214     update: function callee() {
215         let self = this;
216         function update(key, xml) {
217             let node = self.nodes[key];
218             while (node.firstChild)
219                 node.removeChild(node.firstChild);
220             node.appendChild(util.xmlToDom(<>{xml}</>, self.list.document));
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
228         for (let node in values(this.nodes))
229             if (node.update && node.update !== callee)
230                 node.update();
231
232         let event = this.list.document.createEvent("Events");
233         event.initEvent("dactyl-commandupdate", true, false);
234         this.list.document.dispatchEvent(event);
235     }
236 });
237
238 ["cancelUninstall", "findUpdates", "getResourceURI", "hasResource", "isCompatibleWith",
239  "uninstall"].forEach(function (prop) {
240      Addon.prototype[prop] = function proxy() this.addon[prop].apply(this.addon, arguments);
241 });
242
243 ["aboutURL", "appDisabled", "applyBackgroundUpdates", "blocklistState", "contributors", "creator",
244  "description", "developers", "homepageURL", "iconURL", "id", "install", "installDate", "isActive",
245  "isCompatible", "isPlatformCompatible", "name", "operationsRequiringRestart", "optionsURL",
246  "pendingOperations", "pendingUpgrade", "permissions", "providesUpdatesSecurely", "releaseNotesURI",
247  "scope", "screenshots", "size", "sourceURI", "translators", "type", "updateDate", "userDisabled",
248  "version"].forEach(function (prop) {
249     Object.defineProperty(Addon.prototype, prop, {
250         get: function get_proxy() this.addon[prop],
251         set: function set_proxy(val) this.addon[prop] = val
252     });
253 });
254
255 var AddonList = Class("AddonList", {
256     init: function init(modules, types, filter) {
257         this.modules = modules;
258         this.filter = filter && filter.toLowerCase();
259         this.nodes = {};
260         this.addons = [];
261         this.ready = false;
262
263         AddonManager.getAddonsByTypes(types, this.closure(function (addons) {
264             this._addons = addons;
265             if (this.document)
266                 this._init();
267         }));
268         AddonManager.addAddonListener(this);
269     },
270     cleanup: function cleanup() {
271         AddonManager.removeAddonListener(this);
272     },
273
274     _init: function _init() {
275         this._addons.forEach(this.closure.addAddon);
276         this.ready = true;
277         this.update();
278     },
279
280     message: Class.memoize(function () {
281
282         XML.ignoreWhitespace = true;
283         util.xmlToDom(<table highlight="Addons" key="list" xmlns={XHTML}>
284                         <tr highlight="AddonHead">
285                             <td>Name</td>
286                             <td>Version</td>
287                             <td>Status</td>
288                             <td/>
289                             <td>Description</td>
290                         </tr>
291                       </table>, this.document, this.nodes);
292
293         if (this._addons)
294             this._init();
295
296         return this.nodes.list;
297     }),
298
299     addAddon: function addAddon(addon) {
300         if (addon.id in this.addons)
301             this.update(addon);
302         else {
303             if (this.filter && addon.name.toLowerCase().indexOf(this.filter) === -1)
304                 return;
305
306             addon = Addon(addon, this);
307             this.addons[addon.id] = addon;
308
309             let index = values(this.addons).sort(function (a, b) a.compare(b))
310                                            .indexOf(addon);
311
312             this.nodes.list.insertBefore(addon.nodes.row,
313                                          this.nodes.list.childNodes[index + 1]);
314             this.update();
315         }
316     },
317     removeAddon: function removeAddon(addon) {
318         if (addon.id in this.addons) {
319             this.nodes.list.removeChild(this.addons[addon.id].nodes.row);
320             delete this.addons[addon.id];
321             this.update();
322         }
323     },
324
325     leave: function leave(stack) {
326         if (stack.pop)
327             this.cleanup();
328     },
329
330     update: function update(addon) {
331         if (addon && addon.id in this.addons)
332             this.addons[addon.id].update();
333         if (this.ready)
334             this.modules.mow.resize(false);
335     },
336
337     onDisabled:           function (addon) { this.update(addon); },
338     onDisabling:          function (addon) { this.update(addon); },
339     onEnabled:            function (addon) { this.update(addon); },
340     onEnabling:           function (addon) { this.update(addon); },
341     onInstalled:          function (addon) { this.addAddon(addon); },
342     onInstalling:         function (addon) { this.update(addon); },
343     onUninstalled:        function (addon) { this.removeAddon(addon); },
344     onUninstalling:       function (addon) { this.update(addon); },
345     onOperationCancelled: function (addon) { this.update(addon); },
346     onPropertyChanged: function onPropertyChanged(addon, properties) {}
347 });
348
349 var Addons = Module("addons", {
350 }, {
351 }, {
352     commands: function (dactyl, modules, window) {
353         const { CommandOption, commands, completion } = modules;
354
355         commands.add(["addo[ns]", "ao"],
356             "List installed extensions",
357             function (args) {
358                 let addons = AddonList(modules, args["-types"], args[0]);
359                 modules.commandline.echo(addons);
360
361                 if (modules.commandline.savingOutput)
362                     util.waitFor(function () addons.ready);
363             },
364             {
365                 argCount: "?",
366                 options: [
367                     {
368                         names: ["-types", "-type", "-t"],
369                         description: "The add-on types to list",
370                         default: ["extension"],
371                         completer: function (context, args) completion.addonType(context),
372                         type: CommandOption.LIST
373                     }
374                 ]
375             });
376
377         let addonListener = AddonListener(modules);
378
379         commands.add(["exta[dd]"],
380             "Install an extension",
381             function (args) {
382                 let url  = args[0];
383                 let file = io.File(url);
384                 function install(addonInstall) {
385                     addonInstall.addListener(addonListener);
386                     addonInstall.install();
387                 }
388
389                 if (!file.exists())
390                     AddonManager.getInstallForURL(url,   install, "application/x-xpinstall");
391                 else if (file.isReadable() && file.isFile())
392                     AddonManager.getInstallForFile(file, install, "application/x-xpinstall");
393                 else if (file.isDirectory())
394                     dactyl.echoerr(_("addon.cantInstallDir", file.path.quote()));
395                 else
396                     dactyl.echoerr(_("io.notReadable-1", file.path));
397             }, {
398                 argCount: "1",
399                 completer: function (context) {
400                     context.filters.push(function ({ isdir, text }) isdir || /\.xpi$/.test(text));
401                     completion.file(context);
402                 },
403                 literal: 0
404             });
405
406         // TODO: handle extension dependencies
407         values(actions).forEach(function (command) {
408             let perm = command.perm && AddonManager["PERM_CAN_" + command.perm.toUpperCase()];
409             function ok(addon) !perm || addon.permissions & perm;
410
411             commands.add(Array.concat(command.name),
412                 command.description,
413                 function (args) {
414                     let name = args[0];
415                     if (args.bang && !command.bang)
416                         dactyl.assert(!name, _("error.trailing"));
417                     else
418                         dactyl.assert(name, _("error.argumentRequired"));
419
420                     AddonManager.getAddonsByTypes(["extension"], dactyl.wrapCallback(function (list) {
421                         if (!args.bang || command.bang) {
422                             list = list.filter(function (extension) extension.name == name);
423                             if (list.length == 0)
424                                 return void dactyl.echoerr(_("error.invalidArgument", name));
425                             if (!list.every(ok))
426                                 return void dactyl.echoerr(_("error.invalidOperation"));
427                         }
428                         if (command.actions)
429                             command.actions(list, this.modules);
430                         else
431                             list.forEach(function (addon) command.action.call(this.modules, addon, args.bang), this);
432                     }));
433                 }, {
434                     argCount: "?", // FIXME: should be "1"
435                     bang: true,
436                     completer: function (context) {
437                         completion.extension(context);
438                         context.filters.push(function ({ item }) ok(item));
439                         if (command.filter)
440                             context.filters.push(command.filter);
441                     },
442                     literal: 0
443                 });
444         });
445     },
446     completion: function (dactyl, modules, window) {
447         completion.addonType = function addonType(context) {
448             let base = ["extension", "theme"];
449             function update(types) {
450                 context.completions = types.map(function (t) [t, util.capitalize(t)]);
451             }
452
453             context.generate = function generate() {
454                 update(base);
455                 if (AddonManager.getAllAddons) {
456                     context.incomplete = true;
457                     AddonManager.getAllAddons(function (addons) {
458                         context.incomplete = false;
459                         update(array.uniq(base.concat(addons.map(function (a) a.type)),
460                                           true));
461                     });
462                 }
463             }
464         }
465
466         completion.extension = function extension(context, types) {
467             context.title = ["Extension"];
468             context.anchored = false;
469             context.keys = { text: "name", description: "description", icon: "iconURL" },
470             context.generate = function () {
471                 context.incomplete = true;
472                 AddonManager.getAddonsByTypes(types || ["extension"], function (addons) {
473                     context.incomplete = false;
474                     context.completions = addons;
475                 });
476             };
477         };
478     }
479 });
480
481 if (!services.has("extensionManager"))
482     Components.utils.import("resource://gre/modules/AddonManager.jsm");
483 else
484     var AddonManager = {
485         PERM_CAN_UNINSTALL: 1,
486         PERM_CAN_ENABLE: 2,
487         PERM_CAN_DISABLE: 4,
488         PERM_CAN_UPGRADE: 8,
489
490         getAddonByID: function (id, callback) {
491             callback = callback || util.identity;
492             addon = services.extensionManager.getItemForID(id);
493             if (addon)
494                 addon = this.wrapAddon(addon);
495             return callback(addon);
496         },
497         wrapAddon: function wrapAddon(addon) {
498             addon = Object.create(addon.QueryInterface(Ci.nsIUpdateItem));
499
500             function getRdfProperty(item, property) {
501                 let resource = services.rdf.GetResource("urn:mozilla:item:" + item.id);
502                 let value = "";
503
504                 if (resource) {
505                     let target = services.extensionManager.datasource.GetTarget(resource,
506                         services.rdf.GetResource("http://www.mozilla.org/2004/em-rdf#" + property), true);
507                     if (target && target instanceof Ci.nsIRDFLiteral)
508                         value = target.Value;
509                 }
510
511                 return value;
512             }
513
514             ["aboutURL", "creator", "description", "developers",
515              "homepageURL", "installDate", "optionsURL",
516              "releaseNotesURI", "updateDate"].forEach(function (item) {
517                 memoize(addon, item, function (item) getRdfProperty(this, item));
518             });
519
520             update(addon, {
521
522                 get permissions() 1 | (this.userDisabled ? 2 : 4),
523
524                 appDisabled: false,
525
526                 installLocation: Class.memoize(function () services.extensionManager.getInstallLocation(this.id)),
527                 getResourceURI: function getResourceURI(path) {
528                     let file = this.installLocation.getItemFile(this.id, path);
529                     return services.io.newFileURI(file);
530                 },
531
532                 isActive: getRdfProperty(addon, "isDisabled") != "true",
533
534                 uninstall: function uninstall() {
535                     services.extensionManager.uninstallItem(this.id);
536                 },
537
538                 get userDisabled() getRdfProperty(addon, "userDisabled") === "true",
539                 set userDisabled(val) {
540                     services.extensionManager[val ? "disableItem" : "enableItem"](this.id);
541                 }
542             });
543
544             return addon;
545         },
546         getAddonsByTypes: function (types, callback) {
547             let res = [];
548             for (let [, type] in Iterator(types))
549                 for (let [, item] in Iterator(services.extensionManager
550                             .getItemList(Ci.nsIUpdateItem["TYPE_" + type.toUpperCase()], {})))
551                     res.push(this.wrapAddon(item));
552
553             if (callback)
554                 util.timeout(function () { callback(res); });
555             return res;
556         },
557         getInstallForFile: function (file, callback, mimetype) {
558             callback({
559                 addListener: function () {},
560                 install: function () {
561                     services.extensionManager.installItemFromFile(file, "app-profile");
562                 }
563             });
564         },
565         getInstallForURL: function (url, callback, mimetype) {
566             util.assert(false, _("error.unavailable", config.host, services.runtime.version));
567         },
568         observers: [],
569         addAddonListener: function (listener) {
570             observer.listener = listener;
571             function observer(subject, topic, data) {
572                 if (subject instanceof Ci.nsIUpdateItem)
573                     subject = AddonManager.wrapAddon(subject);
574
575                 if (data === "item-installed")
576                     listener.onInstalling(subject, true);
577                 else if (data === "item-uninstalled")
578                     listener.onUnistalling(subject, true);
579                 else if (data === "item-upgraded")
580                     listener.onInstalling(subject, true);
581                 else if (data === "item-enabled")
582                     listener.onEnabling(subject, true);
583                 else if (data === "item-disabled")
584                     listener.onDisabling(subject, true);
585             }
586             services.observer.addObserver(observer, "em-action-requested", false);
587             this.observers.push(observer);
588         },
589         removeAddonListener: function (listener) {
590             this.observers = this.observers.filter(function (observer) {
591                 if (observer.listener !== listener)
592                     return true;
593                 services.observer.removeObserver(observer, "em-action-requested");
594             });
595         }
596     };
597
598 var addonErrors = array.toObject([
599     [AddonManager.ERROR_NETWORK_FAILURE, "A network error occurred"],
600     [AddonManager.ERROR_INCORRECT_HASH,  "The downloaded file did not match the expected hash"],
601     [AddonManager.ERROR_CORRUPT_FILE,    "The file appears to be corrupt"],
602     [AddonManager.ERROR_FILE_ACCESS,     "There was an error accessing the filesystem"]]);
603
604 endModule();
605
606 } catch(e){ if (isString(e)) e = Error(e); dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack); }
607
608 // vim: set fdm=marker sw=4 ts=4 et ft=javascript: