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