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 "Add-on " + action + " " + event + ": " + (install.name || install.sourceURI.spec) +
26 (install.error ? ": " + addonErrors[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 this.upgrade.push(addon);
63 install.addListener(this);
66 onUpdateFinished: function (addon, error) {
67 this.remaining = this.remaining.filter(function (a) a != addon);
68 if (!this.remaining.length)
71 ? "Installing updates for addons: " + this.upgrade.map(function (i) i.name).join(", ")
72 : "No addon updates found");
79 description: "Uninstall an extension",
80 action: callResult("uninstall"),
85 description: "Enable an extension",
86 action: function (addon) { addon.userDisabled = false; },
87 filter: function ({ item }) item.userDisabled,
92 description: "Disable an extension",
93 action: function (addon) { addon.userDisabled = true; },
94 filter: function ({ item }) !item.userDisabled,
98 name: ["exto[ptions]", "extp[references]"],
99 description: "Open an extension's preference dialog",
101 action: function (addon, bang) {
103 this.window.openDialog(addon.optionsURL, "_blank", "chrome");
105 this.dactyl.open(addon.optionsURL, { from: "extoptions" });
107 filter: function ({ item }) item.isActive && item.optionsURL
111 description: "Reload an extension",
112 action: function (addon) {
113 util.assert(util.haveGecko("2b"), _("error.notUseful", config.host));
114 util.timeout(function () {
115 addon.userDisabled = true;
116 addon.userDisabled = false;
120 let ids = set(keys(JSON.parse(prefs.get("extensions.bootstrappedAddons", "{}"))));
121 return function ({ item }) !item.userDisabled && set.has(ids, item.id);
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="AddonStatus" key="status"/>
153 <td highlight="AddonButtons Buttons">
154 <a highlight="Button" key="enable">On </a>
155 <a highlight="Button" key="disable">Off</a>
156 <a highlight="Button" key="delete">Del</a>
157 <a highlight="Button" key="update">Upd</a>
158 <a highlight="Button" key="options">Opt</a>
160 <td highlight="AddonDescription" key="description"/>
162 this.list.document, this.nodes);
167 commandAllowed: function commandAllowed(cmd) {
168 util.assert(set.has(actions, cmd), "Unknown command");
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), "Command not allowed");
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;
228 for (let node in values(this.nodes))
229 if (node.update && node.update !== callee)
232 let event = this.list.document.createEvent("Events");
233 event.initEvent("dactyl-commandupdate", true, false);
234 this.list.document.dispatchEvent(event);
238 ["cancelUninstall", "findUpdates", "getResourceURI", "hasResource", "isCompatibleWith",
239 "uninstall"].forEach(function (prop) {
240 Addon.prototype[prop] = function proxy() this.addon[prop].apply(this.addon, arguments);
243 ["aboutURL", "appDisabled", "applyBackgroundUpdates", "blocklistState", "contributors", "creator",
244 "description", "developers", "homepageURL", "iconURL", "id", "install", "installDate", "isActive",
245 "isCompatible", "isPlatformCompatible", "name", "operationsRequiringRestart", "optionsURL",
246 "pendingOperations", "pendingUpgrade", "permissions", "providesUpdatesSecurely", "releaseNotesURI",
247 "scope", "screenshots", "size", "sourceURI", "translators", "type", "updateDate", "userDisabled",
248 "version"].forEach(function (prop) {
249 Object.defineProperty(Addon.prototype, prop, {
250 get: function get_proxy() this.addon[prop],
251 set: function set_proxy(val) this.addon[prop] = val
255 var AddonList = Class("AddonList", {
256 init: function init(modules, types, filter) {
257 this.modules = modules;
258 this.filter = filter && filter.toLowerCase();
263 AddonManager.getAddonsByTypes(types, this.closure(function (addons) {
264 this._addons = addons;
268 AddonManager.addAddonListener(this);
270 cleanup: function cleanup() {
271 AddonManager.removeAddonListener(this);
274 _init: function _init() {
275 this._addons.forEach(this.closure.addAddon);
280 message: Class.memoize(function () {
282 XML.ignoreWhitespace = true;
283 util.xmlToDom(<table highlight="Addons" key="list" xmlns={XHTML}>
284 <tr highlight="AddonHead">
291 </table>, this.document, this.nodes);
296 return this.nodes.list;
299 addAddon: function addAddon(addon) {
300 if (addon.id in this.addons)
303 if (this.filter && addon.name.toLowerCase().indexOf(this.filter) === -1)
306 addon = Addon(addon, this);
307 this.addons[addon.id] = addon;
309 let index = values(this.addons).sort(function (a, b) a.compare(b))
312 this.nodes.list.insertBefore(addon.nodes.row,
313 this.nodes.list.childNodes[index + 1]);
317 removeAddon: function removeAddon(addon) {
318 if (addon.id in this.addons) {
319 this.nodes.list.removeChild(this.addons[addon.id].nodes.row);
320 delete this.addons[addon.id];
325 leave: function leave(stack) {
330 update: function update(addon) {
331 if (addon && addon.id in this.addons)
332 this.addons[addon.id].update();
334 this.modules.mow.resize(false);
337 onDisabled: function (addon) { this.update(addon); },
338 onDisabling: function (addon) { this.update(addon); },
339 onEnabled: function (addon) { this.update(addon); },
340 onEnabling: function (addon) { this.update(addon); },
341 onInstalled: function (addon) { this.addAddon(addon); },
342 onInstalling: function (addon) { this.update(addon); },
343 onUninstalled: function (addon) { this.removeAddon(addon); },
344 onUninstalling: function (addon) { this.update(addon); },
345 onOperationCancelled: function (addon) { this.update(addon); },
346 onPropertyChanged: function onPropertyChanged(addon, properties) {}
349 var Addons = Module("addons", {
352 commands: function (dactyl, modules, window) {
353 const { CommandOption, commands, completion } = modules;
355 commands.add(["addo[ns]", "ao"],
356 "List installed extensions",
358 let addons = AddonList(modules, args["-types"], args[0]);
359 modules.commandline.echo(addons);
361 if (modules.commandline.savingOutput)
362 util.waitFor(function () addons.ready);
368 names: ["-types", "-type", "-t"],
369 description: "The add-on types to list",
370 default: ["extension"],
371 completer: function (context, args) completion.addonType(context),
372 type: CommandOption.LIST
377 let addonListener = AddonListener(modules);
379 commands.add(["exta[dd]"],
380 "Install an extension",
383 let file = io.File(url);
384 function install(addonInstall) {
385 addonInstall.addListener(addonListener);
386 addonInstall.install();
390 AddonManager.getInstallForURL(url, install, "application/x-xpinstall");
391 else if (file.isReadable() && file.isFile())
392 AddonManager.getInstallForFile(file, install, "application/x-xpinstall");
393 else if (file.isDirectory())
394 dactyl.echoerr(_("addon.cantInstallDir", file.path.quote()));
396 dactyl.echoerr(_("io.notReadable-1", file.path));
399 completer: function (context) {
400 context.filters.push(function ({ isdir, text }) isdir || /\.xpi$/.test(text));
401 completion.file(context);
406 // TODO: handle extension dependencies
407 values(actions).forEach(function (command) {
408 let perm = command.perm && AddonManager["PERM_CAN_" + command.perm.toUpperCase()];
409 function ok(addon) !perm || addon.permissions & perm;
411 commands.add(Array.concat(command.name),
415 if (args.bang && !command.bang)
416 dactyl.assert(!name, _("error.trailing"));
418 dactyl.assert(name, _("error.argumentRequired"));
420 AddonManager.getAddonsByTypes(["extension"], dactyl.wrapCallback(function (list) {
421 if (!args.bang || command.bang) {
422 list = list.filter(function (extension) extension.name == name);
423 if (list.length == 0)
424 return void dactyl.echoerr(_("error.invalidArgument", name));
426 return void dactyl.echoerr(_("error.invalidOperation"));
429 command.actions(list, this.modules);
431 list.forEach(function (addon) command.action.call(this.modules, addon, args.bang), this);
434 argCount: "?", // FIXME: should be "1"
436 completer: function (context) {
437 completion.extension(context);
438 context.filters.push(function ({ item }) ok(item));
440 context.filters.push(command.filter);
446 completion: function (dactyl, modules, window) {
447 completion.addonType = function addonType(context) {
448 let base = ["extension", "theme"];
449 function update(types) {
450 context.completions = types.map(function (t) [t, util.capitalize(t)]);
453 context.generate = function generate() {
455 if (AddonManager.getAllAddons) {
456 context.incomplete = true;
457 AddonManager.getAllAddons(function (addons) {
458 context.incomplete = false;
459 update(array.uniq(base.concat(addons.map(function (a) a.type)),
466 completion.extension = function extension(context, types) {
467 context.title = ["Extension"];
468 context.anchored = false;
469 context.keys = { text: "name", description: "description", icon: "iconURL" },
470 context.generate = function () {
471 context.incomplete = true;
472 AddonManager.getAddonsByTypes(types || ["extension"], function (addons) {
473 context.incomplete = false;
474 context.completions = addons;
481 if (!services.has("extensionManager"))
482 Components.utils.import("resource://gre/modules/AddonManager.jsm");
485 PERM_CAN_UNINSTALL: 1,
490 getAddonByID: function (id, callback) {
491 callback = callback || util.identity;
492 addon = services.extensionManager.getItemForID(id);
494 addon = this.wrapAddon(addon);
495 return callback(addon);
497 wrapAddon: function wrapAddon(addon) {
498 addon = Object.create(addon.QueryInterface(Ci.nsIUpdateItem));
500 function getRdfProperty(item, property) {
501 let resource = services.rdf.GetResource("urn:mozilla:item:" + item.id);
505 let target = services.extensionManager.datasource.GetTarget(resource,
506 services.rdf.GetResource("http://www.mozilla.org/2004/em-rdf#" + property), true);
507 if (target && target instanceof Ci.nsIRDFLiteral)
508 value = target.Value;
514 ["aboutURL", "creator", "description", "developers",
515 "homepageURL", "installDate", "optionsURL",
516 "releaseNotesURI", "updateDate"].forEach(function (item) {
517 memoize(addon, item, function (item) getRdfProperty(this, item));
522 get permissions() 1 | (this.userDisabled ? 2 : 4),
526 installLocation: Class.memoize(function () services.extensionManager.getInstallLocation(this.id)),
527 getResourceURI: function getResourceURI(path) {
528 let file = this.installLocation.getItemFile(this.id, path);
529 return services.io.newFileURI(file);
532 isActive: getRdfProperty(addon, "isDisabled") != "true",
534 uninstall: function uninstall() {
535 services.extensionManager.uninstallItem(this.id);
538 get userDisabled() getRdfProperty(addon, "userDisabled") === "true",
539 set userDisabled(val) {
540 services.extensionManager[val ? "disableItem" : "enableItem"](this.id);
546 getAddonsByTypes: function (types, callback) {
548 for (let [, type] in Iterator(types))
549 for (let [, item] in Iterator(services.extensionManager
550 .getItemList(Ci.nsIUpdateItem["TYPE_" + type.toUpperCase()], {})))
551 res.push(this.wrapAddon(item));
554 util.timeout(function () { callback(res); });
557 getInstallForFile: function (file, callback, mimetype) {
559 addListener: function () {},
560 install: function () {
561 services.extensionManager.installItemFromFile(file, "app-profile");
565 getInstallForURL: function (url, callback, mimetype) {
566 util.assert(false, _("error.unavailable", config.host, services.runtime.version));
569 addAddonListener: function (listener) {
570 observer.listener = listener;
571 function observer(subject, topic, data) {
572 if (subject instanceof Ci.nsIUpdateItem)
573 subject = AddonManager.wrapAddon(subject);
575 if (data === "item-installed")
576 listener.onInstalling(subject, true);
577 else if (data === "item-uninstalled")
578 listener.onUnistalling(subject, true);
579 else if (data === "item-upgraded")
580 listener.onInstalling(subject, true);
581 else if (data === "item-enabled")
582 listener.onEnabling(subject, true);
583 else if (data === "item-disabled")
584 listener.onDisabling(subject, true);
586 services.observer.addObserver(observer, "em-action-requested", false);
587 this.observers.push(observer);
589 removeAddonListener: function (listener) {
590 this.observers = this.observers.filter(function (observer) {
591 if (observer.listener !== listener)
593 services.observer.removeObserver(observer, "em-action-requested");
598 var addonErrors = array.toObject([
599 [AddonManager.ERROR_NETWORK_FAILURE, "A network error occurred"],
600 [AddonManager.ERROR_INCORRECT_HASH, "The downloaded file did not match the expected hash"],
601 [AddonManager.ERROR_CORRUPT_FILE, "The file appears to be corrupt"],
602 [AddonManager.ERROR_FILE_ACCESS, "There was an error accessing the filesystem"]]);
606 } catch(e){ if (isString(e)) e = Error(e); dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack); }
608 // vim: set fdm=marker sw=4 ts=4 et ft=javascript: