CLOSED TREE: TraceMonkey merge head. (a=blockers)
[mozilla-central.git] / toolkit / mozapps / extensions / nsBlocklistService.js
blob09749f53142f3115568063eb5d3291c582cc3db4
1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /*
3 # ***** BEGIN LICENSE BLOCK *****
4 # Version: MPL 1.1/GPL 2.0/LGPL 2.1
6 # The contents of this file are subject to the Mozilla Public License Version
7 # 1.1 (the "License"); you may not use this file except in compliance with
8 # the License. You may obtain a copy of the License at
9 # http://www.mozilla.org/MPL/
11 # Software distributed under the License is distributed on an "AS IS" basis,
12 # WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
13 # for the specific language governing rights and limitations under the
14 # License.
16 # The Original Code is the Blocklist Service.
18 # The Initial Developer of the Original Code is
19 # Mozilla Corporation.
20 # Portions created by the Initial Developer are Copyright (C) 2007
21 # the Initial Developer. All Rights Reserved.
23 # Contributor(s):
24 #   Robert Strong <robert.bugzilla@gmail.com>
25 #   Michael Wu <flamingice@sourmilk.net>
26 #   Dave Townsend <dtownsend@oxymoronical.com>
28 # Alternatively, the contents of this file may be used under the terms of
29 # either the GNU General Public License Version 2 or later (the "GPL"), or
30 # the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
31 # in which case the provisions of the GPL or the LGPL are applicable instead
32 # of those above. If you wish to allow use of your version of this file only
33 # under the terms of either the GPL or the LGPL, and not to allow others to
34 # use your version of this file under the terms of the MPL, indicate your
35 # decision by deleting the provisions above and replace them with the notice
36 # and other provisions required by the GPL or the LGPL. If you do not delete
37 # the provisions above, a recipient may use your version of this file under
38 # the terms of any one of the MPL, the GPL or the LGPL.
40 # ***** END LICENSE BLOCK *****
43 const Cc = Components.classes;
44 const Ci = Components.interfaces;
45 const Cr = Components.results;
47 Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
48 Components.utils.import("resource://gre/modules/FileUtils.jsm");
49 Components.utils.import("resource://gre/modules/AddonManager.jsm");
50 Components.utils.import("resource://gre/modules/Services.jsm");
52 const TOOLKIT_ID                      = "toolkit@mozilla.org"
53 const KEY_PROFILEDIR                  = "ProfD";
54 const KEY_APPDIR                      = "XCurProcD";
55 const FILE_BLOCKLIST                  = "blocklist.xml";
56 const PREF_BLOCKLIST_LASTUPDATETIME   = "app.update.lastUpdateTime.blocklist-background-update-timer";
57 const PREF_BLOCKLIST_URL              = "extensions.blocklist.url";
58 const PREF_BLOCKLIST_ENABLED          = "extensions.blocklist.enabled";
59 const PREF_BLOCKLIST_INTERVAL         = "extensions.blocklist.interval";
60 const PREF_BLOCKLIST_LEVEL            = "extensions.blocklist.level";
61 const PREF_BLOCKLIST_PINGCOUNT        = "extensions.blocklist.pingCount";
62 const PREF_BLOCKLIST_TOTALPINGCOUNT   = "extensions.blocklist.totalPingCount";
63 const PREF_PLUGINS_NOTIFYUSER         = "plugins.update.notifyUser";
64 const PREF_GENERAL_USERAGENT_LOCALE   = "general.useragent.locale";
65 const PREF_PARTNER_BRANCH             = "app.partner.";
66 const PREF_APP_DISTRIBUTION           = "distribution.id";
67 const PREF_APP_DISTRIBUTION_VERSION   = "distribution.version";
68 const PREF_APP_UPDATE_CHANNEL         = "app.update.channel";
69 const PREF_EM_LOGGING_ENABLED         = "extensions.logging.enabled";
70 const XMLURI_BLOCKLIST                = "http://www.mozilla.org/2006/addons-blocklist";
71 const XMLURI_PARSE_ERROR              = "http://www.mozilla.org/newlayout/xml/parsererror.xml"
72 const UNKNOWN_XPCOM_ABI               = "unknownABI";
73 const URI_BLOCKLIST_DIALOG            = "chrome://mozapps/content/extensions/blocklist.xul"
74 const DEFAULT_SEVERITY                = 3;
75 const DEFAULT_LEVEL                   = 2;
76 const MAX_BLOCK_LEVEL                 = 3;
77 const SEVERITY_OUTDATED               = 0;
79 var gLoggingEnabled = null;
80 var gBlocklistEnabled = true;
81 var gBlocklistLevel = DEFAULT_LEVEL;
83 XPCOMUtils.defineLazyServiceGetter(this, "gConsole",
84                                    "@mozilla.org/consoleservice;1",
85                                    "nsIConsoleService");
87 XPCOMUtils.defineLazyServiceGetter(this, "gVersionChecker",
88                                    "@mozilla.org/xpcom/version-comparator;1",
89                                    "nsIVersionComparator");
91 XPCOMUtils.defineLazyGetter(this, "gPref", function bls_gPref() {
92   return Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefService).
93          QueryInterface(Ci.nsIPrefBranch2);
94 });
96 XPCOMUtils.defineLazyGetter(this, "gApp", function bls_gApp() {
97   return Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULAppInfo).
98          QueryInterface(Ci.nsIXULRuntime);
99 });
101 XPCOMUtils.defineLazyGetter(this, "gABI", function bls_gABI() {
102   let abi = null;
103   try {
104     abi = gApp.XPCOMABI;
105   }
106   catch (e) {
107     LOG("BlockList Global gABI: XPCOM ABI unknown.");
108   }
109 #ifdef XP_MACOSX
110   // Mac universal build should report a different ABI than either macppc
111   // or mactel.
112   let macutils = Cc["@mozilla.org/xpcom/mac-utils;1"].
113                  getService(Ci.nsIMacUtils);
115   if (macutils.isUniversalBinary)
116     abi += "-u-" + macutils.architecturesInBinary;
117 #endif
118   return abi;
121 XPCOMUtils.defineLazyGetter(this, "gOSVersion", function bls_gOSVersion() {
122   let osVersion;
123   let sysInfo = Cc["@mozilla.org/system-info;1"].
124                 getService(Ci.nsIPropertyBag2);
125   try {
126     osVersion = sysInfo.getProperty("name") + " " + sysInfo.getProperty("version");
127   }
128   catch (e) {
129     LOG("BlockList Global gOSVersion: OS Version unknown.");
130   }
132   if (osVersion) {
133     try {
134       osVersion += " (" + sysInfo.getProperty("secondaryLibrary") + ")";
135     }
136     catch (e) {
137       // Not all platforms have a secondary widget library, so an error is nothing to worry about.
138     }
139     osVersion = encodeURIComponent(osVersion);
140   }
141   return osVersion;
144 // shared code for suppressing bad cert dialogs
145 XPCOMUtils.defineLazyGetter(this, "gCertUtils", function bls_gCertUtils() {
146   let temp = { };
147   Components.utils.import("resource://gre/modules/CertUtils.jsm", temp);
148   return temp;
151 function getObserverService() {
152   return Cc["@mozilla.org/observer-service;1"].getService(Ci.nsIObserverService);
156  * Logs a string to the error console.
157  * @param   string
158  *          The string to write to the error console..
159  */
160 function LOG(string) {
161   if (gLoggingEnabled) {
162     dump("*** " + string + "\n");
163     gConsole.logStringMessage(string);
164   }
168  * Gets a preference value, handling the case where there is no default.
169  * @param   func
170  *          The name of the preference function to call, on nsIPrefBranch
171  * @param   preference
172  *          The name of the preference
173  * @param   defaultValue
174  *          The default value to return in the event the preference has
175  *          no setting
176  * @returns The value of the preference, or undefined if there was no
177  *          user or default value.
178  */
179 function getPref(func, preference, defaultValue) {
180   try {
181     return gPref[func](preference);
182   }
183   catch (e) {
184   }
185   return defaultValue;
189  * Constructs a URI to a spec.
190  * @param   spec
191  *          The spec to construct a URI to
192  * @returns The nsIURI constructed.
193  */
194 function newURI(spec) {
195   var ioServ = Cc["@mozilla.org/network/io-service;1"].
196                getService(Ci.nsIIOService);
197   return ioServ.newURI(spec, null, null);
200 // Restarts the application checking in with observers first
201 function restartApp() {
202   // Notify all windows that an application quit has been requested.
203   var os = Cc["@mozilla.org/observer-service;1"].
204            getService(Ci.nsIObserverService);
205   var cancelQuit = Cc["@mozilla.org/supports-PRBool;1"].
206                    createInstance(Ci.nsISupportsPRBool);
207   os.notifyObservers(cancelQuit, "quit-application-requested", null);
209   // Something aborted the quit process.
210   if (cancelQuit.data)
211     return;
213   var as = Cc["@mozilla.org/toolkit/app-startup;1"].
214            getService(Ci.nsIAppStartup);
215   as.quit(Ci.nsIAppStartup.eRestart | Ci.nsIAppStartup.eAttemptQuit);
219  * Checks whether this blocklist element is valid for the current OS and ABI.
220  * If the element has an "os" attribute then the current OS must appear in
221  * its comma separated list for the element to be valid. Similarly for the
222  * xpcomabi attribute.
223  */
224 function matchesOSABI(blocklistElement) {
225   if (blocklistElement.hasAttribute("os")) {
226     var choices = blocklistElement.getAttribute("os").split(",");
227     if (choices.length > 0 && choices.indexOf(gApp.OS) < 0)
228       return false;
229   }
231   if (blocklistElement.hasAttribute("xpcomabi")) {
232     choices = blocklistElement.getAttribute("xpcomabi").split(",");
233     if (choices.length > 0 && choices.indexOf(gApp.XPCOMABI) < 0)
234       return false;
235   }
237   return true;
241  * Gets the current value of the locale.  It's possible for this preference to
242  * be localized, so we have to do a little extra work here.  Similar code
243  * exists in nsHttpHandler.cpp when building the UA string.
244  */
245 function getLocale() {
246   try {
247       // Get the default branch
248       var defaultPrefs = gPref.getDefaultBranch(null);
249       return defaultPrefs.getCharPref(PREF_GENERAL_USERAGENT_LOCALE);
250   } catch (e) {}
252   return gPref.getCharPref(PREF_GENERAL_USERAGENT_LOCALE);
256  * Read the update channel from defaults only.  We do this to ensure that
257  * the channel is tightly coupled with the application and does not apply
258  * to other installations of the application that may use the same profile.
259  */
260 function getUpdateChannel() {
261   var channel = "default";
262   var prefName;
263   var prefValue;
265   var defaults = gPref.getDefaultBranch(null);
266   try {
267     channel = defaults.getCharPref(PREF_APP_UPDATE_CHANNEL);
268   } catch (e) {
269     // use default when pref not found
270   }
272   try {
273     var partners = gPref.getChildList(PREF_PARTNER_BRANCH);
274     if (partners.length) {
275       channel += "-cck";
276       partners.sort();
278       for each (prefName in partners) {
279         prefValue = gPref.getCharPref(prefName);
280         channel += "-" + prefValue;
281       }
282     }
283   }
284   catch (e) {
285     Components.utils.reportError(e);
286   }
288   return channel;
291 /* Get the distribution pref values, from defaults only */
292 function getDistributionPrefValue(aPrefName) {
293   var prefValue = "default";
295   var defaults = gPref.getDefaultBranch(null);
296   try {
297     prefValue = defaults.getCharPref(aPrefName);
298   } catch (e) {
299     // use default when pref not found
300   }
302   return prefValue;
306  * Manages the Blocklist. The Blocklist is a representation of the contents of
307  * blocklist.xml and allows us to remotely disable / re-enable blocklisted
308  * items managed by the Extension Manager with an item's appDisabled property.
309  * It also blocklists plugins with data from blocklist.xml.
310  */
312 function Blocklist() {
313   let os = getObserverService();
314   os.addObserver(this, "xpcom-shutdown", false);
315   gLoggingEnabled = getPref("getBoolPref", PREF_EM_LOGGING_ENABLED, false);
316   gBlocklistEnabled = getPref("getBoolPref", PREF_BLOCKLIST_ENABLED, true);
317   gBlocklistLevel = Math.min(getPref("getIntPref", PREF_BLOCKLIST_LEVEL, DEFAULT_LEVEL),
318                                      MAX_BLOCK_LEVEL);
319   gPref.addObserver("extensions.blocklist.", this, false);
322 Blocklist.prototype = {
323   /**
324    * Extension ID -> array of Version Ranges
325    * Each value in the version range array is a JS Object that has the
326    * following properties:
327    *   "minVersion"  The minimum version in a version range (default = 0)
328    *   "maxVersion"  The maximum version in a version range (default = *)
329    *   "targetApps"  Application ID -> array of Version Ranges
330    *                 (default = current application ID)
331    *                 Each value in the version range array is a JS Object that
332    *                 has the following properties:
333    *                   "minVersion"  The minimum version in a version range
334    *                                 (default = 0)
335    *                   "maxVersion"  The maximum version in a version range
336    *                                 (default = *)
337    */
338   _addonEntries: null,
339   _pluginEntries: null,
341   observe: function(aSubject, aTopic, aData) {
342     switch (aTopic) {
343     case "xpcom-shutdown":
344       let os = getObserverService();
345       os.removeObserver(this, "xpcom-shutdown");
346       gPref.removeObserver("extensions.blocklist.", this);
347       break;
348     case "nsPref:changed":
349       switch (aData) {
350         case PREF_BLOCKLIST_ENABLED:
351           gBlocklistEnabled = getPref("getBoolPref", PREF_BLOCKLIST_ENABLED, true);
352           this._loadBlocklist();
353           this._blocklistUpdated(null, null);
354           break;
355         case PREF_BLOCKLIST_LEVEL:
356           gBlocklistLevel = Math.min(getPref("getIntPref", PREF_BLOCKLIST_LEVEL, DEFAULT_LEVEL),
357                                      MAX_BLOCK_LEVEL);
358           this._blocklistUpdated(null, null);
359           break;
360       }
361       break;
362     }
363   },
365   /* See nsIBlocklistService */
366   isAddonBlocklisted: function(id, version, appVersion, toolkitVersion) {
367     return this.getAddonBlocklistState(id, version, appVersion, toolkitVersion) ==
368                    Ci.nsIBlocklistService.STATE_BLOCKED;
369   },
371   /* See nsIBlocklistService */
372   getAddonBlocklistState: function(id, version, appVersion, toolkitVersion) {
373     if (!this._addonEntries)
374       this._loadBlocklist();
375     return this._getAddonBlocklistState(id, version, this._addonEntries,
376                                         appVersion, toolkitVersion);
377   },
379   /**
380    * Private version of getAddonBlocklistState that allows the caller to pass in
381    * the add-on blocklist entries to compare against.
382    *
383    * @param   id
384    *          The ID of the item to get the blocklist state for.
385    * @param   version
386    *          The version of the item to get the blocklist state for.
387    * @param   addonEntries
388    *          The add-on blocklist entries to compare against.
389    * @param   appVersion
390    *          The application version to compare to, will use the current
391    *          version if null.
392    * @param   toolkitVersion
393    *          The toolkit version to compare to, will use the current version if
394    *          null.
395    * @returns The blocklist state for the item, one of the STATE constants as
396    *          defined in nsIBlocklistService.
397    */
398   _getAddonBlocklistState: function(id, version, addonEntries, appVersion, toolkitVersion) {
399     if (!gBlocklistEnabled)
400       return Ci.nsIBlocklistService.STATE_NOT_BLOCKED;
402     if (!appVersion)
403       appVersion = gApp.version;
404     if (!toolkitVersion)
405       toolkitVersion = gApp.platformVersion;
407     var blItem = addonEntries[id];
408     if (!blItem)
409       return Ci.nsIBlocklistService.STATE_NOT_BLOCKED;
411     for (var i = 0; i < blItem.length; ++i) {
412       if (blItem[i].includesItem(version, appVersion, toolkitVersion))
413         return blItem[i].severity >= gBlocklistLevel ? Ci.nsIBlocklistService.STATE_BLOCKED :
414                                                        Ci.nsIBlocklistService.STATE_SOFTBLOCKED;
415     }
416     return Ci.nsIBlocklistService.STATE_NOT_BLOCKED;
417   },
419   notify: function(aTimer) {
420     if (!gBlocklistEnabled)
421       return;
423     try {
424       var dsURI = gPref.getCharPref(PREF_BLOCKLIST_URL);
425     }
426     catch (e) {
427       LOG("Blocklist::notify: The " + PREF_BLOCKLIST_URL + " preference" +
428           " is missing!");
429       return;
430     }
432     var pingCount = getPref("getIntPref", PREF_BLOCKLIST_PINGCOUNT, 0);
433     var totalPingCount = getPref("getIntPref", PREF_BLOCKLIST_TOTALPINGCOUNT, 1);
434     var daysSinceLastPing;
435     if (pingCount < 1) {
436       daysSinceLastPing = pingCount == 0 ? "new" : "reset";
437       pingCount = 1;
438     }
439     else {
440       // Seconds in one day is used because nsIUpdateTimerManager stores the
441       // last update time in seconds.
442       let secondsInDay = 60 * 60 * 24;
443       let lastUpdateTime = getPref("getIntPref", PREF_BLOCKLIST_LASTUPDATETIME, 0);
444       if (lastUpdateTime != 0) {
445         let now = Math.round(Date.now() / 1000);
446         daysSinceLastPing = Math.floor((now - lastUpdateTime) / secondsInDay);
447       }
448       else {
449         daysSinceLastPing = "invalid";
450       }
451     }
453     dsURI = dsURI.replace(/%APP_ID%/g, gApp.ID);
454     dsURI = dsURI.replace(/%APP_VERSION%/g, gApp.version);
455     dsURI = dsURI.replace(/%PRODUCT%/g, gApp.name);
456     dsURI = dsURI.replace(/%VERSION%/g, gApp.version);
457     dsURI = dsURI.replace(/%BUILD_ID%/g, gApp.appBuildID);
458     dsURI = dsURI.replace(/%BUILD_TARGET%/g, gApp.OS + "_" + gABI);
459     dsURI = dsURI.replace(/%OS_VERSION%/g, gOSVersion);
460     dsURI = dsURI.replace(/%LOCALE%/g, getLocale());
461     dsURI = dsURI.replace(/%CHANNEL%/g, getUpdateChannel());
462     dsURI = dsURI.replace(/%PLATFORM_VERSION%/g, gApp.platformVersion);
463     dsURI = dsURI.replace(/%DISTRIBUTION%/g,
464                       getDistributionPrefValue(PREF_APP_DISTRIBUTION));
465     dsURI = dsURI.replace(/%DISTRIBUTION_VERSION%/g,
466                       getDistributionPrefValue(PREF_APP_DISTRIBUTION_VERSION));
467     dsURI = dsURI.replace(/%PING_COUNT%/g, pingCount);
468     dsURI = dsURI.replace(/%TOTAL_PING_COUNT%/g, totalPingCount);
469     dsURI = dsURI.replace(/%DAYS_SINCE_LAST_PING%/g, daysSinceLastPing);
470     dsURI = dsURI.replace(/\+/g, "%2B");
472     // Under normal operations it will take around 5,883,516 years before the
473     // preferences used to store pingCount and totalPingCount will rollover
474     // so this code doesn't bother trying to do the "right thing" here.
475     pingCount++;
476     if (pingCount > 2147483647) {
477       // Rollover to -1 if the value is greater than what is support by an
478       // integer preference. The -1 indicates that the counter has been reset.
479       pingCount = -1;
480     }
481     gPref.setIntPref(PREF_BLOCKLIST_PINGCOUNT, pingCount);
483     totalPingCount++;
484     if (totalPingCount > 2147483647) {
485       // Rollover to 1 if the value is greater than what is support by an
486       // integer preference.
487       totalPingCount = 1;
488     }
489     gPref.setIntPref(PREF_BLOCKLIST_TOTALPINGCOUNT, totalPingCount);
491     // Verify that the URI is valid
492     try {
493       var uri = newURI(dsURI);
494     }
495     catch (e) {
496       LOG("Blocklist::notify: There was an error creating the blocklist URI\r\n" +
497           "for: " + dsURI + ", error: " + e);
498       return;
499     }
501     var request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].
502                   createInstance(Ci.nsIXMLHttpRequest);
503     request.open("GET", uri.spec, true);
504     request.channel.notificationCallbacks = new gCertUtils.BadCertHandler();
505     request.overrideMimeType("text/xml");
506     request.setRequestHeader("Cache-Control", "no-cache");
507     request.QueryInterface(Components.interfaces.nsIJSXMLHttpRequest);
509     var self = this;
510     request.onerror = function(event) { self.onXMLError(event); };
511     request.onload  = function(event) { self.onXMLLoad(event);  };
512     request.send(null);
514     // When the blocklist loads we need to compare it to the current copy so
515     // make sure we have loaded it.
516     if (!this._addonEntries)
517       this._loadBlocklist();
518   },
520   onXMLLoad: function(aEvent) {
521     var request = aEvent.target;
522     try {
523       gCertUtils.checkCert(request.channel);
524     }
525     catch (e) {
526       LOG("Blocklist::onXMLLoad: " + e);
527       return;
528     }
529     var responseXML = request.responseXML;
530     if (!responseXML || responseXML.documentElement.namespaceURI == XMLURI_PARSE_ERROR ||
531         (request.status != 200 && request.status != 0)) {
532       LOG("Blocklist::onXMLLoad: there was an error during load");
533       return;
534     }
535     var blocklistFile = FileUtils.getFile(KEY_PROFILEDIR, [FILE_BLOCKLIST]);
536     if (blocklistFile.exists())
537       blocklistFile.remove(false);
538     var fos = FileUtils.openSafeFileOutputStream(blocklistFile);
539     fos.write(request.responseText, request.responseText.length);
540     FileUtils.closeSafeFileOutputStream(fos);
542     var oldAddonEntries = this._addonEntries;
543     var oldPluginEntries = this._pluginEntries;
544     this._addonEntries = { };
545     this._pluginEntries = { };
546     this._loadBlocklistFromFile(FileUtils.getFile(KEY_PROFILEDIR,
547                                                   [FILE_BLOCKLIST]));
549     this._blocklistUpdated(oldAddonEntries, oldPluginEntries);
550   },
552   onXMLError: function(aEvent) {
553     try {
554       var request = aEvent.target;
555       // the following may throw (e.g. a local file or timeout)
556       var status = request.status;
557     }
558     catch (e) {
559       request = aEvent.target.channel.QueryInterface(Ci.nsIRequest);
560       status = request.status;
561     }
562     var statusText = "nsIXMLHttpRequest channel unavailable";
563     // When status is 0 we don't have a valid channel.
564     if (status != 0) {
565       try {
566         statusText = request.statusText;
567       } catch (e) {
568       }
569     }
570     LOG("Blocklist:onError: There was an error loading the blocklist file\r\n" +
571         statusText);
572   },
574   /**
575    * Finds the newest blocklist file from the application and the profile and
576    * load it or does nothing if neither exist.
577    */
578   _loadBlocklist: function() {
579     this._addonEntries = { };
580     this._pluginEntries = { };
581     var profFile = FileUtils.getFile(KEY_PROFILEDIR, [FILE_BLOCKLIST]);
582     if (profFile.exists()) {
583       this._loadBlocklistFromFile(profFile);
584       return;
585     }
586     var appFile = FileUtils.getFile(KEY_APPDIR, [FILE_BLOCKLIST]);
587     if (appFile.exists()) {
588       this._loadBlocklistFromFile(appFile);
589       return;
590     }
591     LOG("Blocklist::_loadBlocklist: no XML File found");
592   },
594   /**
595 #    The blocklist XML file looks something like this:
597 #    <blocklist xmlns="http://www.mozilla.org/2006/addons-blocklist">
598 #      <emItems>
599 #        <emItem id="item_1@domain">
600 #          <versionRange minVersion="1.0" maxVersion="2.0.*">
601 #            <targetApplication id="{ec8030f7-c20a-464f-9b0e-13a3a9e97384}">
602 #              <versionRange minVersion="1.5" maxVersion="1.5.*"/>
603 #              <versionRange minVersion="1.7" maxVersion="1.7.*"/>
604 #            </targetApplication>
605 #            <targetApplication id="toolkit@mozilla.org">
606 #              <versionRange minVersion="1.9" maxVersion="1.9.*"/>
607 #            </targetApplication>
608 #          </versionRange>
609 #          <versionRange minVersion="3.0" maxVersion="3.0.*">
610 #            <targetApplication id="{ec8030f7-c20a-464f-9b0e-13a3a9e97384}">
611 #              <versionRange minVersion="1.5" maxVersion="1.5.*"/>
612 #            </targetApplication>
613 #            <targetApplication id="toolkit@mozilla.org">
614 #              <versionRange minVersion="1.9" maxVersion="1.9.*"/>
615 #            </targetApplication>
616 #          </versionRange>
617 #        </emItem>
618 #        <emItem id="item_2@domain">
619 #          <versionRange minVersion="3.1" maxVersion="4.*"/>
620 #        </emItem>
621 #        <emItem id="item_3@domain">
622 #          <versionRange>
623 #            <targetApplication id="{ec8030f7-c20a-464f-9b0e-13a3a9e97384}">
624 #              <versionRange minVersion="1.5" maxVersion="1.5.*"/>
625 #            </targetApplication>
626 #          </versionRange>
627 #        </emItem>
628 #        <emItem id="item_4@domain">
629 #          <versionRange>
630 #            <targetApplication>
631 #              <versionRange minVersion="1.5" maxVersion="1.5.*"/>
632 #            </targetApplication>
633 #          </versionRange>
634 #        <emItem id="item_5@domain"/>
635 #      </emItems>
636 #      <pluginItems>
637 #        <pluginItem>
638 #          <!-- All match tags must match a plugin to blocklist a plugin -->
639 #          <match name="name" exp="some plugin"/>
640 #          <match name="description" exp="1[.]2[.]3"/>
641 #        </pluginItem>
642 #      </pluginItems>
643 #    </blocklist>
644    */
646   _loadBlocklistFromFile: function(file) {
647     if (!gBlocklistEnabled) {
648       LOG("Blocklist::_loadBlocklistFromFile: blocklist is disabled");
649       return;
650     }
652     if (!file.exists()) {
653       LOG("Blocklist::_loadBlocklistFromFile: XML File does not exist");
654       return;
655     }
657     var fileStream = Components.classes["@mozilla.org/network/file-input-stream;1"]
658                                .createInstance(Components.interfaces.nsIFileInputStream);
659     fileStream.init(file, FileUtils.MODE_RDONLY, FileUtils.PERMS_FILE, 0);
660     try {
661       var parser = Cc["@mozilla.org/xmlextras/domparser;1"].
662                    createInstance(Ci.nsIDOMParser);
663       var doc = parser.parseFromStream(fileStream, "UTF-8", file.fileSize, "text/xml");
664       if (doc.documentElement.namespaceURI != XMLURI_BLOCKLIST) {
665         LOG("Blocklist::_loadBlocklistFromFile: aborting due to incorrect " +
666             "XML Namespace.\r\nExpected: " + XMLURI_BLOCKLIST + "\r\n" +
667             "Received: " + doc.documentElement.namespaceURI);
668         return;
669       }
671       var childNodes = doc.documentElement.childNodes;
672       for (var i = 0; i < childNodes.length; ++i) {
673         var element = childNodes[i];
674         if (!(element instanceof Ci.nsIDOMElement))
675           continue;
676         switch (element.localName) {
677         case "emItems":
678           this._addonEntries = this._processItemNodes(element.childNodes, "em",
679                                                       this._handleEmItemNode);
680           break;
681         case "pluginItems":
682           this._pluginEntries = this._processItemNodes(element.childNodes, "plugin",
683                                                        this._handlePluginItemNode);
684           break;
685         default:
686           Services.obs.notifyObservers(element,
687                                        "blocklist-data-" + element.localName,
688                                        null);
689         }
690       }
691     }
692     catch (e) {
693       LOG("Blocklist::_loadBlocklistFromFile: Error constructing blocklist " + e);
694       return;
695     }
696     fileStream.close();
697   },
699   _processItemNodes: function(itemNodes, prefix, handler) {
700     var result = [];
701     var itemName = prefix + "Item";
702     for (var i = 0; i < itemNodes.length; ++i) {
703       var blocklistElement = itemNodes.item(i);
704       if (!(blocklistElement instanceof Ci.nsIDOMElement) ||
705           blocklistElement.localName != itemName)
706         continue;
708       handler(blocklistElement, result);
709     }
710     return result;
711   },
713   _handleEmItemNode: function(blocklistElement, result) {
714     if (!matchesOSABI(blocklistElement))
715       return;
717     var versionNodes = blocklistElement.childNodes;
718     var id = blocklistElement.getAttribute("id");
719     result[id] = [];
720     for (var x = 0; x < versionNodes.length; ++x) {
721       var versionRangeElement = versionNodes.item(x);
722       if (!(versionRangeElement instanceof Ci.nsIDOMElement) ||
723           versionRangeElement.localName != "versionRange")
724         continue;
726       result[id].push(new BlocklistItemData(versionRangeElement));
727     }
728     // if only the extension ID is specified block all versions of the
729     // extension for the current application.
730     if (result[id].length == 0)
731       result[id].push(new BlocklistItemData(null));
732   },
734   _handlePluginItemNode: function(blocklistElement, result) {
735     if (!matchesOSABI(blocklistElement))
736       return;
738     var matchNodes = blocklistElement.childNodes;
739     var blockEntry = {
740       matches: {},
741       versions: []
742     };
743     var hasMatch = false;
744     for (var x = 0; x < matchNodes.length; ++x) {
745       var matchElement = matchNodes.item(x);
746       if (!(matchElement instanceof Ci.nsIDOMElement))
747         continue;
748       if (matchElement.localName == "match") {
749         var name = matchElement.getAttribute("name");
750         var exp = matchElement.getAttribute("exp");
751         try {
752           blockEntry.matches[name] = new RegExp(exp, "m");
753           hasMatch = true;
754         } catch (e) {
755           // Ignore invalid regular expressions
756         }
757       }
758       if (matchElement.localName == "versionRange")
759         blockEntry.versions.push(new BlocklistItemData(matchElement));
760     }
761     // Plugin entries require *something* to match to an actual plugin
762     if (!hasMatch)
763       return;
764     // Add a default versionRange if there wasn't one specified
765     if (blockEntry.versions.length == 0)
766       blockEntry.versions.push(new BlocklistItemData(null));
767     result.push(blockEntry);
768   },
770   /* See nsIBlocklistService */
771   getPluginBlocklistState: function(plugin, appVersion, toolkitVersion) {
772     if (!this._pluginEntries)
773       this._loadBlocklist();
774     return this._getPluginBlocklistState(plugin, this._pluginEntries,
775                                          appVersion, toolkitVersion);
776   },
778   /**
779    * Private version of getPluginBlocklistState that allows the caller to pass in
780    * the plugin blocklist entries.
781    *
782    * @param   plugin
783    *          The nsIPluginTag to get the blocklist state for.
784    * @param   pluginEntries
785    *          The plugin blocklist entries to compare against.
786    * @param   appVersion
787    *          The application version to compare to, will use the current
788    *          version if null.
789    * @param   toolkitVersion
790    *          The toolkit version to compare to, will use the current version if
791    *          null.
792    * @returns The blocklist state for the item, one of the STATE constants as
793    *          defined in nsIBlocklistService.
794    */
795   _getPluginBlocklistState: function(plugin, pluginEntries, appVersion, toolkitVersion) {
796     if (!gBlocklistEnabled)
797       return Ci.nsIBlocklistService.STATE_NOT_BLOCKED;
799     if (!appVersion)
800       appVersion = gApp.version;
801     if (!toolkitVersion)
802       toolkitVersion = gApp.platformVersion;
804     for each (var blockEntry in pluginEntries) {
805       var matchFailed = false;
806       for (var name in blockEntry.matches) {
807         if (!(name in plugin) ||
808             typeof(plugin[name]) != "string" ||
809             !blockEntry.matches[name].test(plugin[name])) {
810           matchFailed = true;
811           break;
812         }
813       }
815       if (matchFailed)
816         continue;
818       for (var i = 0; i < blockEntry.versions.length; i++) {
819         if (blockEntry.versions[i].includesItem(plugin.version, appVersion,
820                                                 toolkitVersion)) {
821           if (blockEntry.versions[i].severity >= gBlocklistLevel)
822             return Ci.nsIBlocklistService.STATE_BLOCKED;
823           if (blockEntry.versions[i].severity == SEVERITY_OUTDATED)
824             return Ci.nsIBlocklistService.STATE_OUTDATED;
825           return Ci.nsIBlocklistService.STATE_SOFTBLOCKED;
826         }
827       }
828     }
830     return Ci.nsIBlocklistService.STATE_NOT_BLOCKED;
831   },
833   _blocklistUpdated: function(oldAddonEntries, oldPluginEntries) {
834     var addonList = [];
836     var self = this;
837     AddonManager.getAddonsByTypes(["extension", "theme", "locale"], function(addons) {
839       for (let i = 0; i < addons.length; i++) {
840         let oldState = Ci.nsIBlocklistService.STATE_NOTBLOCKED;
841         if (oldAddonEntries)
842           oldState = self._getAddonBlocklistState(addons[i].id, addons[i].version,
843                                                   oldAddonEntries);
844         let state = self.getAddonBlocklistState(addons[i].id, addons[i].version);
846         LOG("Blocklist state for " + addons[i].id + " changed from " +
847             oldState + " to " + state);
849         // Don't warn about add-ons becoming unblocked.
850         if (state == 0)
851           continue;
853         // We don't want to re-warn about add-ons
854         if (state == oldState)
855           continue;
857         // If an add-on has dropped from hard to soft blocked just mark it as
858         // user disabled and don't warn about it.
859         if (state == Ci.nsIBlocklistService.STATE_SOFTBLOCKED &&
860             oldState == Ci.nsIBlocklistService.STATE_BLOCKED) {
861           addons[i].userDisabled = true;
862           continue;
863         }
865         // If the add-on is already disabled for some reason then don't warn
866         // about it
867         if (!addons[i].isActive)
868           continue;
870         addonList.push({
871           name: addons[i].name,
872           version: addons[i].version,
873           icon: addons[i].iconURL,
874           disable: false,
875           blocked: state == Ci.nsIBlocklistService.STATE_BLOCKED,
876           item: addons[i]
877         });
878       }
880       AddonManagerPrivate.updateAddonAppDisabledStates();
882       var phs = Cc["@mozilla.org/plugin/host;1"].
883                 getService(Ci.nsIPluginHost);
884       var plugins = phs.getPluginTags();
886       for (let i = 0; i < plugins.length; i++) {
887         let oldState = -1;
888         if (oldPluginEntries)
889           oldState = self._getPluginBlocklistState(plugins[i], oldPluginEntries);
890         let state = self.getPluginBlocklistState(plugins[i]);
891         LOG("Blocklist state for " + plugins[i].name + " changed from " +
892             oldState + " to " + state);
893         // We don't want to re-warn about items
894         if (state == oldState)
895           continue;
897         if (plugins[i].blocklisted) {
898           if (state == Ci.nsIBlocklistService.STATE_SOFTBLOCKED)
899             plugins[i].disabled = true;
900         }
901         else if (!plugins[i].disabled && state != Ci.nsIBlocklistService.STATE_NOT_BLOCKED) {
902           if (state == Ci.nsIBlocklistService.STATE_OUTDATED) {
903             gPref.setBoolPref(PREF_PLUGINS_NOTIFYUSER, true);
904           }
905           else {
906             addonList.push({
907               name: plugins[i].name,
908               version: plugins[i].version,
909               icon: "chrome://mozapps/skin/plugins/pluginGeneric.png",
910               disable: false,
911               blocked: state == Ci.nsIBlocklistService.STATE_BLOCKED,
912               item: plugins[i]
913             });
914           }
915         }
916         plugins[i].blocklisted = state == Ci.nsIBlocklistService.STATE_BLOCKED;
917       }
919       if (addonList.length == 0)
920         return;
922       if ("@mozilla.org/addons/blocklist-prompt;1" in Cc) {
923         try {
924           let blockedPrompter = Cc["@mozilla.org/addons/blocklist-prompt;1"]
925                                  .getService(Ci.nsIBlocklistPrompt);
926           blockedPrompter.prompt(addonList);
927         } catch (e) {
928           LOG(e);
929         }
930         return;
931       }
933       var args = {
934         restart: false,
935         list: addonList
936       };
937       // This lets the dialog get the raw js object
938       args.wrappedJSObject = args;
940       var ww = Cc["@mozilla.org/embedcomp/window-watcher;1"].
941                getService(Ci.nsIWindowWatcher);
942       ww.openWindow(null, URI_BLOCKLIST_DIALOG, "",
943                     "chrome,centerscreen,dialog,modal,titlebar", args);
945       for (let i = 0; i < addonList.length; i++) {
946         if (!addonList[i].disable)
947           continue;
949         if (addonList[i].item instanceof Ci.nsIPluginTag)
950           addonList[i].item.disabled = true;
951         else
952           addonList[i].item.userDisabled = true;
953       }
955       if (args.restart)
956         restartApp();
957     });
958   },
960   classID: Components.ID("{66354bc9-7ed1-4692-ae1d-8da97d6b205e}"),
961   QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
962                                          Ci.nsIBlocklistService,
963                                          Ci.nsITimerCallback]),
967  * Helper for constructing a blocklist.
968  */
969 function BlocklistItemData(versionRangeElement) {
970   var versionRange = this.getBlocklistVersionRange(versionRangeElement);
971   this.minVersion = versionRange.minVersion;
972   this.maxVersion = versionRange.maxVersion;
973   if (versionRangeElement && versionRangeElement.hasAttribute("severity"))
974     this.severity = versionRangeElement.getAttribute("severity");
975   else
976     this.severity = DEFAULT_SEVERITY;
977   this.targetApps = { };
978   var found = false;
980   if (versionRangeElement) {
981     for (var i = 0; i < versionRangeElement.childNodes.length; ++i) {
982       var targetAppElement = versionRangeElement.childNodes.item(i);
983       if (!(targetAppElement instanceof Ci.nsIDOMElement) ||
984           targetAppElement.localName != "targetApplication")
985         continue;
986       found = true;
987       // default to the current application if id is not provided.
988       var appID = targetAppElement.hasAttribute("id") ? targetAppElement.getAttribute("id") : gApp.ID;
989       this.targetApps[appID] = this.getBlocklistAppVersions(targetAppElement);
990     }
991   }
992   // Default to all versions of the current application when no targetApplication
993   // elements were found
994   if (!found)
995     this.targetApps[gApp.ID] = this.getBlocklistAppVersions(null);
998 BlocklistItemData.prototype = {
999   /**
1000    * Tests if a version of an item is included in the version range and target
1001    * application information represented by this BlocklistItemData using the
1002    * provided application and toolkit versions.
1003    * @param   version
1004    *          The version of the item being tested.
1005    * @param   appVersion
1006    *          The application version to test with.
1007    * @param   toolkitVersion
1008    *          The toolkit version to test with.
1009    * @returns True if the version range covers the item version and application
1010    *          or toolkit version.
1011    */
1012   includesItem: function(version, appVersion, toolkitVersion) {
1013     // Some platforms have no version for plugins, these don't match if there
1014     // was a min/maxVersion provided
1015     if (!version && (this.minVersion || this.maxVersion))
1016       return false;
1018     // Check if the item version matches
1019     if (!this.matchesRange(version, this.minVersion, this.maxVersion))
1020       return false;
1022     // Check if the application version matches
1023     if (this.matchesTargetRange(gApp.ID, appVersion))
1024       return true;
1026     // Check if the toolkit version matches
1027     return this.matchesTargetRange(TOOLKIT_ID, toolkitVersion);
1028   },
1030   /**
1031    * Checks if a version is higher than or equal to the minVersion (if provided)
1032    * and lower than or equal to the maxVersion (if provided).
1033    * @param   version
1034    *          The version to test.
1035    * @param   minVersion
1036    *          The minimum version. If null it is assumed that version is always
1037    *          larger.
1038    * @param   maxVersion
1039    *          The maximum version. If null it is assumed that version is always
1040    *          smaller.
1041    */
1042   matchesRange: function(version, minVersion, maxVersion) {
1043     if (minVersion && gVersionChecker.compare(version, minVersion) < 0)
1044       return false;
1045     if (maxVersion && gVersionChecker.compare(version, maxVersion) > 0)
1046       return false;
1047     return true;
1048   },
1050   /**
1051    * Tests if there is a matching range for the given target application id and
1052    * version.
1053    * @param   appID
1054    *          The application ID to test for, may be for an application or toolkit
1055    * @param   appVersion
1056    *          The version of the application to test for.
1057    * @returns True if this version range covers the application version given.
1058    */
1059   matchesTargetRange: function(appID, appVersion) {
1060     var blTargetApp = this.targetApps[appID];
1061     if (!blTargetApp)
1062       return false;
1064     for (var x = 0; x < blTargetApp.length; ++x) {
1065       if (this.matchesRange(appVersion, blTargetApp[x].minVersion, blTargetApp[x].maxVersion))
1066         return true;
1067     }
1069     return false;
1070   },
1072   /**
1073    * Retrieves a version range (e.g. minVersion and maxVersion) for a
1074    * blocklist item's targetApplication element.
1075    * @param   targetAppElement
1076    *          A targetApplication blocklist element.
1077    * @returns An array of JS objects with the following properties:
1078    *          "minVersion"  The minimum version in a version range (default = null).
1079    *          "maxVersion"  The maximum version in a version range (default = null).
1080    */
1081   getBlocklistAppVersions: function(targetAppElement) {
1082     var appVersions = [ ];
1084     if (targetAppElement) {
1085       for (var i = 0; i < targetAppElement.childNodes.length; ++i) {
1086         var versionRangeElement = targetAppElement.childNodes.item(i);
1087         if (!(versionRangeElement instanceof Ci.nsIDOMElement) ||
1088             versionRangeElement.localName != "versionRange")
1089           continue;
1090         appVersions.push(this.getBlocklistVersionRange(versionRangeElement));
1091       }
1092     }
1093     // return minVersion = null and maxVersion = null if no specific versionRange
1094     // elements were found
1095     if (appVersions.length == 0)
1096       appVersions.push(this.getBlocklistVersionRange(null));
1097     return appVersions;
1098   },
1100   /**
1101    * Retrieves a version range (e.g. minVersion and maxVersion) for a blocklist
1102    * versionRange element.
1103    * @param   versionRangeElement
1104    *          The versionRange blocklist element.
1105    * @returns A JS object with the following properties:
1106    *          "minVersion"  The minimum version in a version range (default = null).
1107    *          "maxVersion"  The maximum version in a version range (default = null).
1108    */
1109   getBlocklistVersionRange: function(versionRangeElement) {
1110     var minVersion = null;
1111     var maxVersion = null;
1112     if (!versionRangeElement)
1113       return { minVersion: minVersion, maxVersion: maxVersion };
1115     if (versionRangeElement.hasAttribute("minVersion"))
1116       minVersion = versionRangeElement.getAttribute("minVersion");
1117     if (versionRangeElement.hasAttribute("maxVersion"))
1118       maxVersion = versionRangeElement.getAttribute("maxVersion");
1120     return { minVersion: minVersion, maxVersion: maxVersion };
1121   }
1124 var NSGetFactory = XPCOMUtils.generateNSGetFactory([Blocklist]);