1 // Copyright (c) 2009-2011 by Kris Maglione <maglione.k@gmail.com>
2 // Copyright (c) 2009-2010 by Doug Kearns <dougkearns@gmail.com>
4 // This work is licensed for reuse under an MIT license. Details are
5 // given in the LICENSE.txt file included with this file.
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"]
17 var callResult = function callResult(method) {
18 let args = Array.slice(arguments, 1);
19 return function (result) { result[method].apply(result, args); };
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] : "")));
29 var AddonListener = Class("AddonListener", {
30 init: function init(modules) {
31 this.dactyl = modules.dactyl;
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")
47 var updateAddons = Class("UpgradeListener", AddonListener, {
48 init: function init(addons, modules) {
49 init.supercall(this, modules);
51 util.assert(!addons.length || addons[0].findUpdates,
52 _("error.unavailable", config.host, services.runtime.version));
54 this.remaining = addons;
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);
61 onUpdateAvailable: function (addon, install) {
62 util.dump("onUpdateAvailable");
63 this.upgrade.push(addon);
64 install.addListener(this);
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)
72 ? _("addon.installingUpdates", this.upgrade.map(function (i) i.name).join(", "))
73 : _("addon.noUpdates"));
80 description: "Uninstall an extension",
81 action: callResult("uninstall"),
86 description: "Enable an extension",
87 action: function (addon) { addon.userDisabled = false; },
88 filter: function ({ item }) item.userDisabled,
93 description: "Disable an extension",
94 action: function (addon) { addon.userDisabled = true; },
95 filter: function ({ item }) !item.userDisabled,
99 name: ["exto[ptions]", "extp[references]"],
100 description: "Open an extension's preference dialog",
102 action: function (addon, bang) {
104 this.window.openDialog(addon.optionsURL, "_blank", "chrome");
106 this.dactyl.open(addon.optionsURL, { from: "extoptions" });
108 filter: function ({ item }) item.isActive && item.optionsURL
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;
121 let ids = Set(keys(JSON.parse(prefs.get("extensions.bootstrappedAddons", "{}"))));
122 return function ({ item }) !item.userDisabled && Set.has(ids, item.id);
128 description: "Toggle an extension's enabled status",
129 action: function (addon) { addon.userDisabled = !addon.userDisabled; }
133 description: "Update an extension",
134 actions: updateAddons,
139 var Addon = Class("Addon", {
140 init: function init(addon, list) {
142 this.instance = this;
148 XML.ignoreWhitespace = true;
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>
161 <td highlight="AddonDescription" key="description"/>
163 this.list.document, this.nodes);
168 commandAllowed: function commandAllowed(cmd) {
169 util.assert(Set.has(actions, cmd), _("addon.unknownCommand"));
171 let action = actions[cmd];
172 if ("perm" in action && !(this.permissions & AddonManager["PERM_CAN_" + action.perm.toUpperCase()]))
174 if ("filter" in action && !action.filter({ item: this }))
179 command: function command(cmd) {
180 util.assert(this.commandAllowed(cmd), _("addon.commandNotAllowed"));
182 let action = actions[cmd];
184 action.action.call(this.list.modules, this, true);
186 action.actions([this], this.list.modules);
189 compare: function compare(other) String.localeCompare(this.name, other.name),
192 XML.ignoreWhitespace = XML.prettyPrinting = false;
193 default xml namespace = XHTML;
195 let info = this.isActive ? <span highlight="Enabled">enabled</span>
196 : <span highlight="Disabled">disabled</span>;
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"];
210 return <>{info} (<span highlight={pending[0]}>{pending[1]}</span>
211  on <a href="#" dactyl:command="dactyl.restart" xmlns:dactyl={NS}>restart</a>)</>;
215 update: function callee() {
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));
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;
229 for (let node in values(this.nodes))
230 if (node.update && node.update !== callee)
233 let event = this.list.document.createEvent("Events");
234 event.initEvent("dactyl-commandupdate", true, false);
235 this.list.document.dispatchEvent(event);
239 ["cancelUninstall", "findUpdates", "getResourceURI", "hasResource", "isCompatibleWith",
240 "uninstall"].forEach(function (prop) {
241 Addon.prototype[prop] = function proxy() this.addon[prop].apply(this.addon, arguments);
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
256 var AddonList = Class("AddonList", {
257 init: function init(modules, types, filter) {
258 this.modules = modules;
259 this.filter = filter && filter.toLowerCase();
264 AddonManager.getAddonsByTypes(types, this.closure(function (addons) {
265 this._addons = addons;
269 AddonManager.addAddonListener(this);
271 cleanup: function cleanup() {
272 AddonManager.removeAddonListener(this);
275 _init: function _init() {
276 this._addons.forEach(this.closure.addAddon);
281 message: Class.memoize(function () {
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>
290 <td>{_("title.Description")}</td>
292 </table>, this.document, this.nodes);
297 return this.nodes.list;
300 addAddon: function addAddon(addon) {
301 if (addon.id in this.addons)
304 if (this.filter && addon.name.toLowerCase().indexOf(this.filter) === -1)
307 addon = Addon(addon, this);
308 this.addons[addon.id] = addon;
310 let index = values(this.addons).sort(function (a, b) a.compare(b))
313 this.nodes.list.insertBefore(addon.nodes.row,
314 this.nodes.list.childNodes[index + 1]);
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];
326 leave: function leave(stack) {
331 update: function update(addon) {
332 if (addon && addon.id in this.addons)
333 this.addons[addon.id].update();
335 this.modules.mow.resize(false);
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) {}
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)])
358 commands: function (dactyl, modules, window) {
359 const { CommandOption, commands, completion } = modules;
361 commands.add(["addo[ns]", "ao"],
362 "List installed extensions",
364 let addons = AddonList(modules, args["-types"], args[0]);
365 modules.commandline.echo(addons);
367 if (modules.commandline.savingOutput)
368 util.waitFor(function () addons.ready);
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
383 let addonListener = AddonListener(modules);
385 commands.add(["exta[dd]"],
386 "Install an extension",
389 let file = io.File(url);
390 function install(addonInstall) {
391 addonInstall.addListener(addonListener);
392 addonInstall.install();
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()));
402 dactyl.echoerr(_("io.notReadable", file.path));
405 completer: function (context) {
406 context.filters.push(function ({ isdir, text }) isdir || /\.xpi$/.test(text));
407 completion.file(context);
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;
417 commands.add(Array.concat(command.name),
421 if (args.bang && !command.bang)
422 dactyl.assert(!name, _("error.trailingCharacters"));
424 dactyl.assert(name, _("error.argumentRequired"));
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));
432 return void dactyl.echoerr(_("error.invalidOperation"));
435 command.actions(list, this.modules);
437 list.forEach(function (addon) command.action.call(this.modules, addon, args.bang), this);
440 argCount: "?", // FIXME: should be "1"
442 completer: function (context, args) {
443 completion.extension(context, args["-types"]);
444 context.filters.push(function ({ item }) ok(item));
446 context.filters.push(command.filter);
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
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)]);
468 context.generate = function generate() {
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)),
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;
496 if (!services.has("extensionManager"))
497 Components.utils.import("resource://gre/modules/AddonManager.jsm");
500 PERM_CAN_UNINSTALL: 1,
505 getAddonByID: function (id, callback) {
506 callback = callback || util.identity;
507 addon = services.extensionManager.getItemForID(id);
509 addon = this.wrapAddon(addon);
510 return callback(addon);
513 wrapAddon: function wrapAddon(addon) {
514 addon = Object.create(addon.QueryInterface(Ci.nsIUpdateItem));
516 ["aboutURL", "creator", "description", "developers",
517 "homepageURL", "installDate", "optionsURL",
518 "releaseNotesURI", "updateDate"].forEach(function (item) {
519 memoize(addon, item, function (item) this.getProperty(item));
524 get permissions() 1 | (this.userDisabled ? 2 : 4),
528 getProperty: function getProperty(property) {
529 let resource = services.rdf.GetResource("urn:mozilla:item:" + this.id);
532 let target = services.extensionManager.datasource.GetTarget(resource,
533 services.rdf.GetResource("http://www.mozilla.org/2004/em-rdf#" + property), true);
535 if (target && target instanceof Ci.nsIRDFLiteral)
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);
548 get isActive() this.getProperty("isDisabled") != "true",
550 uninstall: function uninstall() {
551 services.extensionManager.uninstallItem(this.id);
554 get userDisabled() this.getProperty("userDisabled") === "true",
555 set userDisabled(val) {
556 services.extensionManager[val ? "disableItem" : "enableItem"](this.id);
563 getAddonsByTypes: function (types, callback) {
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));
571 util.timeout(function () { callback(res); });
575 getInstallForFile: function (file, callback, mimetype) {
577 addListener: function () {},
578 install: function () {
579 services.extensionManager.installItemFromFile(file, "app-profile");
584 getInstallForURL: function (url, callback, mimetype) {
585 util.assert(false, _("error.unavailable", config.host, services.runtime.version));
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);
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);
606 services.observer.addObserver(observer, "em-action-requested", false);
607 this.observers.push(observer);
609 removeAddonListener: function (listener) {
610 this.observers = this.observers.filter(function (observer) {
611 if (observer.listener !== listener)
613 services.observer.removeObserver(observer, "em-action-requested");
620 } catch(e){ if (isString(e)) e = Error(e); dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack); }
622 // vim: set fdm=marker sw=4 ts=4 et ft=javascript: