]> git.donarmstrong.com Git - dactyl.git/blob - common/modules/addons.jsm
Import 1.0rc1 supporting Firefox up to 11.*
[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 }, this);
15
16 var callResult = function callResult(method) {
17     let args = Array.slice(arguments, 1);
18     return function (result) { result[method].apply(result, args); };
19 }
20
21 var listener = function listener(action, event)
22     function addonListener(install) {
23         this.dactyl[install.error ? "echoerr" : "echomsg"](
24             _("addon.error", action, event, (install.name || install.sourceURI.spec) +
25                 (install.error ? ": " + addons.errors[install.error] : "")));
26     }
27
28 var AddonListener = Class("AddonListener", {
29     init: function init(modules) {
30         this.dactyl = modules.dactyl;
31     },
32
33     onNewInstall:        function (install) {},
34     onExternalInstall:   function (addon, existingAddon, needsRestart) {},
35     onDownloadStarted:   listener("download", "started"),
36     onDownloadEnded:     listener("download", "complete"),
37     onDownloadCancelled: listener("download", "canceled"),
38     onDownloadFailed:    listener("download", "failed"),
39     onDownloadProgress:  function (install) {},
40     onInstallStarted:    function (install) {},
41     onInstallEnded:      listener("installation", "complete"),
42     onInstallCancelled:  listener("installation", "canceled"),
43     onInstallFailed:     listener("installation", "failed")
44 });
45
46 var updateAddons = Class("UpgradeListener", AddonListener, {
47     init: function init(addons, modules) {
48         init.supercall(this, modules);
49
50         util.assert(!addons.length || addons[0].findUpdates,
51                     _("error.unavailable", config.host, services.runtime.version));
52
53         this.remaining = addons;
54         this.upgrade = [];
55         this.dactyl.echomsg(_("addon.check", addons.map(function (a) a.name).join(", ")));
56         for (let addon in values(addons))
57             addon.findUpdates(this, AddonManager.UPDATE_WHEN_USER_REQUESTED, null, null);
58
59     },
60     onUpdateAvailable: function (addon, install) {
61         this.upgrade.push(addon);
62         install.addListener(this);
63         install.install();
64     },
65     onUpdateFinished: function (addon, error) {
66         this.remaining = this.remaining.filter(function (a) a.type != addon.type || a.id != addon.id);
67         if (!this.remaining.length)
68             this.dactyl.echomsg(
69                 this.upgrade.length
70                     ? _("addon.installingUpdates", this.upgrade.map(function (i) i.name).join(", "))
71                     : _("addon.noUpdates"));
72     }
73 });
74
75 var actions = {
76     delete: {
77         name: ["extde[lete]", "extrm"],
78         description: "Uninstall an extension",
79         action: callResult("uninstall"),
80         perm: "uninstall"
81     },
82     enable: {
83         name: "exte[nable]",
84         description: "Enable an extension",
85         action: function (addon) { addon.userDisabled = false; },
86         filter: function ({ item }) item.userDisabled,
87         perm: "enable"
88     },
89     disable: {
90         name: "extd[isable]",
91         description: "Disable an extension",
92         action: function (addon) { addon.userDisabled = true; },
93         filter: function ({ item }) !item.userDisabled,
94         perm: "disable"
95     },
96     options: {
97         name: ["exto[ptions]", "extp[references]"],
98         description: "Open an extension's preference dialog",
99         bang: true,
100         action: function (addon, bang) {
101             if (bang)
102                 this.window.openDialog(addon.optionsURL, "_blank", "chrome");
103             else
104                 this.dactyl.open(addon.optionsURL, { from: "extoptions" });
105         },
106         filter: function ({ item }) item.isActive && item.optionsURL
107     },
108     rehash: {
109         name: "extr[ehash]",
110         description: "Reload an extension",
111         action: function (addon) {
112             util.assert(config.haveGecko("2b"), _("command.notUseful", config.host));
113             util.flushCache();
114             util.timeout(function () {
115                 addon.userDisabled = true;
116                 addon.userDisabled = false;
117             });
118         },
119         get filter() {
120             return function ({ item }) !item.userDisabled &&
121                 !(item.operationsRequiringRestart & (AddonManager.OP_NEEDS_RESTART_ENABLE | AddonManager.OP_NEEDS_RESTART_DISABLE))
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="AddonButtons Buttons">
153                     <a highlight="Button" href="javascript:0" key="enable">{_("addon.action.On")}</a>
154                     <a highlight="Button" href="javascript:0" key="disable">{_("addon.action.Off")}</a>
155                     <a highlight="Button" href="javascript:0" key="delete">{_("addon.action.Delete")}</a>
156                     <a highlight="Button" href="javascript:0" key="update">{_("addon.action.Update")}</a>
157                     <a highlight="Button" href="javascript:0" key="options">{_("addon.action.Options")}</a>
158                 </td>
159                 <td highlight="AddonStatus" key="status"/>
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), _("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({ item: 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         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         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, 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/>
289                             <td>{_("title.Status")}</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 (addon) addon.id == name || addon.name == name);
429                             dactyl.assert(list.length, _("error.invalidArgument", name));
430                             dactyl.assert(list.some(ok), _("error.invalidOperation"));
431                             list = list.filter(ok);
432                         }
433                         if (command.actions)
434                             command.actions(list, this.modules);
435                         else
436                             list.forEach(function (addon) command.action.call(this.modules, addon, args.bang), this);
437                     }));
438                 }, {
439                     argCount: "?", // FIXME: should be "1"
440                     bang: true,
441                     completer: function (context, args) {
442                         completion.addon(context, args["-types"]);
443                         context.filters.push(function ({ item }) ok(item));
444                         if (command.filter)
445                             context.filters.push(command.filter);
446                     },
447                     literal: 0,
448                     options: [
449                         {
450                             names: ["-types", "-type", "-t"],
451                             description: "The add-on types to operate on",
452                             default: ["extension"],
453                             completer: function (context, args) completion.addonType(context),
454                             type: CommandOption.LIST
455                         }
456                     ]
457                 });
458         });
459     },
460     completion: function (dactyl, modules, window) {
461         completion.addonType = function addonType(context) {
462             let base = ["extension", "theme"];
463             function update(types) {
464                 context.completions = types.map(function (t) [t, util.capitalize(t)]);
465             }
466
467             context.generate = function generate() {
468                 update(base);
469                 if (AddonManager.getAllAddons) {
470                     context.incomplete = true;
471                     AddonManager.getAllAddons(function (addons) {
472                         context.incomplete = false;
473                         update(array.uniq(base.concat(addons.map(function (a) a.type)),
474                                           true));
475                     });
476                 }
477             };
478         };
479
480         completion.addon = function addon(context, types) {
481             context.title = ["Add-on"];
482             context.anchored = false;
483             context.keys = {
484                 text: function (addon) [addon.name, addon.id],
485                 description: "description",
486                 icon: "iconURL"
487             };
488             context.generate = function () {
489                 context.incomplete = true;
490                 AddonManager.getAddonsByTypes(types || ["extension"], function (addons) {
491                     context.incomplete = false;
492                     context.completions = addons;
493                 });
494             };
495         };
496     }
497 });
498
499 if (!services.has("extensionManager"))
500     Components.utils.import("resource://gre/modules/AddonManager.jsm");
501 else
502     var AddonManager = {
503         PERM_CAN_UNINSTALL: 1,
504         PERM_CAN_ENABLE: 2,
505         PERM_CAN_DISABLE: 4,
506         PERM_CAN_UPGRADE: 8,
507
508         getAddonByID: function (id, callback) {
509             callback = callback || util.identity;
510             addon = services.extensionManager.getItemForID(id);
511             if (addon)
512                 addon = this.wrapAddon(addon);
513             return callback(addon);
514         },
515
516         wrapAddon: function wrapAddon(addon) {
517             addon = Object.create(addon.QueryInterface(Ci.nsIUpdateItem));
518
519             ["aboutURL", "creator", "description", "developers",
520              "homepageURL", "installDate", "optionsURL",
521              "releaseNotesURI", "updateDate"].forEach(function (item) {
522                 memoize(addon, item, function (item) this.getProperty(item));
523             });
524
525             update(addon, {
526
527                 get permissions() 1 | (this.userDisabled ? 2 : 4),
528
529                 appDisabled: false,
530
531                 getProperty: function getProperty(property) {
532                     let resource = services.rdf.GetResource("urn:mozilla:item:" + this.id);
533
534                     if (resource) {
535                         let target = services.extensionManager.datasource.GetTarget(resource,
536                             services.rdf.GetResource("http://www.mozilla.org/2004/em-rdf#" + property), true);
537
538                         if (target && target instanceof Ci.nsIRDFLiteral)
539                             return target.Value;
540                     }
541
542                     return "";
543                 },
544
545                 installLocation: Class.Memoize(function () services.extensionManager.getInstallLocation(this.id)),
546                 getResourceURI: function getResourceURI(path) {
547                     let file = this.installLocation.getItemFile(this.id, path);
548                     return services.io.newFileURI(file);
549                 },
550
551                 get isActive() this.getProperty("isDisabled") != "true",
552
553                 uninstall: function uninstall() {
554                     services.extensionManager.uninstallItem(this.id);
555                 },
556
557                 get userDisabled() this.getProperty("userDisabled") === "true",
558                 set userDisabled(val) {
559                     services.extensionManager[val ? "disableItem" : "enableItem"](this.id);
560                 }
561             });
562
563             return addon;
564         },
565
566         getAddonsByTypes: function (types, callback) {
567             let res = [];
568             for (let [, type] in Iterator(types))
569                 for (let [, item] in Iterator(services.extensionManager
570                             .getItemList(Ci.nsIUpdateItem["TYPE_" + type.toUpperCase()], {})))
571                     res.push(this.wrapAddon(item));
572
573             if (callback)
574                 util.timeout(function () { callback(res); });
575             return res;
576         },
577
578         getInstallForFile: function (file, callback, mimetype) {
579             callback({
580                 addListener: function () {},
581                 install: function () {
582                     services.extensionManager.installItemFromFile(file, "app-profile");
583                 }
584             });
585         },
586
587         getInstallForURL: function (url, callback, mimetype) {
588             util.assert(false, _("error.unavailable", config.host, services.runtime.version));
589         },
590
591         observers: [],
592         addAddonListener: function (listener) {
593             observer.listener = listener;
594             function observer(subject, topic, data) {
595                 if (subject instanceof Ci.nsIUpdateItem)
596                     subject = AddonManager.wrapAddon(subject);
597
598                 if (data === "item-installed")
599                     listener.onInstalling(subject, true);
600                 else if (data === "item-uninstalled")
601                     listener.onUnistalling(subject, true);
602                 else if (data === "item-upgraded")
603                     listener.onInstalling(subject, true);
604                 else if (data === "item-enabled")
605                     listener.onEnabling(subject, true);
606                 else if (data === "item-disabled")
607                     listener.onDisabling(subject, true);
608             }
609             services.observer.addObserver(observer, "em-action-requested", false);
610             this.observers.push(observer);
611         },
612         removeAddonListener: function (listener) {
613             this.observers = this.observers.filter(function (observer) {
614                 if (observer.listener !== listener)
615                     return true;
616                 services.observer.removeObserver(observer, "em-action-requested");
617             });
618         }
619     };
620
621 endModule();
622
623 } catch(e){ if (isString(e)) e = Error(e); dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack); }
624
625 // vim: set fdm=marker sw=4 ts=4 et ft=javascript: