]> git.donarmstrong.com Git - dactyl.git/blob - common/modules/addons.jsm
Import 1.0 supporting Firefox up to 14.*
[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 (addon) addon.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 (addon) !addon.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 (addon) addon.isActive && addon.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 (addon) !addon.userDisabled &&
121                 !(addon.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(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, io } = 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) && (!command.filter || command.filter(addon));
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                         dactyl.assert(list.every(ok));
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.addon(context, args["-types"]);
444                         context.filters.push(function ({ item }) ok(item));
445                     },
446                     literal: 0,
447                     options: [
448                         {
449                             names: ["-types", "-type", "-t"],
450                             description: "The add-on types to operate on",
451                             default: ["extension"],
452                             completer: function (context, args) completion.addonType(context),
453                             type: CommandOption.LIST
454                         }
455                     ]
456                 });
457         });
458     },
459     completion: function (dactyl, modules, window) {
460         completion.addonType = function addonType(context) {
461             let base = ["extension", "theme"];
462             function update(types) {
463                 context.completions = types.map(function (t) [t, util.capitalize(t)]);
464             }
465
466             context.generate = function generate() {
467                 update(base);
468                 if (AddonManager.getAllAddons) {
469                     context.incomplete = true;
470                     AddonManager.getAllAddons(function (addons) {
471                         context.incomplete = false;
472                         update(array.uniq(base.concat(addons.map(function (a) a.type)),
473                                           true));
474                     });
475                 }
476             };
477         };
478
479         completion.addon = function addon(context, types) {
480             context.title = ["Add-on"];
481             context.anchored = false;
482             context.keys = {
483                 text: function (addon) [addon.name, addon.id],
484                 description: "description",
485                 icon: "iconURL"
486             };
487             context.generate = function () {
488                 context.incomplete = true;
489                 AddonManager.getAddonsByTypes(types || ["extension"], function (addons) {
490                     context.incomplete = false;
491                     context.completions = addons;
492                 });
493             };
494         };
495     }
496 });
497
498 if (!services.has("extensionManager"))
499     Components.utils.import("resource://gre/modules/AddonManager.jsm");
500 else
501     var AddonManager = {
502         PERM_CAN_UNINSTALL: 1,
503         PERM_CAN_ENABLE: 2,
504         PERM_CAN_DISABLE: 4,
505         PERM_CAN_UPGRADE: 8,
506
507         getAddonByID: function (id, callback) {
508             callback = callback || util.identity;
509             addon = services.extensionManager.getItemForID(id);
510             if (addon)
511                 addon = this.wrapAddon(addon);
512             return callback(addon);
513         },
514
515         wrapAddon: function wrapAddon(addon) {
516             addon = Object.create(addon.QueryInterface(Ci.nsIUpdateItem));
517
518             ["aboutURL", "creator", "description", "developers",
519              "homepageURL", "installDate", "optionsURL",
520              "releaseNotesURI", "updateDate"].forEach(function (item) {
521                 memoize(addon, item, function (item) this.getProperty(item));
522             });
523
524             update(addon, {
525
526                 get permissions() 1 | (this.userDisabled ? 2 : 4),
527
528                 appDisabled: false,
529
530                 getProperty: function getProperty(property) {
531                     let resource = services.rdf.GetResource("urn:mozilla:item:" + this.id);
532
533                     if (resource) {
534                         let target = services.extensionManager.datasource.GetTarget(resource,
535                             services.rdf.GetResource("http://www.mozilla.org/2004/em-rdf#" + property), true);
536
537                         if (target && target instanceof Ci.nsIRDFLiteral)
538                             return target.Value;
539                     }
540
541                     return "";
542                 },
543
544                 installLocation: Class.Memoize(function () services.extensionManager.getInstallLocation(this.id)),
545                 getResourceURI: function getResourceURI(path) {
546                     let file = this.installLocation.getItemFile(this.id, path);
547                     return services.io.newFileURI(file);
548                 },
549
550                 get isActive() this.getProperty("isDisabled") != "true",
551
552                 uninstall: function uninstall() {
553                     services.extensionManager.uninstallItem(this.id);
554                 },
555
556                 get userDisabled() this.getProperty("userDisabled") === "true",
557                 set userDisabled(val) {
558                     services.extensionManager[val ? "disableItem" : "enableItem"](this.id);
559                 }
560             });
561
562             return addon;
563         },
564
565         getAddonsByTypes: function (types, callback) {
566             let res = [];
567             for (let [, type] in Iterator(types))
568                 for (let [, item] in Iterator(services.extensionManager
569                             .getItemList(Ci.nsIUpdateItem["TYPE_" + type.toUpperCase()], {})))
570                     res.push(this.wrapAddon(item));
571
572             if (callback)
573                 util.timeout(function () { callback(res); });
574             return res;
575         },
576
577         getInstallForFile: function (file, callback, mimetype) {
578             callback({
579                 addListener: function () {},
580                 install: function () {
581                     services.extensionManager.installItemFromFile(file, "app-profile");
582                 }
583             });
584         },
585
586         getInstallForURL: function (url, callback, mimetype) {
587             util.assert(false, _("error.unavailable", config.host, services.runtime.version));
588         },
589
590         observers: [],
591         addAddonListener: function (listener) {
592             observer.listener = listener;
593             function observer(subject, topic, data) {
594                 if (subject instanceof Ci.nsIUpdateItem)
595                     subject = AddonManager.wrapAddon(subject);
596
597                 if (data === "item-installed")
598                     listener.onInstalling(subject, true);
599                 else if (data === "item-uninstalled")
600                     listener.onUnistalling(subject, true);
601                 else if (data === "item-upgraded")
602                     listener.onInstalling(subject, true);
603                 else if (data === "item-enabled")
604                     listener.onEnabling(subject, true);
605                 else if (data === "item-disabled")
606                     listener.onDisabling(subject, true);
607             }
608             services.observer.addObserver(observer, "em-action-requested", false);
609             this.observers.push(observer);
610         },
611         removeAddonListener: function (listener) {
612             this.observers = this.observers.filter(function (observer) {
613                 if (observer.listener !== listener)
614                     return true;
615                 services.observer.removeObserver(observer, "em-action-requested");
616             });
617         }
618     };
619
620 endModule();
621
622 } catch(e){ if (isString(e)) e = Error(e); dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack); }
623
624 // vim: set fdm=marker sw=4 ts=4 et ft=javascript: