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 ({ item }) item.userDisabled,
91 description: "Disable an extension",
92 action: function (addon) { addon.userDisabled = true; },
93 filter: function ({ item }) !item.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 ({ item }) item.isActive && item.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 ({ item }) !item.userDisabled &&
121 !(item.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({ item: 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 } = 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 (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);
434 command.actions(list, this.modules);
436 list.forEach(function (addon) command.action.call(this.modules, addon, args.bang), this);
439 argCount: "?", // FIXME: should be "1"
441 completer: function (context, args) {
442 completion.addon(context, args["-types"]);
443 context.filters.push(function ({ item }) ok(item));
445 context.filters.push(command.filter);
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
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)]);
467 context.generate = function generate() {
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)),
480 completion.addon = function addon(context, types) {
481 context.title = ["Add-on"];
482 context.anchored = false;
484 text: function (addon) [addon.name, addon.id],
485 description: "description",
488 context.generate = function () {
489 context.incomplete = true;
490 AddonManager.getAddonsByTypes(types || ["extension"], function (addons) {
491 context.incomplete = false;
492 context.completions = addons;
499 if (!services.has("extensionManager"))
500 Components.utils.import("resource://gre/modules/AddonManager.jsm");
503 PERM_CAN_UNINSTALL: 1,
508 getAddonByID: function (id, callback) {
509 callback = callback || util.identity;
510 addon = services.extensionManager.getItemForID(id);
512 addon = this.wrapAddon(addon);
513 return callback(addon);
516 wrapAddon: function wrapAddon(addon) {
517 addon = Object.create(addon.QueryInterface(Ci.nsIUpdateItem));
519 ["aboutURL", "creator", "description", "developers",
520 "homepageURL", "installDate", "optionsURL",
521 "releaseNotesURI", "updateDate"].forEach(function (item) {
522 memoize(addon, item, function (item) this.getProperty(item));
527 get permissions() 1 | (this.userDisabled ? 2 : 4),
531 getProperty: function getProperty(property) {
532 let resource = services.rdf.GetResource("urn:mozilla:item:" + this.id);
535 let target = services.extensionManager.datasource.GetTarget(resource,
536 services.rdf.GetResource("http://www.mozilla.org/2004/em-rdf#" + property), true);
538 if (target && target instanceof Ci.nsIRDFLiteral)
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);
551 get isActive() this.getProperty("isDisabled") != "true",
553 uninstall: function uninstall() {
554 services.extensionManager.uninstallItem(this.id);
557 get userDisabled() this.getProperty("userDisabled") === "true",
558 set userDisabled(val) {
559 services.extensionManager[val ? "disableItem" : "enableItem"](this.id);
566 getAddonsByTypes: function (types, callback) {
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));
574 util.timeout(function () { callback(res); });
578 getInstallForFile: function (file, callback, mimetype) {
580 addListener: function () {},
581 install: function () {
582 services.extensionManager.installItemFromFile(file, "app-profile");
587 getInstallForURL: function (url, callback, mimetype) {
588 util.assert(false, _("error.unavailable", config.host, services.runtime.version));
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);
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);
609 services.observer.addObserver(observer, "em-action-requested", false);
610 this.observers.push(observer);
612 removeAddonListener: function (listener) {
613 this.observers = this.observers.filter(function (observer) {
614 if (observer.listener !== listener)
616 services.observer.removeObserver(observer, "em-action-requested");
623 } catch(e){ if (isString(e)) e = Error(e); dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack); }
625 // vim: set fdm=marker sw=4 ts=4 et ft=javascript: