mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-10-31 14:15:30 +00:00
424899c3cd
Mostly just declaring globals that Cu.imports defines but there are some actual bugs here that have been fixed as well as one test that just never ran because of a hidden exception. MozReview-Commit-ID: J6uIpYp8ANx --HG-- extra : rebase_source : 5c19b92e4242088b6fc7a268f255fe9a795928f6 extra : source : 3e5b6df276a9a20fe7b3655656e62a09bc46aaa9
601 lines
18 KiB
JavaScript
601 lines
18 KiB
JavaScript
/* 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(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)
|
|
]);
|