michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public
michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this
michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0:
michael@0: "use strict";
michael@0:
michael@0: const Cc = Components.classes;
michael@0: const Ci = Components.interfaces;
michael@0: const Cu = Components.utils;
michael@0:
michael@0: this.EXPORTED_SYMBOLS = [];
michael@0:
michael@0: Cu.import("resource://gre/modules/AddonManager.jsm");
michael@0: Cu.import("resource://gre/modules/Services.jsm");
michael@0:
michael@0: const URI_EXTENSION_STRINGS = "chrome://mozapps/locale/extensions/extensions.properties";
michael@0: const STRING_TYPE_NAME = "type.%ID%.name";
michael@0: const LIST_UPDATED_TOPIC = "plugins-list-updated";
michael@0:
michael@0: Cu.import("resource://gre/modules/Log.jsm");
michael@0: const LOGGER_ID = "addons.plugins";
michael@0:
michael@0: // Create a new logger for use by the Addons Plugin Provider
michael@0: // (Requires AddonManager.jsm)
michael@0: let logger = Log.repository.getLogger(LOGGER_ID);
michael@0:
michael@0: function getIDHashForString(aStr) {
michael@0: // return the two-digit hexadecimal code for a byte
michael@0: function toHexString(charCode)
michael@0: ("0" + charCode.toString(16)).slice(-2);
michael@0:
michael@0: let hasher = Cc["@mozilla.org/security/hash;1"].
michael@0: createInstance(Ci.nsICryptoHash);
michael@0: hasher.init(Ci.nsICryptoHash.MD5);
michael@0: let stringStream = Cc["@mozilla.org/io/string-input-stream;1"].
michael@0: createInstance(Ci.nsIStringInputStream);
michael@0: stringStream.data = aStr ? aStr : "null";
michael@0: hasher.updateFromStream(stringStream, -1);
michael@0:
michael@0: // convert the binary hash data to a hex string.
michael@0: let binary = hasher.finish(false);
michael@0: let hash = [toHexString(binary.charCodeAt(i)) for (i in binary)].join("").toLowerCase();
michael@0: return "{" + hash.substr(0, 8) + "-" +
michael@0: hash.substr(8, 4) + "-" +
michael@0: hash.substr(12, 4) + "-" +
michael@0: hash.substr(16, 4) + "-" +
michael@0: hash.substr(20) + "}";
michael@0: }
michael@0:
michael@0: var PluginProvider = {
michael@0: // A dictionary mapping IDs to names and descriptions
michael@0: plugins: null,
michael@0:
michael@0: startup: function PL_startup() {
michael@0: Services.obs.addObserver(this, LIST_UPDATED_TOPIC, false);
michael@0: Services.obs.addObserver(this, AddonManager.OPTIONS_NOTIFICATION_DISPLAYED, false);
michael@0: },
michael@0:
michael@0: /**
michael@0: * Called when the application is shutting down. Only necessary for tests
michael@0: * to be able to simulate a shutdown.
michael@0: */
michael@0: shutdown: function PL_shutdown() {
michael@0: this.plugins = null;
michael@0: Services.obs.removeObserver(this, AddonManager.OPTIONS_NOTIFICATION_DISPLAYED);
michael@0: Services.obs.removeObserver(this, LIST_UPDATED_TOPIC);
michael@0: },
michael@0:
michael@0: observe: function(aSubject, aTopic, aData) {
michael@0: switch (aTopic) {
michael@0: case AddonManager.OPTIONS_NOTIFICATION_DISPLAYED:
michael@0: this.getAddonByID(aData, function PL_displayPluginInfo(plugin) {
michael@0: if (!plugin)
michael@0: return;
michael@0:
michael@0: let libLabel = aSubject.getElementById("pluginLibraries");
michael@0: libLabel.textContent = plugin.pluginLibraries.join(", ");
michael@0:
michael@0: let typeLabel = aSubject.getElementById("pluginMimeTypes"), types = [];
michael@0: for (let type of plugin.pluginMimeTypes) {
michael@0: let extras = [type.description.trim(), type.suffixes].
michael@0: filter(function(x) x).join(": ");
michael@0: types.push(type.type + (extras ? " (" + extras + ")" : ""));
michael@0: }
michael@0: typeLabel.textContent = types.join(",\n");
michael@0: });
michael@0: break;
michael@0: case LIST_UPDATED_TOPIC:
michael@0: if (this.plugins)
michael@0: this.updatePluginList();
michael@0: break;
michael@0: }
michael@0: },
michael@0:
michael@0: /**
michael@0: * Creates a PluginWrapper for a plugin object.
michael@0: */
michael@0: buildWrapper: function PL_buildWrapper(aPlugin) {
michael@0: return new PluginWrapper(aPlugin.id,
michael@0: aPlugin.name,
michael@0: aPlugin.description,
michael@0: aPlugin.tags);
michael@0: },
michael@0:
michael@0: /**
michael@0: * Called to get an Addon with a particular ID.
michael@0: *
michael@0: * @param aId
michael@0: * The ID of the add-on to retrieve
michael@0: * @param aCallback
michael@0: * A callback to pass the Addon to
michael@0: */
michael@0: getAddonByID: function PL_getAddon(aId, aCallback) {
michael@0: if (!this.plugins)
michael@0: this.buildPluginList();
michael@0:
michael@0: if (aId in this.plugins)
michael@0: aCallback(this.buildWrapper(this.plugins[aId]));
michael@0: else
michael@0: aCallback(null);
michael@0: },
michael@0:
michael@0: /**
michael@0: * Called to get Addons of a particular type.
michael@0: *
michael@0: * @param aTypes
michael@0: * An array of types to fetch. Can be null to get all types.
michael@0: * @param callback
michael@0: * A callback to pass an array of Addons to
michael@0: */
michael@0: getAddonsByTypes: function PL_getAddonsByTypes(aTypes, aCallback) {
michael@0: if (aTypes && aTypes.indexOf("plugin") < 0) {
michael@0: aCallback([]);
michael@0: return;
michael@0: }
michael@0:
michael@0: if (!this.plugins)
michael@0: this.buildPluginList();
michael@0:
michael@0: let results = [];
michael@0:
michael@0: for (let id in this.plugins) {
michael@0: this.getAddonByID(id, function(aAddon) {
michael@0: results.push(aAddon);
michael@0: });
michael@0: }
michael@0:
michael@0: aCallback(results);
michael@0: },
michael@0:
michael@0: /**
michael@0: * Called to get Addons that have pending operations.
michael@0: *
michael@0: * @param aTypes
michael@0: * An array of types to fetch. Can be null to get all types
michael@0: * @param aCallback
michael@0: * A callback to pass an array of Addons to
michael@0: */
michael@0: getAddonsWithOperationsByTypes: function PL_getAddonsWithOperationsByTypes(aTypes, aCallback) {
michael@0: aCallback([]);
michael@0: },
michael@0:
michael@0: /**
michael@0: * Called to get the current AddonInstalls, optionally restricting by type.
michael@0: *
michael@0: * @param aTypes
michael@0: * An array of types or null to get all types
michael@0: * @param aCallback
michael@0: * A callback to pass the array of AddonInstalls to
michael@0: */
michael@0: getInstallsByTypes: function PL_getInstallsByTypes(aTypes, aCallback) {
michael@0: aCallback([]);
michael@0: },
michael@0:
michael@0: /**
michael@0: * Builds a list of the current plugins reported by the plugin host
michael@0: *
michael@0: * @return a dictionary of plugins indexed by our generated ID
michael@0: */
michael@0: getPluginList: function PL_getPluginList() {
michael@0: let tags = Cc["@mozilla.org/plugin/host;1"].
michael@0: getService(Ci.nsIPluginHost).
michael@0: getPluginTags({});
michael@0:
michael@0: let list = {};
michael@0: let seenPlugins = {};
michael@0: for (let tag of tags) {
michael@0: if (!(tag.name in seenPlugins))
michael@0: seenPlugins[tag.name] = {};
michael@0: if (!(tag.description in seenPlugins[tag.name])) {
michael@0: let plugin = {
michael@0: id: getIDHashForString(tag.name + tag.description),
michael@0: name: tag.name,
michael@0: description: tag.description,
michael@0: tags: [tag]
michael@0: };
michael@0:
michael@0: seenPlugins[tag.name][tag.description] = plugin;
michael@0: list[plugin.id] = plugin;
michael@0: }
michael@0: else {
michael@0: seenPlugins[tag.name][tag.description].tags.push(tag);
michael@0: }
michael@0: }
michael@0:
michael@0: return list;
michael@0: },
michael@0:
michael@0: /**
michael@0: * Builds the list of known plugins from the plugin host
michael@0: */
michael@0: buildPluginList: function PL_buildPluginList() {
michael@0: this.plugins = this.getPluginList();
michael@0: },
michael@0:
michael@0: /**
michael@0: * Updates the plugins from the plugin host by comparing the current plugins
michael@0: * to the last known list sending out any necessary API notifications for
michael@0: * changes.
michael@0: */
michael@0: updatePluginList: function PL_updatePluginList() {
michael@0: let newList = this.getPluginList();
michael@0:
michael@0: let lostPlugins = [this.buildWrapper(this.plugins[id])
michael@0: for each (id in Object.keys(this.plugins)) if (!(id in newList))];
michael@0: let newPlugins = [this.buildWrapper(newList[id])
michael@0: for each (id in Object.keys(newList)) if (!(id in this.plugins))];
michael@0: let matchedIDs = [id for each (id in Object.keys(newList)) if (id in this.plugins)];
michael@0:
michael@0: // The plugin host generates new tags for every plugin after a scan and
michael@0: // if the plugin's filename has changed then the disabled state won't have
michael@0: // been carried across, send out notifications for anything that has
michael@0: // changed (see bug 830267).
michael@0: let changedWrappers = [];
michael@0: for (let id of matchedIDs) {
michael@0: let oldWrapper = this.buildWrapper(this.plugins[id]);
michael@0: let newWrapper = this.buildWrapper(newList[id]);
michael@0:
michael@0: if (newWrapper.isActive != oldWrapper.isActive) {
michael@0: AddonManagerPrivate.callAddonListeners(newWrapper.isActive ?
michael@0: "onEnabling" : "onDisabling",
michael@0: newWrapper, false);
michael@0: changedWrappers.push(newWrapper);
michael@0: }
michael@0: }
michael@0:
michael@0: // Notify about new installs
michael@0: for (let plugin of newPlugins) {
michael@0: AddonManagerPrivate.callInstallListeners("onExternalInstall", null,
michael@0: plugin, null, false);
michael@0: AddonManagerPrivate.callAddonListeners("onInstalling", plugin, false);
michael@0: }
michael@0:
michael@0: // Notify for any plugins that have vanished.
michael@0: for (let plugin of lostPlugins)
michael@0: AddonManagerPrivate.callAddonListeners("onUninstalling", plugin, false);
michael@0:
michael@0: this.plugins = newList;
michael@0:
michael@0: // Signal that new installs are complete
michael@0: for (let plugin of newPlugins)
michael@0: AddonManagerPrivate.callAddonListeners("onInstalled", plugin);
michael@0:
michael@0: // Signal that enables/disables are complete
michael@0: for (let wrapper of changedWrappers) {
michael@0: AddonManagerPrivate.callAddonListeners(wrapper.isActive ?
michael@0: "onEnabled" : "onDisabled",
michael@0: wrapper);
michael@0: }
michael@0:
michael@0: // Signal that uninstalls are complete
michael@0: for (let plugin of lostPlugins)
michael@0: AddonManagerPrivate.callAddonListeners("onUninstalled", plugin);
michael@0: }
michael@0: };
michael@0:
michael@0: /**
michael@0: * The PluginWrapper wraps a set of nsIPluginTags to provide the data visible to
michael@0: * public callers through the API.
michael@0: */
michael@0: function PluginWrapper(aId, aName, aDescription, aTags) {
michael@0: let safedesc = aDescription.replace(/<\/?[a-z][^>]*>/gi, " ");
michael@0: let homepageURL = null;
michael@0: if (/]*>/i.test(aDescription))
michael@0: homepageURL = /"'\s]*)/i.exec(aDescription)[1];
michael@0:
michael@0: this.__defineGetter__("id", function() aId);
michael@0: this.__defineGetter__("type", function() "plugin");
michael@0: this.__defineGetter__("name", function() aName);
michael@0: this.__defineGetter__("creator", function() null);
michael@0: this.__defineGetter__("description", function() safedesc);
michael@0: this.__defineGetter__("version", function() aTags[0].version);
michael@0: this.__defineGetter__("homepageURL", function() homepageURL);
michael@0:
michael@0: this.__defineGetter__("isActive", function() !aTags[0].blocklisted && !aTags[0].disabled);
michael@0: this.__defineGetter__("appDisabled", function() aTags[0].blocklisted);
michael@0:
michael@0: this.__defineGetter__("userDisabled", function() {
michael@0: if (aTags[0].disabled)
michael@0: return true;
michael@0:
michael@0: if ((Services.prefs.getBoolPref("plugins.click_to_play") && aTags[0].clicktoplay) ||
michael@0: this.blocklistState == Ci.nsIBlocklistService.STATE_VULNERABLE_UPDATE_AVAILABLE ||
michael@0: this.blocklistState == Ci.nsIBlocklistService.STATE_VULNERABLE_NO_UPDATE)
michael@0: return AddonManager.STATE_ASK_TO_ACTIVATE;
michael@0:
michael@0: return false;
michael@0: });
michael@0:
michael@0: this.__defineSetter__("userDisabled", function(aVal) {
michael@0: let previousVal = this.userDisabled;
michael@0: if (aVal === previousVal)
michael@0: return aVal;
michael@0:
michael@0: for (let tag of aTags) {
michael@0: if (aVal === true)
michael@0: tag.enabledState = Ci.nsIPluginTag.STATE_DISABLED;
michael@0: else if (aVal === false)
michael@0: tag.enabledState = Ci.nsIPluginTag.STATE_ENABLED;
michael@0: else if (aVal == AddonManager.STATE_ASK_TO_ACTIVATE)
michael@0: tag.enabledState = Ci.nsIPluginTag.STATE_CLICKTOPLAY;
michael@0: }
michael@0:
michael@0: // If 'userDisabled' was 'true' and we're going to a state that's not
michael@0: // that, we're enabling, so call those listeners.
michael@0: if (previousVal === true && aVal !== true) {
michael@0: AddonManagerPrivate.callAddonListeners("onEnabling", this, false);
michael@0: AddonManagerPrivate.callAddonListeners("onEnabled", this);
michael@0: }
michael@0:
michael@0: // If 'userDisabled' was not 'true' and we're going to a state where
michael@0: // it is, we're disabling, so call those listeners.
michael@0: if (previousVal !== true && aVal === true) {
michael@0: AddonManagerPrivate.callAddonListeners("onDisabling", this, false);
michael@0: AddonManagerPrivate.callAddonListeners("onDisabled", this);
michael@0: }
michael@0:
michael@0: // If the 'userDisabled' value involved AddonManager.STATE_ASK_TO_ACTIVATE,
michael@0: // call the onPropertyChanged listeners.
michael@0: if (previousVal == AddonManager.STATE_ASK_TO_ACTIVATE ||
michael@0: aVal == AddonManager.STATE_ASK_TO_ACTIVATE) {
michael@0: AddonManagerPrivate.callAddonListeners("onPropertyChanged", this, ["userDisabled"]);
michael@0: }
michael@0:
michael@0: return aVal;
michael@0: });
michael@0:
michael@0:
michael@0: this.__defineGetter__("blocklistState", function() {
michael@0: let bs = Cc["@mozilla.org/extensions/blocklist;1"].
michael@0: getService(Ci.nsIBlocklistService);
michael@0: return bs.getPluginBlocklistState(aTags[0]);
michael@0: });
michael@0:
michael@0: this.__defineGetter__("blocklistURL", function() {
michael@0: let bs = Cc["@mozilla.org/extensions/blocklist;1"].
michael@0: getService(Ci.nsIBlocklistService);
michael@0: return bs.getPluginBlocklistURL(aTags[0]);
michael@0: });
michael@0:
michael@0: this.__defineGetter__("size", function() {
michael@0: function getDirectorySize(aFile) {
michael@0: let size = 0;
michael@0: let entries = aFile.directoryEntries.QueryInterface(Ci.nsIDirectoryEnumerator);
michael@0: let entry;
michael@0: while ((entry = entries.nextFile)) {
michael@0: if (entry.isSymlink() || !entry.isDirectory())
michael@0: size += entry.fileSize;
michael@0: else
michael@0: size += getDirectorySize(entry);
michael@0: }
michael@0: entries.close();
michael@0: return size;
michael@0: }
michael@0:
michael@0: let size = 0;
michael@0: let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
michael@0: for (let tag of aTags) {
michael@0: file.initWithPath(tag.fullpath);
michael@0: if (file.isDirectory())
michael@0: size += getDirectorySize(file);
michael@0: else
michael@0: size += file.fileSize;
michael@0: }
michael@0: return size;
michael@0: });
michael@0:
michael@0: this.__defineGetter__("pluginLibraries", function() {
michael@0: let libs = [];
michael@0: for (let tag of aTags)
michael@0: libs.push(tag.filename);
michael@0: return libs;
michael@0: });
michael@0:
michael@0: this.__defineGetter__("pluginFullpath", function() {
michael@0: let paths = [];
michael@0: for (let tag of aTags)
michael@0: paths.push(tag.fullpath);
michael@0: return paths;
michael@0: })
michael@0:
michael@0: this.__defineGetter__("pluginMimeTypes", function() {
michael@0: let types = [];
michael@0: for (let tag of aTags) {
michael@0: let mimeTypes = tag.getMimeTypes({});
michael@0: let mimeDescriptions = tag.getMimeDescriptions({});
michael@0: let extensions = tag.getExtensions({});
michael@0: for (let i = 0; i < mimeTypes.length; i++) {
michael@0: let type = {};
michael@0: type.type = mimeTypes[i];
michael@0: type.description = mimeDescriptions[i];
michael@0: type.suffixes = extensions[i];
michael@0:
michael@0: types.push(type);
michael@0: }
michael@0: }
michael@0: return types;
michael@0: });
michael@0:
michael@0: this.__defineGetter__("installDate", function() {
michael@0: let date = 0;
michael@0: for (let tag of aTags) {
michael@0: date = Math.max(date, tag.lastModifiedTime);
michael@0: }
michael@0: return new Date(date);
michael@0: });
michael@0:
michael@0: this.__defineGetter__("scope", function() {
michael@0: let path = aTags[0].fullpath;
michael@0: // Plugins inside the application directory are in the application scope
michael@0: let dir = Services.dirsvc.get("APlugns", Ci.nsIFile);
michael@0: if (path.startsWith(dir.path))
michael@0: return AddonManager.SCOPE_APPLICATION;
michael@0:
michael@0: // Plugins inside the profile directory are in the profile scope
michael@0: dir = Services.dirsvc.get("ProfD", Ci.nsIFile);
michael@0: if (path.startsWith(dir.path))
michael@0: return AddonManager.SCOPE_PROFILE;
michael@0:
michael@0: // Plugins anywhere else in the user's home are in the user scope,
michael@0: // but not all platforms have a home directory.
michael@0: try {
michael@0: dir = Services.dirsvc.get("Home", Ci.nsIFile);
michael@0: if (path.startsWith(dir.path))
michael@0: return AddonManager.SCOPE_USER;
michael@0: } catch (e if (e.result && e.result == Components.results.NS_ERROR_FAILURE)) {
michael@0: // Do nothing: missing "Home".
michael@0: }
michael@0:
michael@0: // Any other locations are system scope
michael@0: return AddonManager.SCOPE_SYSTEM;
michael@0: });
michael@0:
michael@0: this.__defineGetter__("pendingOperations", function() {
michael@0: return AddonManager.PENDING_NONE;
michael@0: });
michael@0:
michael@0: this.__defineGetter__("operationsRequiringRestart", function() {
michael@0: return AddonManager.OP_NEEDS_RESTART_NONE;
michael@0: });
michael@0:
michael@0: this.__defineGetter__("permissions", function() {
michael@0: let permissions = 0;
michael@0: if (!this.appDisabled) {
michael@0:
michael@0: if (this.userDisabled !== true)
michael@0: permissions |= AddonManager.PERM_CAN_DISABLE;
michael@0:
michael@0: let blocklistState = this.blocklistState;
michael@0: let isCTPBlocklisted =
michael@0: (blocklistState == Ci.nsIBlocklistService.STATE_VULNERABLE_NO_UPDATE ||
michael@0: blocklistState == Ci.nsIBlocklistService.STATE_VULNERABLE_UPDATE_AVAILABLE);
michael@0:
michael@0: if (this.userDisabled !== AddonManager.STATE_ASK_TO_ACTIVATE &&
michael@0: (Services.prefs.getBoolPref("plugins.click_to_play") ||
michael@0: isCTPBlocklisted)) {
michael@0: permissions |= AddonManager.PERM_CAN_ASK_TO_ACTIVATE;
michael@0: }
michael@0:
michael@0: if (this.userDisabled !== false && !isCTPBlocklisted) {
michael@0: permissions |= AddonManager.PERM_CAN_ENABLE;
michael@0: }
michael@0: }
michael@0: return permissions;
michael@0: });
michael@0: }
michael@0:
michael@0: PluginWrapper.prototype = {
michael@0: optionsType: AddonManager.OPTIONS_TYPE_INLINE_INFO,
michael@0: optionsURL: "chrome://mozapps/content/extensions/pluginPrefs.xul",
michael@0:
michael@0: get updateDate() {
michael@0: return this.installDate;
michael@0: },
michael@0:
michael@0: get isCompatible() {
michael@0: return true;
michael@0: },
michael@0:
michael@0: get isPlatformCompatible() {
michael@0: return true;
michael@0: },
michael@0:
michael@0: get providesUpdatesSecurely() {
michael@0: return true;
michael@0: },
michael@0:
michael@0: get foreignInstall() {
michael@0: return true;
michael@0: },
michael@0:
michael@0: isCompatibleWith: function(aAppVerison, aPlatformVersion) {
michael@0: return true;
michael@0: },
michael@0:
michael@0: findUpdates: function(aListener, aReason, aAppVersion, aPlatformVersion) {
michael@0: if ("onNoCompatibilityUpdateAvailable" in aListener)
michael@0: aListener.onNoCompatibilityUpdateAvailable(this);
michael@0: if ("onNoUpdateAvailable" in aListener)
michael@0: aListener.onNoUpdateAvailable(this);
michael@0: if ("onUpdateFinished" in aListener)
michael@0: aListener.onUpdateFinished(this);
michael@0: }
michael@0: };
michael@0:
michael@0: AddonManagerPrivate.registerProvider(PluginProvider, [
michael@0: new AddonManagerPrivate.AddonType("plugin", URI_EXTENSION_STRINGS,
michael@0: STRING_TYPE_NAME,
michael@0: AddonManager.VIEW_TYPE_LIST, 6000,
michael@0: AddonManager.TYPE_SUPPORTS_ASK_TO_ACTIVATE)
michael@0: ]);