1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/toolkit/mozapps/extensions/internal/PluginProvider.jsm Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,531 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +"use strict"; 1.9 + 1.10 +const Cc = Components.classes; 1.11 +const Ci = Components.interfaces; 1.12 +const Cu = Components.utils; 1.13 + 1.14 +this.EXPORTED_SYMBOLS = []; 1.15 + 1.16 +Cu.import("resource://gre/modules/AddonManager.jsm"); 1.17 +Cu.import("resource://gre/modules/Services.jsm"); 1.18 + 1.19 +const URI_EXTENSION_STRINGS = "chrome://mozapps/locale/extensions/extensions.properties"; 1.20 +const STRING_TYPE_NAME = "type.%ID%.name"; 1.21 +const LIST_UPDATED_TOPIC = "plugins-list-updated"; 1.22 + 1.23 +Cu.import("resource://gre/modules/Log.jsm"); 1.24 +const LOGGER_ID = "addons.plugins"; 1.25 + 1.26 +// Create a new logger for use by the Addons Plugin Provider 1.27 +// (Requires AddonManager.jsm) 1.28 +let logger = Log.repository.getLogger(LOGGER_ID); 1.29 + 1.30 +function getIDHashForString(aStr) { 1.31 + // return the two-digit hexadecimal code for a byte 1.32 + function toHexString(charCode) 1.33 + ("0" + charCode.toString(16)).slice(-2); 1.34 + 1.35 + let hasher = Cc["@mozilla.org/security/hash;1"]. 1.36 + createInstance(Ci.nsICryptoHash); 1.37 + hasher.init(Ci.nsICryptoHash.MD5); 1.38 + let stringStream = Cc["@mozilla.org/io/string-input-stream;1"]. 1.39 + createInstance(Ci.nsIStringInputStream); 1.40 + stringStream.data = aStr ? aStr : "null"; 1.41 + hasher.updateFromStream(stringStream, -1); 1.42 + 1.43 + // convert the binary hash data to a hex string. 1.44 + let binary = hasher.finish(false); 1.45 + let hash = [toHexString(binary.charCodeAt(i)) for (i in binary)].join("").toLowerCase(); 1.46 + return "{" + hash.substr(0, 8) + "-" + 1.47 + hash.substr(8, 4) + "-" + 1.48 + hash.substr(12, 4) + "-" + 1.49 + hash.substr(16, 4) + "-" + 1.50 + hash.substr(20) + "}"; 1.51 +} 1.52 + 1.53 +var PluginProvider = { 1.54 + // A dictionary mapping IDs to names and descriptions 1.55 + plugins: null, 1.56 + 1.57 + startup: function PL_startup() { 1.58 + Services.obs.addObserver(this, LIST_UPDATED_TOPIC, false); 1.59 + Services.obs.addObserver(this, AddonManager.OPTIONS_NOTIFICATION_DISPLAYED, false); 1.60 + }, 1.61 + 1.62 + /** 1.63 + * Called when the application is shutting down. Only necessary for tests 1.64 + * to be able to simulate a shutdown. 1.65 + */ 1.66 + shutdown: function PL_shutdown() { 1.67 + this.plugins = null; 1.68 + Services.obs.removeObserver(this, AddonManager.OPTIONS_NOTIFICATION_DISPLAYED); 1.69 + Services.obs.removeObserver(this, LIST_UPDATED_TOPIC); 1.70 + }, 1.71 + 1.72 + observe: function(aSubject, aTopic, aData) { 1.73 + switch (aTopic) { 1.74 + case AddonManager.OPTIONS_NOTIFICATION_DISPLAYED: 1.75 + this.getAddonByID(aData, function PL_displayPluginInfo(plugin) { 1.76 + if (!plugin) 1.77 + return; 1.78 + 1.79 + let libLabel = aSubject.getElementById("pluginLibraries"); 1.80 + libLabel.textContent = plugin.pluginLibraries.join(", "); 1.81 + 1.82 + let typeLabel = aSubject.getElementById("pluginMimeTypes"), types = []; 1.83 + for (let type of plugin.pluginMimeTypes) { 1.84 + let extras = [type.description.trim(), type.suffixes]. 1.85 + filter(function(x) x).join(": "); 1.86 + types.push(type.type + (extras ? " (" + extras + ")" : "")); 1.87 + } 1.88 + typeLabel.textContent = types.join(",\n"); 1.89 + }); 1.90 + break; 1.91 + case LIST_UPDATED_TOPIC: 1.92 + if (this.plugins) 1.93 + this.updatePluginList(); 1.94 + break; 1.95 + } 1.96 + }, 1.97 + 1.98 + /** 1.99 + * Creates a PluginWrapper for a plugin object. 1.100 + */ 1.101 + buildWrapper: function PL_buildWrapper(aPlugin) { 1.102 + return new PluginWrapper(aPlugin.id, 1.103 + aPlugin.name, 1.104 + aPlugin.description, 1.105 + aPlugin.tags); 1.106 + }, 1.107 + 1.108 + /** 1.109 + * Called to get an Addon with a particular ID. 1.110 + * 1.111 + * @param aId 1.112 + * The ID of the add-on to retrieve 1.113 + * @param aCallback 1.114 + * A callback to pass the Addon to 1.115 + */ 1.116 + getAddonByID: function PL_getAddon(aId, aCallback) { 1.117 + if (!this.plugins) 1.118 + this.buildPluginList(); 1.119 + 1.120 + if (aId in this.plugins) 1.121 + aCallback(this.buildWrapper(this.plugins[aId])); 1.122 + else 1.123 + aCallback(null); 1.124 + }, 1.125 + 1.126 + /** 1.127 + * Called to get Addons of a particular type. 1.128 + * 1.129 + * @param aTypes 1.130 + * An array of types to fetch. Can be null to get all types. 1.131 + * @param callback 1.132 + * A callback to pass an array of Addons to 1.133 + */ 1.134 + getAddonsByTypes: function PL_getAddonsByTypes(aTypes, aCallback) { 1.135 + if (aTypes && aTypes.indexOf("plugin") < 0) { 1.136 + aCallback([]); 1.137 + return; 1.138 + } 1.139 + 1.140 + if (!this.plugins) 1.141 + this.buildPluginList(); 1.142 + 1.143 + let results = []; 1.144 + 1.145 + for (let id in this.plugins) { 1.146 + this.getAddonByID(id, function(aAddon) { 1.147 + results.push(aAddon); 1.148 + }); 1.149 + } 1.150 + 1.151 + aCallback(results); 1.152 + }, 1.153 + 1.154 + /** 1.155 + * Called to get Addons that have pending operations. 1.156 + * 1.157 + * @param aTypes 1.158 + * An array of types to fetch. Can be null to get all types 1.159 + * @param aCallback 1.160 + * A callback to pass an array of Addons to 1.161 + */ 1.162 + getAddonsWithOperationsByTypes: function PL_getAddonsWithOperationsByTypes(aTypes, aCallback) { 1.163 + aCallback([]); 1.164 + }, 1.165 + 1.166 + /** 1.167 + * Called to get the current AddonInstalls, optionally restricting by type. 1.168 + * 1.169 + * @param aTypes 1.170 + * An array of types or null to get all types 1.171 + * @param aCallback 1.172 + * A callback to pass the array of AddonInstalls to 1.173 + */ 1.174 + getInstallsByTypes: function PL_getInstallsByTypes(aTypes, aCallback) { 1.175 + aCallback([]); 1.176 + }, 1.177 + 1.178 + /** 1.179 + * Builds a list of the current plugins reported by the plugin host 1.180 + * 1.181 + * @return a dictionary of plugins indexed by our generated ID 1.182 + */ 1.183 + getPluginList: function PL_getPluginList() { 1.184 + let tags = Cc["@mozilla.org/plugin/host;1"]. 1.185 + getService(Ci.nsIPluginHost). 1.186 + getPluginTags({}); 1.187 + 1.188 + let list = {}; 1.189 + let seenPlugins = {}; 1.190 + for (let tag of tags) { 1.191 + if (!(tag.name in seenPlugins)) 1.192 + seenPlugins[tag.name] = {}; 1.193 + if (!(tag.description in seenPlugins[tag.name])) { 1.194 + let plugin = { 1.195 + id: getIDHashForString(tag.name + tag.description), 1.196 + name: tag.name, 1.197 + description: tag.description, 1.198 + tags: [tag] 1.199 + }; 1.200 + 1.201 + seenPlugins[tag.name][tag.description] = plugin; 1.202 + list[plugin.id] = plugin; 1.203 + } 1.204 + else { 1.205 + seenPlugins[tag.name][tag.description].tags.push(tag); 1.206 + } 1.207 + } 1.208 + 1.209 + return list; 1.210 + }, 1.211 + 1.212 + /** 1.213 + * Builds the list of known plugins from the plugin host 1.214 + */ 1.215 + buildPluginList: function PL_buildPluginList() { 1.216 + this.plugins = this.getPluginList(); 1.217 + }, 1.218 + 1.219 + /** 1.220 + * Updates the plugins from the plugin host by comparing the current plugins 1.221 + * to the last known list sending out any necessary API notifications for 1.222 + * changes. 1.223 + */ 1.224 + updatePluginList: function PL_updatePluginList() { 1.225 + let newList = this.getPluginList(); 1.226 + 1.227 + let lostPlugins = [this.buildWrapper(this.plugins[id]) 1.228 + for each (id in Object.keys(this.plugins)) if (!(id in newList))]; 1.229 + let newPlugins = [this.buildWrapper(newList[id]) 1.230 + for each (id in Object.keys(newList)) if (!(id in this.plugins))]; 1.231 + let matchedIDs = [id for each (id in Object.keys(newList)) if (id in this.plugins)]; 1.232 + 1.233 + // The plugin host generates new tags for every plugin after a scan and 1.234 + // if the plugin's filename has changed then the disabled state won't have 1.235 + // been carried across, send out notifications for anything that has 1.236 + // changed (see bug 830267). 1.237 + let changedWrappers = []; 1.238 + for (let id of matchedIDs) { 1.239 + let oldWrapper = this.buildWrapper(this.plugins[id]); 1.240 + let newWrapper = this.buildWrapper(newList[id]); 1.241 + 1.242 + if (newWrapper.isActive != oldWrapper.isActive) { 1.243 + AddonManagerPrivate.callAddonListeners(newWrapper.isActive ? 1.244 + "onEnabling" : "onDisabling", 1.245 + newWrapper, false); 1.246 + changedWrappers.push(newWrapper); 1.247 + } 1.248 + } 1.249 + 1.250 + // Notify about new installs 1.251 + for (let plugin of newPlugins) { 1.252 + AddonManagerPrivate.callInstallListeners("onExternalInstall", null, 1.253 + plugin, null, false); 1.254 + AddonManagerPrivate.callAddonListeners("onInstalling", plugin, false); 1.255 + } 1.256 + 1.257 + // Notify for any plugins that have vanished. 1.258 + for (let plugin of lostPlugins) 1.259 + AddonManagerPrivate.callAddonListeners("onUninstalling", plugin, false); 1.260 + 1.261 + this.plugins = newList; 1.262 + 1.263 + // Signal that new installs are complete 1.264 + for (let plugin of newPlugins) 1.265 + AddonManagerPrivate.callAddonListeners("onInstalled", plugin); 1.266 + 1.267 + // Signal that enables/disables are complete 1.268 + for (let wrapper of changedWrappers) { 1.269 + AddonManagerPrivate.callAddonListeners(wrapper.isActive ? 1.270 + "onEnabled" : "onDisabled", 1.271 + wrapper); 1.272 + } 1.273 + 1.274 + // Signal that uninstalls are complete 1.275 + for (let plugin of lostPlugins) 1.276 + AddonManagerPrivate.callAddonListeners("onUninstalled", plugin); 1.277 + } 1.278 +}; 1.279 + 1.280 +/** 1.281 + * The PluginWrapper wraps a set of nsIPluginTags to provide the data visible to 1.282 + * public callers through the API. 1.283 + */ 1.284 +function PluginWrapper(aId, aName, aDescription, aTags) { 1.285 + let safedesc = aDescription.replace(/<\/?[a-z][^>]*>/gi, " "); 1.286 + let homepageURL = null; 1.287 + if (/<A\s+HREF=[^>]*>/i.test(aDescription)) 1.288 + homepageURL = /<A\s+HREF=["']?([^>"'\s]*)/i.exec(aDescription)[1]; 1.289 + 1.290 + this.__defineGetter__("id", function() aId); 1.291 + this.__defineGetter__("type", function() "plugin"); 1.292 + this.__defineGetter__("name", function() aName); 1.293 + this.__defineGetter__("creator", function() null); 1.294 + this.__defineGetter__("description", function() safedesc); 1.295 + this.__defineGetter__("version", function() aTags[0].version); 1.296 + this.__defineGetter__("homepageURL", function() homepageURL); 1.297 + 1.298 + this.__defineGetter__("isActive", function() !aTags[0].blocklisted && !aTags[0].disabled); 1.299 + this.__defineGetter__("appDisabled", function() aTags[0].blocklisted); 1.300 + 1.301 + this.__defineGetter__("userDisabled", function() { 1.302 + if (aTags[0].disabled) 1.303 + return true; 1.304 + 1.305 + if ((Services.prefs.getBoolPref("plugins.click_to_play") && aTags[0].clicktoplay) || 1.306 + this.blocklistState == Ci.nsIBlocklistService.STATE_VULNERABLE_UPDATE_AVAILABLE || 1.307 + this.blocklistState == Ci.nsIBlocklistService.STATE_VULNERABLE_NO_UPDATE) 1.308 + return AddonManager.STATE_ASK_TO_ACTIVATE; 1.309 + 1.310 + return false; 1.311 + }); 1.312 + 1.313 + this.__defineSetter__("userDisabled", function(aVal) { 1.314 + let previousVal = this.userDisabled; 1.315 + if (aVal === previousVal) 1.316 + return aVal; 1.317 + 1.318 + for (let tag of aTags) { 1.319 + if (aVal === true) 1.320 + tag.enabledState = Ci.nsIPluginTag.STATE_DISABLED; 1.321 + else if (aVal === false) 1.322 + tag.enabledState = Ci.nsIPluginTag.STATE_ENABLED; 1.323 + else if (aVal == AddonManager.STATE_ASK_TO_ACTIVATE) 1.324 + tag.enabledState = Ci.nsIPluginTag.STATE_CLICKTOPLAY; 1.325 + } 1.326 + 1.327 + // If 'userDisabled' was 'true' and we're going to a state that's not 1.328 + // that, we're enabling, so call those listeners. 1.329 + if (previousVal === true && aVal !== true) { 1.330 + AddonManagerPrivate.callAddonListeners("onEnabling", this, false); 1.331 + AddonManagerPrivate.callAddonListeners("onEnabled", this); 1.332 + } 1.333 + 1.334 + // If 'userDisabled' was not 'true' and we're going to a state where 1.335 + // it is, we're disabling, so call those listeners. 1.336 + if (previousVal !== true && aVal === true) { 1.337 + AddonManagerPrivate.callAddonListeners("onDisabling", this, false); 1.338 + AddonManagerPrivate.callAddonListeners("onDisabled", this); 1.339 + } 1.340 + 1.341 + // If the 'userDisabled' value involved AddonManager.STATE_ASK_TO_ACTIVATE, 1.342 + // call the onPropertyChanged listeners. 1.343 + if (previousVal == AddonManager.STATE_ASK_TO_ACTIVATE || 1.344 + aVal == AddonManager.STATE_ASK_TO_ACTIVATE) { 1.345 + AddonManagerPrivate.callAddonListeners("onPropertyChanged", this, ["userDisabled"]); 1.346 + } 1.347 + 1.348 + return aVal; 1.349 + }); 1.350 + 1.351 + 1.352 + this.__defineGetter__("blocklistState", function() { 1.353 + let bs = Cc["@mozilla.org/extensions/blocklist;1"]. 1.354 + getService(Ci.nsIBlocklistService); 1.355 + return bs.getPluginBlocklistState(aTags[0]); 1.356 + }); 1.357 + 1.358 + this.__defineGetter__("blocklistURL", function() { 1.359 + let bs = Cc["@mozilla.org/extensions/blocklist;1"]. 1.360 + getService(Ci.nsIBlocklistService); 1.361 + return bs.getPluginBlocklistURL(aTags[0]); 1.362 + }); 1.363 + 1.364 + this.__defineGetter__("size", function() { 1.365 + function getDirectorySize(aFile) { 1.366 + let size = 0; 1.367 + let entries = aFile.directoryEntries.QueryInterface(Ci.nsIDirectoryEnumerator); 1.368 + let entry; 1.369 + while ((entry = entries.nextFile)) { 1.370 + if (entry.isSymlink() || !entry.isDirectory()) 1.371 + size += entry.fileSize; 1.372 + else 1.373 + size += getDirectorySize(entry); 1.374 + } 1.375 + entries.close(); 1.376 + return size; 1.377 + } 1.378 + 1.379 + let size = 0; 1.380 + let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); 1.381 + for (let tag of aTags) { 1.382 + file.initWithPath(tag.fullpath); 1.383 + if (file.isDirectory()) 1.384 + size += getDirectorySize(file); 1.385 + else 1.386 + size += file.fileSize; 1.387 + } 1.388 + return size; 1.389 + }); 1.390 + 1.391 + this.__defineGetter__("pluginLibraries", function() { 1.392 + let libs = []; 1.393 + for (let tag of aTags) 1.394 + libs.push(tag.filename); 1.395 + return libs; 1.396 + }); 1.397 + 1.398 + this.__defineGetter__("pluginFullpath", function() { 1.399 + let paths = []; 1.400 + for (let tag of aTags) 1.401 + paths.push(tag.fullpath); 1.402 + return paths; 1.403 + }) 1.404 + 1.405 + this.__defineGetter__("pluginMimeTypes", function() { 1.406 + let types = []; 1.407 + for (let tag of aTags) { 1.408 + let mimeTypes = tag.getMimeTypes({}); 1.409 + let mimeDescriptions = tag.getMimeDescriptions({}); 1.410 + let extensions = tag.getExtensions({}); 1.411 + for (let i = 0; i < mimeTypes.length; i++) { 1.412 + let type = {}; 1.413 + type.type = mimeTypes[i]; 1.414 + type.description = mimeDescriptions[i]; 1.415 + type.suffixes = extensions[i]; 1.416 + 1.417 + types.push(type); 1.418 + } 1.419 + } 1.420 + return types; 1.421 + }); 1.422 + 1.423 + this.__defineGetter__("installDate", function() { 1.424 + let date = 0; 1.425 + for (let tag of aTags) { 1.426 + date = Math.max(date, tag.lastModifiedTime); 1.427 + } 1.428 + return new Date(date); 1.429 + }); 1.430 + 1.431 + this.__defineGetter__("scope", function() { 1.432 + let path = aTags[0].fullpath; 1.433 + // Plugins inside the application directory are in the application scope 1.434 + let dir = Services.dirsvc.get("APlugns", Ci.nsIFile); 1.435 + if (path.startsWith(dir.path)) 1.436 + return AddonManager.SCOPE_APPLICATION; 1.437 + 1.438 + // Plugins inside the profile directory are in the profile scope 1.439 + dir = Services.dirsvc.get("ProfD", Ci.nsIFile); 1.440 + if (path.startsWith(dir.path)) 1.441 + return AddonManager.SCOPE_PROFILE; 1.442 + 1.443 + // Plugins anywhere else in the user's home are in the user scope, 1.444 + // but not all platforms have a home directory. 1.445 + try { 1.446 + dir = Services.dirsvc.get("Home", Ci.nsIFile); 1.447 + if (path.startsWith(dir.path)) 1.448 + return AddonManager.SCOPE_USER; 1.449 + } catch (e if (e.result && e.result == Components.results.NS_ERROR_FAILURE)) { 1.450 + // Do nothing: missing "Home". 1.451 + } 1.452 + 1.453 + // Any other locations are system scope 1.454 + return AddonManager.SCOPE_SYSTEM; 1.455 + }); 1.456 + 1.457 + this.__defineGetter__("pendingOperations", function() { 1.458 + return AddonManager.PENDING_NONE; 1.459 + }); 1.460 + 1.461 + this.__defineGetter__("operationsRequiringRestart", function() { 1.462 + return AddonManager.OP_NEEDS_RESTART_NONE; 1.463 + }); 1.464 + 1.465 + this.__defineGetter__("permissions", function() { 1.466 + let permissions = 0; 1.467 + if (!this.appDisabled) { 1.468 + 1.469 + if (this.userDisabled !== true) 1.470 + permissions |= AddonManager.PERM_CAN_DISABLE; 1.471 + 1.472 + let blocklistState = this.blocklistState; 1.473 + let isCTPBlocklisted = 1.474 + (blocklistState == Ci.nsIBlocklistService.STATE_VULNERABLE_NO_UPDATE || 1.475 + blocklistState == Ci.nsIBlocklistService.STATE_VULNERABLE_UPDATE_AVAILABLE); 1.476 + 1.477 + if (this.userDisabled !== AddonManager.STATE_ASK_TO_ACTIVATE && 1.478 + (Services.prefs.getBoolPref("plugins.click_to_play") || 1.479 + isCTPBlocklisted)) { 1.480 + permissions |= AddonManager.PERM_CAN_ASK_TO_ACTIVATE; 1.481 + } 1.482 + 1.483 + if (this.userDisabled !== false && !isCTPBlocklisted) { 1.484 + permissions |= AddonManager.PERM_CAN_ENABLE; 1.485 + } 1.486 + } 1.487 + return permissions; 1.488 + }); 1.489 +} 1.490 + 1.491 +PluginWrapper.prototype = { 1.492 + optionsType: AddonManager.OPTIONS_TYPE_INLINE_INFO, 1.493 + optionsURL: "chrome://mozapps/content/extensions/pluginPrefs.xul", 1.494 + 1.495 + get updateDate() { 1.496 + return this.installDate; 1.497 + }, 1.498 + 1.499 + get isCompatible() { 1.500 + return true; 1.501 + }, 1.502 + 1.503 + get isPlatformCompatible() { 1.504 + return true; 1.505 + }, 1.506 + 1.507 + get providesUpdatesSecurely() { 1.508 + return true; 1.509 + }, 1.510 + 1.511 + get foreignInstall() { 1.512 + return true; 1.513 + }, 1.514 + 1.515 + isCompatibleWith: function(aAppVerison, aPlatformVersion) { 1.516 + return true; 1.517 + }, 1.518 + 1.519 + findUpdates: function(aListener, aReason, aAppVersion, aPlatformVersion) { 1.520 + if ("onNoCompatibilityUpdateAvailable" in aListener) 1.521 + aListener.onNoCompatibilityUpdateAvailable(this); 1.522 + if ("onNoUpdateAvailable" in aListener) 1.523 + aListener.onNoUpdateAvailable(this); 1.524 + if ("onUpdateFinished" in aListener) 1.525 + aListener.onUpdateFinished(this); 1.526 + } 1.527 +}; 1.528 + 1.529 +AddonManagerPrivate.registerProvider(PluginProvider, [ 1.530 + new AddonManagerPrivate.AddonType("plugin", URI_EXTENSION_STRINGS, 1.531 + STRING_TYPE_NAME, 1.532 + AddonManager.VIEW_TYPE_LIST, 6000, 1.533 + AddonManager.TYPE_SUPPORTS_ASK_TO_ACTIVATE) 1.534 +]);