]> git.donarmstrong.com Git - dactyl.git/blob - common/modules/addons.jsm
Import 1.0b7.1 supporting Firefox up to 8.*
[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             _("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         util.dump("onUpdateAvailable");
63         this.upgrade.push(addon);
64         install.addListener(this);
65         install.install();
66     },
67     onUpdateFinished: function (addon, error) {
68         this.remaining = this.remaining.filter(function (a) a.type != addon.type || a.id != addon.id);
69         if (!this.remaining.length)
70             this.dactyl.echomsg(
71                 this.upgrade.length
72                     ? _("addon.installingUpdates", this.upgrade.map(function (i) i.name).join(", "))
73                     : _("addon.noUpdates"));
74     }
75 });
76
77 var actions = {
78     delete: {
79         name: "extde[lete]",
80         description: "Uninstall an extension",
81         action: callResult("uninstall"),
82         perm: "uninstall"
83     },
84     enable: {
85         name: "exte[nable]",
86         description: "Enable an extension",
87         action: function (addon) { addon.userDisabled = false; },
88         filter: function ({ item }) item.userDisabled,
89         perm: "enable"
90     },
91     disable: {
92         name: "extd[isable]",
93         description: "Disable an extension",
94         action: function (addon) { addon.userDisabled = true; },
95         filter: function ({ item }) !item.userDisabled,
96         perm: "disable"
97     },
98     options: {
99         name: ["exto[ptions]", "extp[references]"],
100         description: "Open an extension's preference dialog",
101         bang: true,
102         action: function (addon, bang) {
103             if (bang)
104                 this.window.openDialog(addon.optionsURL, "_blank", "chrome");
105             else
106                 this.dactyl.open(addon.optionsURL, { from: "extoptions" });
107         },
108         filter: function ({ item }) item.isActive && item.optionsURL
109     },
110     rehash: {
111         name: "extr[ehash]",
112         description: "Reload an extension",
113         action: function (addon) {
114             util.assert(util.haveGecko("2b"), _("command.notUseful", config.host));
115             util.timeout(function () {
116                 addon.userDisabled = true;
117                 addon.userDisabled = false;
118             });
119         },
120         get filter() {
121             let ids = Set(keys(JSON.parse(prefs.get("extensions.bootstrappedAddons", "{}"))));
122             return function ({ item }) !item.userDisabled && Set.has(ids, item.id);
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         XML.ignoreWhitespace = true;
149         util.xmlToDom(
150             <tr highlight="Addon" key="row" xmlns:dactyl={NS} xmlns={XHTML}>
151                 <td highlight="AddonName" key="name"/>
152                 <td highlight="AddonVersion" key="version"/>
153                 <td highlight="AddonStatus" key="status"/>
154                 <td highlight="AddonButtons Buttons">
155                     <a highlight="Button" href="javascript:0" key="enable">{_("addon.action.On")}</a>
156                     <a highlight="Button" href="javascript:0" key="disable">{_("addon.action.Off")}</a>
157                     <a highlight="Button" href="javascript:0" key="delete">{_("addon.action.Delete")}</a>
158                     <a highlight="Button" href="javascript:0" key="update">{_("addon.action.Update")}</a>
159                     <a highlight="Button" href="javascript:0" key="options">{_("addon.action.Options")}</a>
160                 </td>
161                 <td highlight="AddonDescription" key="description"/>
162             </tr>,
163             this.list.document, this.nodes);
164
165         this.update();
166     },
167
168     commandAllowed: function commandAllowed(cmd) {
169         util.assert(Set.has(actions, cmd), _("addon.unknownCommand"));
170
171         let action = actions[cmd];
172         if ("perm" in action && !(this.permissions & AddonManager["PERM_CAN_" + action.perm.toUpperCase()]))
173             return false;
174         if ("filter" in action && !action.filter({ item: this }))
175             return false;
176         return true;
177     },
178
179     command: function command(cmd) {
180         util.assert(this.commandAllowed(cmd), _("addon.commandNotAllowed"));
181
182         let action = actions[cmd];
183         if (action.action)
184             action.action.call(this.list.modules, this, true);
185         else
186             action.actions([this], this.list.modules);
187     },
188
189     compare: function compare(other) String.localeCompare(this.name, other.name),
190
191     get statusInfo() {
192         XML.ignoreWhitespace = XML.prettyPrinting = false;
193         default xml namespace = XHTML;
194
195         let info = this.isActive ? <span highlight="Enabled">enabled</span>
196                                  : <span highlight="Disabled">disabled</span>;
197
198         let pending;
199         if (this.pendingOperations & AddonManager.PENDING_UNINSTALL)
200             pending = ["Disabled", "uninstalled"];
201         else if (this.pendingOperations & AddonManager.PENDING_DISABLE)
202             pending = ["Disabled", "disabled"];
203         else if (this.pendingOperations & AddonManager.PENDING_INSTALL)
204             pending = ["Enabled", "installed"];
205         else if (this.pendingOperations & AddonManager.PENDING_ENABLE)
206             pending = ["Enabled", "enabled"];
207         else if (this.pendingOperations & AddonManager.PENDING_UPGRADE)
208             pending = ["Enabled", "upgraded"];
209         if (pending)
210             return <>{info}&#xa0;(<span highlight={pending[0]}>{pending[1]}</span>
211                                   &#xa0;on <a href="#" dactyl:command="dactyl.restart" xmlns:dactyl={NS}>restart</a>)</>;
212         return info;
213     },
214
215     update: function callee() {
216         let self = this;
217         function update(key, xml) {
218             let node = self.nodes[key];
219             while (node.firstChild)
220                 node.removeChild(node.firstChild);
221             node.appendChild(util.xmlToDom(<>{xml}</>, self.list.document));
222         }
223
224         update("name", template.icon({ icon: this.iconURL }, this.name));
225         this.nodes.version.textContent = this.version;
226         update("status", this.statusInfo);
227         this.nodes.description.textContent = this.description;
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, this.closure(function (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.closure.addAddon);
277         this.ready = true;
278         this.update();
279     },
280
281     message: Class.memoize(function () {
282
283         XML.ignoreWhitespace = true;
284         util.xmlToDom(<table highlight="Addons" key="list" xmlns={XHTML}>
285                         <tr highlight="AddonHead">
286                             <td>{_("title.Name")}</td>
287                             <td>{_("title.Version")}</td>
288                             <td>{_("title.Status")}</td>
289                             <td/>
290                             <td>{_("title.Description")}</td>
291                         </tr>
292                       </table>, this.document, this.nodes);
293
294         if (this._addons)
295             this._init();
296
297         return this.nodes.list;
298     }),
299
300     addAddon: function addAddon(addon) {
301         if (addon.id in this.addons)
302             this.update(addon);
303         else {
304             if (this.filter && addon.name.toLowerCase().indexOf(this.filter) === -1)
305                 return;
306
307             addon = Addon(addon, this);
308             this.addons[addon.id] = addon;
309
310             let index = values(this.addons).sort(function (a, b) a.compare(b))
311                                            .indexOf(addon);
312
313             this.nodes.list.insertBefore(addon.nodes.row,
314                                          this.nodes.list.childNodes[index + 1]);
315             this.update();
316         }
317     },
318     removeAddon: function removeAddon(addon) {
319         if (addon.id in this.addons) {
320             this.nodes.list.removeChild(this.addons[addon.id].nodes.row);
321             delete this.addons[addon.id];
322             this.update();
323         }
324     },
325
326     leave: function leave(stack) {
327         if (stack.pop)
328             this.cleanup();
329     },
330
331     update: function update(addon) {
332         if (addon && addon.id in this.addons)
333             this.addons[addon.id].update();
334         if (this.ready)
335             this.modules.mow.resize(false);
336     },
337
338     onDisabled:           function (addon) { this.update(addon); },
339     onDisabling:          function (addon) { this.update(addon); },
340     onEnabled:            function (addon) { this.update(addon); },
341     onEnabling:           function (addon) { this.update(addon); },
342     onInstalled:          function (addon) { this.addAddon(addon); },
343     onInstalling:         function (addon) { this.update(addon); },
344     onUninstalled:        function (addon) { this.removeAddon(addon); },
345     onUninstalling:       function (addon) { this.update(addon); },
346     onOperationCancelled: function (addon) { this.update(addon); },
347     onPropertyChanged: function onPropertyChanged(addon, properties) {}
348 });
349
350 var Addons = Module("addons", {
351     errors: Class.memoize(function ()
352             array(["ERROR_NETWORK_FAILURE", "ERROR_INCORRECT_HASH",
353                    "ERROR_CORRUPT_FILE", "ERROR_FILE_ACCESS"])
354                 .map(function (e) [AddonManager[e], _("AddonManager." + e)])
355                 .toObject())
356 }, {
357 }, {
358     commands: function (dactyl, modules, window) {
359         const { CommandOption, commands, completion } = modules;
360
361         commands.add(["addo[ns]", "ao"],
362             "List installed extensions",
363             function (args) {
364                 let addons = AddonList(modules, args["-types"], args[0]);
365                 modules.commandline.echo(addons);
366
367                 if (modules.commandline.savingOutput)
368                     util.waitFor(function () addons.ready);
369             },
370             {
371                 argCount: "?",
372                 options: [
373                     {
374                         names: ["-types", "-type", "-t"],
375                         description: "The add-on types to list",
376                         default: ["extension"],
377                         completer: function (context, args) completion.addonType(context),
378                         type: CommandOption.LIST
379                     }
380                 ]
381             });
382
383         let addonListener = AddonListener(modules);
384
385         commands.add(["exta[dd]"],
386             "Install an extension",
387             function (args) {
388                 let url  = args[0];
389                 let file = io.File(url);
390                 function install(addonInstall) {
391                     addonInstall.addListener(addonListener);
392                     addonInstall.install();
393                 }
394
395                 if (!file.exists())
396                     AddonManager.getInstallForURL(url,   install, "application/x-xpinstall");
397                 else if (file.isReadable() && file.isFile())
398                     AddonManager.getInstallForFile(file, install, "application/x-xpinstall");
399                 else if (file.isDirectory())
400                     dactyl.echoerr(_("addon.cantInstallDir", file.path.quote()));
401                 else
402                     dactyl.echoerr(_("io.notReadable", file.path));
403             }, {
404                 argCount: "1",
405                 completer: function (context) {
406                     context.filters.push(function ({ isdir, text }) isdir || /\.xpi$/.test(text));
407                     completion.file(context);
408                 },
409                 literal: 0
410             });
411
412         // TODO: handle extension dependencies
413         values(actions).forEach(function (command) {
414             let perm = command.perm && AddonManager["PERM_CAN_" + command.perm.toUpperCase()];
415             function ok(addon) !perm || addon.permissions & perm;
416
417             commands.add(Array.concat(command.name),
418                 command.description,
419                 function (args) {
420                     let name = args[0];
421                     if (args.bang && !command.bang)
422                         dactyl.assert(!name, _("error.trailingCharacters"));
423                     else
424                         dactyl.assert(name, _("error.argumentRequired"));
425
426                     AddonManager.getAddonsByTypes(args["-types"], dactyl.wrapCallback(function (list) {
427                         if (!args.bang || command.bang) {
428                             list = list.filter(function (extension) extension.name == name);
429                             if (list.length == 0)
430                                 return void dactyl.echoerr(_("error.invalidArgument", name));
431                             if (!list.every(ok))
432                                 return void dactyl.echoerr(_("error.invalidOperation"));
433                         }
434                         if (command.actions)
435                             command.actions(list, this.modules);
436                         else
437                             list.forEach(function (addon) command.action.call(this.modules, addon, args.bang), this);
438                     }));
439                 }, {
440                     argCount: "?", // FIXME: should be "1"
441                     bang: true,
442                     completer: function (context, args) {
443                         completion.extension(context, args["-types"]);
444                         context.filters.push(function ({ item }) ok(item));
445                         if (command.filter)
446                             context.filters.push(command.filter);
447                     },
448                     literal: 0,
449                     options: [
450                         {
451                             names: ["-types", "-type", "-t"],
452                             description: "The add-on types to operate on",
453                             default: ["extension"],
454                             completer: function (context, args) completion.addonType(context),
455                             type: CommandOption.LIST
456                         }
457                     ]
458                 });
459         });
460     },
461     completion: function (dactyl, modules, window) {
462         completion.addonType = function addonType(context) {
463             let base = ["extension", "theme"];
464             function update(types) {
465                 context.completions = types.map(function (t) [t, util.capitalize(t)]);
466             }
467
468             context.generate = function generate() {
469                 update(base);
470                 if (AddonManager.getAllAddons) {
471                     context.incomplete = true;
472                     AddonManager.getAllAddons(function (addons) {
473                         context.incomplete = false;
474                         update(array.uniq(base.concat(addons.map(function (a) a.type)),
475                                           true));
476                     });
477                 }
478             };
479         };
480
481         completion.extension = function extension(context, types) {
482             context.title = ["Extension"];
483             context.anchored = false;
484             context.keys = { text: "name", description: "description", icon: "iconURL" },
485             context.generate = function () {
486                 context.incomplete = true;
487                 AddonManager.getAddonsByTypes(types || ["extension"], function (addons) {
488                     context.incomplete = false;
489                     context.completions = addons;
490                 });
491             };
492         };
493     }
494 });
495
496 if (!services.has("extensionManager"))
497     Components.utils.import("resource://gre/modules/AddonManager.jsm");
498 else
499     var AddonManager = {
500         PERM_CAN_UNINSTALL: 1,
501         PERM_CAN_ENABLE: 2,
502         PERM_CAN_DISABLE: 4,
503         PERM_CAN_UPGRADE: 8,
504
505         getAddonByID: function (id, callback) {
506             callback = callback || util.identity;
507             addon = services.extensionManager.getItemForID(id);
508             if (addon)
509                 addon = this.wrapAddon(addon);
510             return callback(addon);
511         },
512
513         wrapAddon: function wrapAddon(addon) {
514             addon = Object.create(addon.QueryInterface(Ci.nsIUpdateItem));
515
516             ["aboutURL", "creator", "description", "developers",
517              "homepageURL", "installDate", "optionsURL",
518              "releaseNotesURI", "updateDate"].forEach(function (item) {
519                 memoize(addon, item, function (item) this.getProperty(item));
520             });
521
522             update(addon, {
523
524                 get permissions() 1 | (this.userDisabled ? 2 : 4),
525
526                 appDisabled: false,
527
528                 getProperty: function getProperty(property) {
529                     let resource = services.rdf.GetResource("urn:mozilla:item:" + this.id);
530
531                     if (resource) {
532                         let target = services.extensionManager.datasource.GetTarget(resource,
533                             services.rdf.GetResource("http://www.mozilla.org/2004/em-rdf#" + property), true);
534
535                         if (target && target instanceof Ci.nsIRDFLiteral)
536                             return target.Value;
537                     }
538
539                     return "";
540                 },
541
542                 installLocation: Class.memoize(function () services.extensionManager.getInstallLocation(this.id)),
543                 getResourceURI: function getResourceURI(path) {
544                     let file = this.installLocation.getItemFile(this.id, path);
545                     return services.io.newFileURI(file);
546                 },
547
548                 get isActive() this.getProperty("isDisabled") != "true",
549
550                 uninstall: function uninstall() {
551                     services.extensionManager.uninstallItem(this.id);
552                 },
553
554                 get userDisabled() this.getProperty("userDisabled") === "true",
555                 set userDisabled(val) {
556                     services.extensionManager[val ? "disableItem" : "enableItem"](this.id);
557                 }
558             });
559
560             return addon;
561         },
562
563         getAddonsByTypes: function (types, callback) {
564             let res = [];
565             for (let [, type] in Iterator(types))
566                 for (let [, item] in Iterator(services.extensionManager
567                             .getItemList(Ci.nsIUpdateItem["TYPE_" + type.toUpperCase()], {})))
568                     res.push(this.wrapAddon(item));
569
570             if (callback)
571                 util.timeout(function () { callback(res); });
572             return res;
573         },
574
575         getInstallForFile: function (file, callback, mimetype) {
576             callback({
577                 addListener: function () {},
578                 install: function () {
579                     services.extensionManager.installItemFromFile(file, "app-profile");
580                 }
581             });
582         },
583
584         getInstallForURL: function (url, callback, mimetype) {
585             util.assert(false, _("error.unavailable", config.host, services.runtime.version));
586         },
587
588         observers: [],
589         addAddonListener: function (listener) {
590             observer.listener = listener;
591             function observer(subject, topic, data) {
592                 if (subject instanceof Ci.nsIUpdateItem)
593                     subject = AddonManager.wrapAddon(subject);
594
595                 if (data === "item-installed")
596                     listener.onInstalling(subject, true);
597                 else if (data === "item-uninstalled")
598                     listener.onUnistalling(subject, true);
599                 else if (data === "item-upgraded")
600                     listener.onInstalling(subject, true);
601                 else if (data === "item-enabled")
602                     listener.onEnabling(subject, true);
603                 else if (data === "item-disabled")
604                     listener.onDisabling(subject, true);
605             }
606             services.observer.addObserver(observer, "em-action-requested", false);
607             this.observers.push(observer);
608         },
609         removeAddonListener: function (listener) {
610             this.observers = this.observers.filter(function (observer) {
611                 if (observer.listener !== listener)
612                     return true;
613                 services.observer.removeObserver(observer, "em-action-requested");
614             });
615         }
616     };
617
618 endModule();
619
620 } catch(e){ if (isString(e)) e = Error(e); dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack); }
621
622 // vim: set fdm=marker sw=4 ts=4 et ft=javascript: