diff options
Diffstat (limited to 'toolkit/mozapps/webextensions/internal/PluginProvider.jsm')
-rw-r--r-- | toolkit/mozapps/webextensions/internal/PluginProvider.jsm | 600 |
1 files changed, 600 insertions, 0 deletions
diff --git a/toolkit/mozapps/webextensions/internal/PluginProvider.jsm b/toolkit/mozapps/webextensions/internal/PluginProvider.jsm new file mode 100644 index 000000000..075159a9a --- /dev/null +++ b/toolkit/mozapps/webextensions/internal/PluginProvider.jsm @@ -0,0 +1,600 @@ +/* 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"); +/* globals AddonManagerPrivate*/ +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) +var logger = Log.repository.getLogger(LOGGER_ID); + +function getIDHashForString(aStr) { + // return the two-digit hexadecimal code for a byte + let 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); + let hash = Array.from(binary, c => toHexString(c.charCodeAt(0))); + 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() { + return "PluginProvider"; + }, + + // A dictionary mapping IDs to names and descriptions + plugins: null, + + startup: function() { + 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() { + 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(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(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(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(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(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, (addon) => results.push(addon)); + + 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(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(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() { + 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), + name: 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() { + 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() { + let newList = this.getPluginList(); + + let lostPlugins = Object.keys(this.plugins).filter(id => !(id in newList)). + map(id => this.buildWrapper(this.plugins[id])); + let newPlugins = Object.keys(newList).filter(id => !(id in this.plugins)). + map(id => this.buildWrapper(newList[id])); + let matchedIDs = Object.keys(newList).filter(id => id in this.plugins); + + // 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"; +} + +const wrapperMap = new WeakMap(); +let pluginFor = wrapper => wrapperMap.get(wrapper); + +/** + * The PluginWrapper wraps a set of nsIPluginTags to provide the data visible to + * public callers through the API. + */ +function PluginWrapper(id, name, description, tags) { + wrapperMap.set(this, { id, name, description, tags }); +} + +PluginWrapper.prototype = { + get id() { + return pluginFor(this).id; + }, + + get type() { + return "plugin"; + }, + + get name() { + return pluginFor(this).name; + }, + + get creator() { + return null; + }, + + get description() { + return pluginFor(this).description.replace(/<\/?[a-z][^>]*>/gi, " "); + }, + + get version() { + let { tags: [tag] } = pluginFor(this); + return tag.version; + }, + + get homepageURL() { + let { description } = pluginFor(this); + if (/<A\s+HREF=[^>]*>/i.test(description)) + return /<A\s+HREF=["']?([^>"'\s]*)/i.exec(description)[1]; + return null; + }, + + get isActive() { + let { tags: [tag] } = pluginFor(this); + return !tag.blocklisted && !tag.disabled; + }, + + get appDisabled() { + let { tags: [tag] } = pluginFor(this); + return tag.blocklisted; + }, + + get userDisabled() { + let { tags: [tag] } = pluginFor(this); + if (tag.disabled) + return true; + + if ((Services.prefs.getBoolPref("plugins.click_to_play") && tag.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; + }, + + set userDisabled(val) { + let previousVal = this.userDisabled; + if (val === previousVal) + return val; + + let { tags } = pluginFor(this); + + for (let tag of tags) { + if (val === true) + tag.enabledState = Ci.nsIPluginTag.STATE_DISABLED; + else if (val === false) + tag.enabledState = Ci.nsIPluginTag.STATE_ENABLED; + else if (val == 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 && val !== 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 && val === 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 || + val == AddonManager.STATE_ASK_TO_ACTIVATE) { + AddonManagerPrivate.callAddonListeners("onPropertyChanged", this, ["userDisabled"]); + } + + return val; + }, + + get blocklistState() { + let { tags: [tag] } = pluginFor(this); + let bs = Cc["@mozilla.org/extensions/blocklist;1"]. + getService(Ci.nsIBlocklistService); + return bs.getPluginBlocklistState(tag); + }, + + get blocklistURL() { + let { tags: [tag] } = pluginFor(this); + let bs = Cc["@mozilla.org/extensions/blocklist;1"]. + getService(Ci.nsIBlocklistService); + return bs.getPluginBlocklistURL(tag); + }, + + get size() { + 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 pluginFor(this).tags) { + file.initWithPath(tag.fullpath); + if (file.isDirectory()) + size += getDirectorySize(file); + else + size += file.fileSize; + } + return size; + }, + + get pluginLibraries() { + let libs = []; + for (let tag of pluginFor(this).tags) + libs.push(tag.filename); + return libs; + }, + + get pluginFullpath() { + let paths = []; + for (let tag of pluginFor(this).tags) + paths.push(tag.fullpath); + return paths; + }, + + get pluginMimeTypes() { + let types = []; + for (let tag of pluginFor(this).tags) { + 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; + }, + + get installDate() { + let date = 0; + for (let tag of pluginFor(this).tags) { + date = Math.max(date, tag.lastModifiedTime); + } + return new Date(date); + }, + + get scope() { + let { tags: [tag] } = pluginFor(this); + let path = tag.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) + throw e; + // Do nothing: missing "Home". + } + + // Any other locations are system scope + return AddonManager.SCOPE_SYSTEM; + }, + + get pendingOperations() { + return AddonManager.PENDING_NONE; + }, + + get operationsRequiringRestart() { + return AddonManager.OP_NEEDS_RESTART_NONE; + }, + + get permissions() { + let { tags: [tag] } = pluginFor(this); + let permissions = 0; + if (tag.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; + }, + + get optionsType() { + if (canDisableFlashProtectedMode(this)) { + return AddonManager.OPTIONS_TYPE_INLINE; + } + return AddonManager.OPTIONS_TYPE_INLINE_INFO; + }, + + get optionsURL() { + return "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(aAppVersion, 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) +]); |