/* 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 (/]*>/i.test(description)) {
return /"'\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
),
]);