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"],
16 var callResult = function callResult(method) {
17 let args = Array.slice(arguments, 1);
18 return function (result) { result[method].apply(result, args); };
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] : "")));
28 var AddonListener = Class("AddonListener", {
29 init: function init(modules) {
30 this.dactyl = modules.dactyl;
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")
46 var updateAddons = Class("UpgradeListener", AddonListener, {
47 init: function init(addons, modules) {
48 init.supercall(this, modules);
50 util.assert(!addons.length || addons[0].findUpdates,
51 _("error.unavailable", config.host, services.runtime.version));
53 this.remaining = addons;
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);
60 onUpdateAvailable: function (addon, install) {
61 this.upgrade.push(addon);
62 install.addListener(this);
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)
70 ? _("addon.installingUpdates", this.upgrade.map(function (i) i.name).join(", "))
71 : _("addon.noUpdates"));
77 name: ["extde[lete]", "extrm"],
78 description: "Uninstall an extension",
79 action: callResult("uninstall"),
84 description: "Enable an extension",
85 action: function (addon) { addon.userDisabled = false; },
86 filter: function (addon) addon.userDisabled,
91 description: "Disable an extension",
92 action: function (addon) { addon.userDisabled = true; },
93 filter: function (addon) !addon.userDisabled,
97 name: ["exto[ptions]", "extp[references]"],
98 description: "Open an extension's preference dialog",
100 action: function (addon, bang) {
102 this.window.openDialog(addon.optionsURL, "_blank", "chrome");
104 this.dactyl.open(addon.optionsURL, { from: "extoptions" });
106 filter: function (addon) addon.isActive && addon.optionsURL
110 description: "Reload an extension",
111 action: function (addon) {
112 util.assert(config.haveGecko("2b"), _("command.notUseful", config.host));
114 util.timeout(function () {
115 addon.userDisabled = true;
116 addon.userDisabled = false;
120 return function (addon) !addon.userDisabled &&
121 !(addon.operationsRequiringRestart & (AddonManager.OP_NEEDS_RESTART_ENABLE | AddonManager.OP_NEEDS_RESTART_DISABLE))
127 description: "Toggle an extension's enabled status",
128 action: function (addon) { addon.userDisabled = !addon.userDisabled; }
132 description: "Update an extension",
133 actions: updateAddons,
138 var Addon = Class("Addon", {
139 init: function init(addon, list) {
141 this.instance = this;
147 XML.ignoreWhitespace = true;
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>
159 <td highlight="AddonStatus" key="status"/>
160 <td highlight="AddonDescription" key="description"/>
162 this.list.document, this.nodes);
167 commandAllowed: function commandAllowed(cmd) {
168 util.assert(Set.has(actions, cmd), _("addon.unknownCommand"));
170 let action = actions[cmd];
171 if ("perm" in action && !(this.permissions & AddonManager["PERM_CAN_" + action.perm.toUpperCase()]))
173 if ("filter" in action && !action.filter(this))
178 command: function command(cmd) {
179 util.assert(this.commandAllowed(cmd), _("addon.commandNotAllowed"));
181 let action = actions[cmd];
183 action.action.call(this.list.modules, this, true);
185 action.actions([this], this.list.modules);
188 compare: function compare(other) String.localeCompare(this.name, other.name),
191 XML.ignoreWhitespace = XML.prettyPrinting = false;
192 default xml namespace = XHTML;
194 let info = this.isActive ? <span highlight="Enabled">enabled</span>
195 : <span highlight="Disabled">disabled</span>;
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"];
209 return <>{info} (<span highlight={pending[0]}>{pending[1]}</span>
210  on <a href="#" dactyl:command="dactyl.restart" xmlns:dactyl={NS}>restart</a>)</>;
214 update: function callee() {
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));
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);
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>
289 <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, io } = 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) && (!command.filter || command.filter(addon));
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 (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);
433 dactyl.assert(list.every(ok));
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.addon(context, args["-types"]);
444 context.filters.push(function ({ item }) ok(item));
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
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)]);
466 context.generate = function generate() {
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)),
479 completion.addon = function addon(context, types) {
480 context.title = ["Add-on"];
481 context.anchored = false;
483 text: function (addon) [addon.name, addon.id],
484 description: "description",
487 context.generate = function () {
488 context.incomplete = true;
489 AddonManager.getAddonsByTypes(types || ["extension"], function (addons) {
490 context.incomplete = false;
491 context.completions = addons;
498 if (!services.has("extensionManager"))
499 Components.utils.import("resource://gre/modules/AddonManager.jsm");
502 PERM_CAN_UNINSTALL: 1,
507 getAddonByID: function (id, callback) {
508 callback = callback || util.identity;
509 addon = services.extensionManager.getItemForID(id);
511 addon = this.wrapAddon(addon);
512 return callback(addon);
515 wrapAddon: function wrapAddon(addon) {
516 addon = Object.create(addon.QueryInterface(Ci.nsIUpdateItem));
518 ["aboutURL", "creator", "description", "developers",
519 "homepageURL", "installDate", "optionsURL",
520 "releaseNotesURI", "updateDate"].forEach(function (item) {
521 memoize(addon, item, function (item) this.getProperty(item));
526 get permissions() 1 | (this.userDisabled ? 2 : 4),
530 getProperty: function getProperty(property) {
531 let resource = services.rdf.GetResource("urn:mozilla:item:" + this.id);
534 let target = services.extensionManager.datasource.GetTarget(resource,
535 services.rdf.GetResource("http://www.mozilla.org/2004/em-rdf#" + property), true);
537 if (target && target instanceof Ci.nsIRDFLiteral)
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);
550 get isActive() this.getProperty("isDisabled") != "true",
552 uninstall: function uninstall() {
553 services.extensionManager.uninstallItem(this.id);
556 get userDisabled() this.getProperty("userDisabled") === "true",
557 set userDisabled(val) {
558 services.extensionManager[val ? "disableItem" : "enableItem"](this.id);
565 getAddonsByTypes: function (types, callback) {
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));
573 util.timeout(function () { callback(res); });
577 getInstallForFile: function (file, callback, mimetype) {
579 addListener: function () {},
580 install: function () {
581 services.extensionManager.installItemFromFile(file, "app-profile");
586 getInstallForURL: function (url, callback, mimetype) {
587 util.assert(false, _("error.unavailable", config.host, services.runtime.version));
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);
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);
608 services.observer.addObserver(observer, "em-action-requested", false);
609 this.observers.push(observer);
611 removeAddonListener: function (listener) {
612 this.observers = this.observers.filter(function (observer) {
613 if (observer.listener !== listener)
615 services.observer.removeObserver(observer, "em-action-requested");
622 } catch(e){ if (isString(e)) e = Error(e); dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack); }
624 // vim: set fdm=marker sw=4 ts=4 et ft=javascript: