|
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 ]); |