toolkit/mozapps/extensions/internal/PluginProvider.jsm

changeset 0
6474c204b198
equal deleted inserted replaced
-1:000000000000 0:353328e6a38a
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5 "use strict";
6
7 const Cc = Components.classes;
8 const Ci = Components.interfaces;
9 const Cu = Components.utils;
10
11 this.EXPORTED_SYMBOLS = [];
12
13 Cu.import("resource://gre/modules/AddonManager.jsm");
14 Cu.import("resource://gre/modules/Services.jsm");
15
16 const URI_EXTENSION_STRINGS = "chrome://mozapps/locale/extensions/extensions.properties";
17 const STRING_TYPE_NAME = "type.%ID%.name";
18 const LIST_UPDATED_TOPIC = "plugins-list-updated";
19
20 Cu.import("resource://gre/modules/Log.jsm");
21 const LOGGER_ID = "addons.plugins";
22
23 // Create a new logger for use by the Addons Plugin Provider
24 // (Requires AddonManager.jsm)
25 let logger = Log.repository.getLogger(LOGGER_ID);
26
27 function getIDHashForString(aStr) {
28 // return the two-digit hexadecimal code for a byte
29 function toHexString(charCode)
30 ("0" + charCode.toString(16)).slice(-2);
31
32 let hasher = Cc["@mozilla.org/security/hash;1"].
33 createInstance(Ci.nsICryptoHash);
34 hasher.init(Ci.nsICryptoHash.MD5);
35 let stringStream = Cc["@mozilla.org/io/string-input-stream;1"].
36 createInstance(Ci.nsIStringInputStream);
37 stringStream.data = aStr ? aStr : "null";
38 hasher.updateFromStream(stringStream, -1);
39
40 // convert the binary hash data to a hex string.
41 let binary = hasher.finish(false);
42 let hash = [toHexString(binary.charCodeAt(i)) for (i in binary)].join("").toLowerCase();
43 return "{" + hash.substr(0, 8) + "-" +
44 hash.substr(8, 4) + "-" +
45 hash.substr(12, 4) + "-" +
46 hash.substr(16, 4) + "-" +
47 hash.substr(20) + "}";
48 }
49
50 var PluginProvider = {
51 // A dictionary mapping IDs to names and descriptions
52 plugins: null,
53
54 startup: function PL_startup() {
55 Services.obs.addObserver(this, LIST_UPDATED_TOPIC, false);
56 Services.obs.addObserver(this, AddonManager.OPTIONS_NOTIFICATION_DISPLAYED, false);
57 },
58
59 /**
60 * Called when the application is shutting down. Only necessary for tests
61 * to be able to simulate a shutdown.
62 */
63 shutdown: function PL_shutdown() {
64 this.plugins = null;
65 Services.obs.removeObserver(this, AddonManager.OPTIONS_NOTIFICATION_DISPLAYED);
66 Services.obs.removeObserver(this, LIST_UPDATED_TOPIC);
67 },
68
69 observe: function(aSubject, aTopic, aData) {
70 switch (aTopic) {
71 case AddonManager.OPTIONS_NOTIFICATION_DISPLAYED:
72 this.getAddonByID(aData, function PL_displayPluginInfo(plugin) {
73 if (!plugin)
74 return;
75
76 let libLabel = aSubject.getElementById("pluginLibraries");
77 libLabel.textContent = plugin.pluginLibraries.join(", ");
78
79 let typeLabel = aSubject.getElementById("pluginMimeTypes"), types = [];
80 for (let type of plugin.pluginMimeTypes) {
81 let extras = [type.description.trim(), type.suffixes].
82 filter(function(x) x).join(": ");
83 types.push(type.type + (extras ? " (" + extras + ")" : ""));
84 }
85 typeLabel.textContent = types.join(",\n");
86 });
87 break;
88 case LIST_UPDATED_TOPIC:
89 if (this.plugins)
90 this.updatePluginList();
91 break;
92 }
93 },
94
95 /**
96 * Creates a PluginWrapper for a plugin object.
97 */
98 buildWrapper: function PL_buildWrapper(aPlugin) {
99 return new PluginWrapper(aPlugin.id,
100 aPlugin.name,
101 aPlugin.description,
102 aPlugin.tags);
103 },
104
105 /**
106 * Called to get an Addon with a particular ID.
107 *
108 * @param aId
109 * The ID of the add-on to retrieve
110 * @param aCallback
111 * A callback to pass the Addon to
112 */
113 getAddonByID: function PL_getAddon(aId, aCallback) {
114 if (!this.plugins)
115 this.buildPluginList();
116
117 if (aId in this.plugins)
118 aCallback(this.buildWrapper(this.plugins[aId]));
119 else
120 aCallback(null);
121 },
122
123 /**
124 * Called to get Addons of a particular type.
125 *
126 * @param aTypes
127 * An array of types to fetch. Can be null to get all types.
128 * @param callback
129 * A callback to pass an array of Addons to
130 */
131 getAddonsByTypes: function PL_getAddonsByTypes(aTypes, aCallback) {
132 if (aTypes && aTypes.indexOf("plugin") < 0) {
133 aCallback([]);
134 return;
135 }
136
137 if (!this.plugins)
138 this.buildPluginList();
139
140 let results = [];
141
142 for (let id in this.plugins) {
143 this.getAddonByID(id, function(aAddon) {
144 results.push(aAddon);
145 });
146 }
147
148 aCallback(results);
149 },
150
151 /**
152 * Called to get Addons that have pending operations.
153 *
154 * @param aTypes
155 * An array of types to fetch. Can be null to get all types
156 * @param aCallback
157 * A callback to pass an array of Addons to
158 */
159 getAddonsWithOperationsByTypes: function PL_getAddonsWithOperationsByTypes(aTypes, aCallback) {
160 aCallback([]);
161 },
162
163 /**
164 * Called to get the current AddonInstalls, optionally restricting by type.
165 *
166 * @param aTypes
167 * An array of types or null to get all types
168 * @param aCallback
169 * A callback to pass the array of AddonInstalls to
170 */
171 getInstallsByTypes: function PL_getInstallsByTypes(aTypes, aCallback) {
172 aCallback([]);
173 },
174
175 /**
176 * Builds a list of the current plugins reported by the plugin host
177 *
178 * @return a dictionary of plugins indexed by our generated ID
179 */
180 getPluginList: function PL_getPluginList() {
181 let tags = Cc["@mozilla.org/plugin/host;1"].
182 getService(Ci.nsIPluginHost).
183 getPluginTags({});
184
185 let list = {};
186 let seenPlugins = {};
187 for (let tag of tags) {
188 if (!(tag.name in seenPlugins))
189 seenPlugins[tag.name] = {};
190 if (!(tag.description in seenPlugins[tag.name])) {
191 let plugin = {
192 id: getIDHashForString(tag.name + tag.description),
193 name: tag.name,
194 description: tag.description,
195 tags: [tag]
196 };
197
198 seenPlugins[tag.name][tag.description] = plugin;
199 list[plugin.id] = plugin;
200 }
201 else {
202 seenPlugins[tag.name][tag.description].tags.push(tag);
203 }
204 }
205
206 return list;
207 },
208
209 /**
210 * Builds the list of known plugins from the plugin host
211 */
212 buildPluginList: function PL_buildPluginList() {
213 this.plugins = this.getPluginList();
214 },
215
216 /**
217 * Updates the plugins from the plugin host by comparing the current plugins
218 * to the last known list sending out any necessary API notifications for
219 * changes.
220 */
221 updatePluginList: function PL_updatePluginList() {
222 let newList = this.getPluginList();
223
224 let lostPlugins = [this.buildWrapper(this.plugins[id])
225 for each (id in Object.keys(this.plugins)) if (!(id in newList))];
226 let newPlugins = [this.buildWrapper(newList[id])
227 for each (id in Object.keys(newList)) if (!(id in this.plugins))];
228 let matchedIDs = [id for each (id in Object.keys(newList)) if (id in this.plugins)];
229
230 // The plugin host generates new tags for every plugin after a scan and
231 // if the plugin's filename has changed then the disabled state won't have
232 // been carried across, send out notifications for anything that has
233 // changed (see bug 830267).
234 let changedWrappers = [];
235 for (let id of matchedIDs) {
236 let oldWrapper = this.buildWrapper(this.plugins[id]);
237 let newWrapper = this.buildWrapper(newList[id]);
238
239 if (newWrapper.isActive != oldWrapper.isActive) {
240 AddonManagerPrivate.callAddonListeners(newWrapper.isActive ?
241 "onEnabling" : "onDisabling",
242 newWrapper, false);
243 changedWrappers.push(newWrapper);
244 }
245 }
246
247 // Notify about new installs
248 for (let plugin of newPlugins) {
249 AddonManagerPrivate.callInstallListeners("onExternalInstall", null,
250 plugin, null, false);
251 AddonManagerPrivate.callAddonListeners("onInstalling", plugin, false);
252 }
253
254 // Notify for any plugins that have vanished.
255 for (let plugin of lostPlugins)
256 AddonManagerPrivate.callAddonListeners("onUninstalling", plugin, false);
257
258 this.plugins = newList;
259
260 // Signal that new installs are complete
261 for (let plugin of newPlugins)
262 AddonManagerPrivate.callAddonListeners("onInstalled", plugin);
263
264 // Signal that enables/disables are complete
265 for (let wrapper of changedWrappers) {
266 AddonManagerPrivate.callAddonListeners(wrapper.isActive ?
267 "onEnabled" : "onDisabled",
268 wrapper);
269 }
270
271 // Signal that uninstalls are complete
272 for (let plugin of lostPlugins)
273 AddonManagerPrivate.callAddonListeners("onUninstalled", plugin);
274 }
275 };
276
277 /**
278 * The PluginWrapper wraps a set of nsIPluginTags to provide the data visible to
279 * public callers through the API.
280 */
281 function PluginWrapper(aId, aName, aDescription, aTags) {
282 let safedesc = aDescription.replace(/<\/?[a-z][^>]*>/gi, " ");
283 let homepageURL = null;
284 if (/<A\s+HREF=[^>]*>/i.test(aDescription))
285 homepageURL = /<A\s+HREF=["']?([^>"'\s]*)/i.exec(aDescription)[1];
286
287 this.__defineGetter__("id", function() aId);
288 this.__defineGetter__("type", function() "plugin");
289 this.__defineGetter__("name", function() aName);
290 this.__defineGetter__("creator", function() null);
291 this.__defineGetter__("description", function() safedesc);
292 this.__defineGetter__("version", function() aTags[0].version);
293 this.__defineGetter__("homepageURL", function() homepageURL);
294
295 this.__defineGetter__("isActive", function() !aTags[0].blocklisted && !aTags[0].disabled);
296 this.__defineGetter__("appDisabled", function() aTags[0].blocklisted);
297
298 this.__defineGetter__("userDisabled", function() {
299 if (aTags[0].disabled)
300 return true;
301
302 if ((Services.prefs.getBoolPref("plugins.click_to_play") && aTags[0].clicktoplay) ||
303 this.blocklistState == Ci.nsIBlocklistService.STATE_VULNERABLE_UPDATE_AVAILABLE ||
304 this.blocklistState == Ci.nsIBlocklistService.STATE_VULNERABLE_NO_UPDATE)
305 return AddonManager.STATE_ASK_TO_ACTIVATE;
306
307 return false;
308 });
309
310 this.__defineSetter__("userDisabled", function(aVal) {
311 let previousVal = this.userDisabled;
312 if (aVal === previousVal)
313 return aVal;
314
315 for (let tag of aTags) {
316 if (aVal === true)
317 tag.enabledState = Ci.nsIPluginTag.STATE_DISABLED;
318 else if (aVal === false)
319 tag.enabledState = Ci.nsIPluginTag.STATE_ENABLED;
320 else if (aVal == AddonManager.STATE_ASK_TO_ACTIVATE)
321 tag.enabledState = Ci.nsIPluginTag.STATE_CLICKTOPLAY;
322 }
323
324 // If 'userDisabled' was 'true' and we're going to a state that's not
325 // that, we're enabling, so call those listeners.
326 if (previousVal === true && aVal !== true) {
327 AddonManagerPrivate.callAddonListeners("onEnabling", this, false);
328 AddonManagerPrivate.callAddonListeners("onEnabled", this);
329 }
330
331 // If 'userDisabled' was not 'true' and we're going to a state where
332 // it is, we're disabling, so call those listeners.
333 if (previousVal !== true && aVal === true) {
334 AddonManagerPrivate.callAddonListeners("onDisabling", this, false);
335 AddonManagerPrivate.callAddonListeners("onDisabled", this);
336 }
337
338 // If the 'userDisabled' value involved AddonManager.STATE_ASK_TO_ACTIVATE,
339 // call the onPropertyChanged listeners.
340 if (previousVal == AddonManager.STATE_ASK_TO_ACTIVATE ||
341 aVal == AddonManager.STATE_ASK_TO_ACTIVATE) {
342 AddonManagerPrivate.callAddonListeners("onPropertyChanged", this, ["userDisabled"]);
343 }
344
345 return aVal;
346 });
347
348
349 this.__defineGetter__("blocklistState", function() {
350 let bs = Cc["@mozilla.org/extensions/blocklist;1"].
351 getService(Ci.nsIBlocklistService);
352 return bs.getPluginBlocklistState(aTags[0]);
353 });
354
355 this.__defineGetter__("blocklistURL", function() {
356 let bs = Cc["@mozilla.org/extensions/blocklist;1"].
357 getService(Ci.nsIBlocklistService);
358 return bs.getPluginBlocklistURL(aTags[0]);
359 });
360
361 this.__defineGetter__("size", function() {
362 function getDirectorySize(aFile) {
363 let size = 0;
364 let entries = aFile.directoryEntries.QueryInterface(Ci.nsIDirectoryEnumerator);
365 let entry;
366 while ((entry = entries.nextFile)) {
367 if (entry.isSymlink() || !entry.isDirectory())
368 size += entry.fileSize;
369 else
370 size += getDirectorySize(entry);
371 }
372 entries.close();
373 return size;
374 }
375
376 let size = 0;
377 let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
378 for (let tag of aTags) {
379 file.initWithPath(tag.fullpath);
380 if (file.isDirectory())
381 size += getDirectorySize(file);
382 else
383 size += file.fileSize;
384 }
385 return size;
386 });
387
388 this.__defineGetter__("pluginLibraries", function() {
389 let libs = [];
390 for (let tag of aTags)
391 libs.push(tag.filename);
392 return libs;
393 });
394
395 this.__defineGetter__("pluginFullpath", function() {
396 let paths = [];
397 for (let tag of aTags)
398 paths.push(tag.fullpath);
399 return paths;
400 })
401
402 this.__defineGetter__("pluginMimeTypes", function() {
403 let types = [];
404 for (let tag of aTags) {
405 let mimeTypes = tag.getMimeTypes({});
406 let mimeDescriptions = tag.getMimeDescriptions({});
407 let extensions = tag.getExtensions({});
408 for (let i = 0; i < mimeTypes.length; i++) {
409 let type = {};
410 type.type = mimeTypes[i];
411 type.description = mimeDescriptions[i];
412 type.suffixes = extensions[i];
413
414 types.push(type);
415 }
416 }
417 return types;
418 });
419
420 this.__defineGetter__("installDate", function() {
421 let date = 0;
422 for (let tag of aTags) {
423 date = Math.max(date, tag.lastModifiedTime);
424 }
425 return new Date(date);
426 });
427
428 this.__defineGetter__("scope", function() {
429 let path = aTags[0].fullpath;
430 // Plugins inside the application directory are in the application scope
431 let dir = Services.dirsvc.get("APlugns", Ci.nsIFile);
432 if (path.startsWith(dir.path))
433 return AddonManager.SCOPE_APPLICATION;
434
435 // Plugins inside the profile directory are in the profile scope
436 dir = Services.dirsvc.get("ProfD", Ci.nsIFile);
437 if (path.startsWith(dir.path))
438 return AddonManager.SCOPE_PROFILE;
439
440 // Plugins anywhere else in the user's home are in the user scope,
441 // but not all platforms have a home directory.
442 try {
443 dir = Services.dirsvc.get("Home", Ci.nsIFile);
444 if (path.startsWith(dir.path))
445 return AddonManager.SCOPE_USER;
446 } catch (e if (e.result && e.result == Components.results.NS_ERROR_FAILURE)) {
447 // Do nothing: missing "Home".
448 }
449
450 // Any other locations are system scope
451 return AddonManager.SCOPE_SYSTEM;
452 });
453
454 this.__defineGetter__("pendingOperations", function() {
455 return AddonManager.PENDING_NONE;
456 });
457
458 this.__defineGetter__("operationsRequiringRestart", function() {
459 return AddonManager.OP_NEEDS_RESTART_NONE;
460 });
461
462 this.__defineGetter__("permissions", function() {
463 let permissions = 0;
464 if (!this.appDisabled) {
465
466 if (this.userDisabled !== true)
467 permissions |= AddonManager.PERM_CAN_DISABLE;
468
469 let blocklistState = this.blocklistState;
470 let isCTPBlocklisted =
471 (blocklistState == Ci.nsIBlocklistService.STATE_VULNERABLE_NO_UPDATE ||
472 blocklistState == Ci.nsIBlocklistService.STATE_VULNERABLE_UPDATE_AVAILABLE);
473
474 if (this.userDisabled !== AddonManager.STATE_ASK_TO_ACTIVATE &&
475 (Services.prefs.getBoolPref("plugins.click_to_play") ||
476 isCTPBlocklisted)) {
477 permissions |= AddonManager.PERM_CAN_ASK_TO_ACTIVATE;
478 }
479
480 if (this.userDisabled !== false && !isCTPBlocklisted) {
481 permissions |= AddonManager.PERM_CAN_ENABLE;
482 }
483 }
484 return permissions;
485 });
486 }
487
488 PluginWrapper.prototype = {
489 optionsType: AddonManager.OPTIONS_TYPE_INLINE_INFO,
490 optionsURL: "chrome://mozapps/content/extensions/pluginPrefs.xul",
491
492 get updateDate() {
493 return this.installDate;
494 },
495
496 get isCompatible() {
497 return true;
498 },
499
500 get isPlatformCompatible() {
501 return true;
502 },
503
504 get providesUpdatesSecurely() {
505 return true;
506 },
507
508 get foreignInstall() {
509 return true;
510 },
511
512 isCompatibleWith: function(aAppVerison, aPlatformVersion) {
513 return true;
514 },
515
516 findUpdates: function(aListener, aReason, aAppVersion, aPlatformVersion) {
517 if ("onNoCompatibilityUpdateAvailable" in aListener)
518 aListener.onNoCompatibilityUpdateAvailable(this);
519 if ("onNoUpdateAvailable" in aListener)
520 aListener.onNoUpdateAvailable(this);
521 if ("onUpdateFinished" in aListener)
522 aListener.onUpdateFinished(this);
523 }
524 };
525
526 AddonManagerPrivate.registerProvider(PluginProvider, [
527 new AddonManagerPrivate.AddonType("plugin", URI_EXTENSION_STRINGS,
528 STRING_TYPE_NAME,
529 AddonManager.VIEW_TYPE_LIST, 6000,
530 AddonManager.TYPE_SUPPORTS_ASK_TO_ACTIVATE)
531 ]);

mercurial