diff options
Diffstat (limited to 'toolkit/mozapps/extensions/internal/PluginProvider.jsm')
-rw-r--r-- | toolkit/mozapps/extensions/internal/PluginProvider.jsm | 535 |
1 files changed, 535 insertions, 0 deletions
diff --git a/toolkit/mozapps/extensions/internal/PluginProvider.jsm b/toolkit/mozapps/extensions/internal/PluginProvider.jsm new file mode 100644 index 0000000000..962ce70cb7 --- /dev/null +++ b/toolkit/mozapps/extensions/internal/PluginProvider.jsm @@ -0,0 +1,535 @@ +/* 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"; + +/* exported logger */ + +var EXPORTED_SYMBOLS = []; + +const { AddonManager, AddonManagerPrivate } = ChromeUtils.import( + "resource://gre/modules/AddonManager.jsm" +); +const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); + +ChromeUtils.defineModuleGetter( + this, + "Blocklist", + "resource://gre/modules/Blocklist.jsm" +); + +const URI_EXTENSION_STRINGS = + "chrome://mozapps/locale/extensions/extensions.properties"; +const LIST_UPDATED_TOPIC = "plugins-list-updated"; + +const { Log } = ChromeUtils.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); + +var PluginProvider = { + get name() { + return "PluginProvider"; + }, + + // A dictionary mapping IDs to names and descriptions + plugins: null, + + startup() { + Services.obs.addObserver(this, LIST_UPDATED_TOPIC); + }, + + /** + * Called when the application is shutting down. Only necessary for tests + * to be able to simulate a shutdown. + */ + shutdown() { + this.plugins = null; + Services.obs.removeObserver(this, LIST_UPDATED_TOPIC); + }, + + async observe(aSubject, aTopic, aData) { + switch (aTopic) { + case LIST_UPDATED_TOPIC: + if (this.plugins) { + this.updatePluginList(); + } + break; + } + }, + + /** + * Creates a PluginWrapper for a plugin object. + */ + 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 + */ + async getAddonByID(aId) { + if (!this.plugins) { + this.buildPluginList(); + } + + if (aId in this.plugins) { + return this.buildWrapper(this.plugins[aId]); + } + return null; + }, + + /** + * Called to get Addons of a particular type. + * + * @param aTypes + * An array of types to fetch. Can be null to get all types. + */ + async getAddonsByTypes(aTypes) { + if (aTypes && !aTypes.includes("plugin")) { + return []; + } + + if (!this.plugins) { + this.buildPluginList(); + } + + return Promise.all( + Object.keys(this.plugins).map(id => this.getAddonByID(id)) + ); + }, + + /** + * Called to get the current AddonInstalls, optionally restricting by type. + * + * @param aTypes + * An array of types or null to get all types + */ + getInstallsByTypes(aTypes) { + return []; + }, + + /** + * Builds a list of the current plugins reported by the plugin host + * + * @return a dictionary of plugins indexed by our generated ID + */ + 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: 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() { + 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() { + 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); + } + }, +}; + +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 ( + 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 === false && this.isFlashPlugin) { + val = AddonManager.STATE_ASK_TO_ACTIVATE; + } + + 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; + }, + + async enable() { + this.userDisabled = false; + }, + async disable() { + this.userDisabled = true; + }, + + get blocklistState() { + let { + tags: [tag], + } = pluginFor(this); + return tag.blocklistState; + }, + + async getBlocklistURL() { + let { + tags: [tag], + } = pluginFor(this); + return Blocklist.getPluginBlockURL(tag); + }, + + 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 profile directory are in the profile scope + let 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 != Cr.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() { + return 0; + }, + + get optionsType() { + return null; + }, + + get optionsURL() { + return null; + }, + + get updateDate() { + return this.installDate; + }, + + get isCompatible() { + return true; + }, + + get isPlatformCompatible() { + return true; + }, + + get providesUpdatesSecurely() { + return true; + }, + + get foreignInstall() { + return true; + }, + + get installTelemetryInfo() { + return { source: "plugin" }; + }, + + isCompatibleWith(aAppVersion, aPlatformVersion) { + return true; + }, + + findUpdates(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); + } + }, + + get isFlashPlugin() { + return pluginFor(this).tags.some(t => t.isFlashPlugin); + }, +}; + +AddonManagerPrivate.registerProvider(PluginProvider, [ + new AddonManagerPrivate.AddonType( + "plugin", + URI_EXTENSION_STRINGS, + "type.plugin.name", + AddonManager.VIEW_TYPE_LIST, + 6000, + AddonManager.TYPE_SUPPORTS_ASK_TO_ACTIVATE + ), +]); |