Merge inbound to mozilla-central. a=merge
authorBogdan Tara <btara@mozilla.com>
Wed, 21 Nov 2018 11:40:19 +0200
changeset 503876 7488645b27ac9b273d6785db04204684673b3657
parent 503865 8b1b35582dd7f986619174828bae634511718de9 (current diff)
parent 503875 0119ff5d4de90ac8c66397721aacb91beb8f6756 (diff)
child 503887 95902e82a59eef77a3f5cde7014419e1f13ed763
child 503951 ab0dfc5b827dcc0a2eef87d8e9ccbf383a397f72
push id10290
push userffxbld-merge
push dateMon, 03 Dec 2018 16:23:23 +0000
treeherdermozilla-beta@700bed2445e6 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone65.0a1
first release with
nightly linux32
7488645b27ac / 65.0a1 / 20181121100034 / files
nightly linux64
7488645b27ac / 65.0a1 / 20181121100034 / files
nightly mac
7488645b27ac / 65.0a1 / 20181121100034 / files
nightly win32
7488645b27ac / 65.0a1 / 20181121100034 / files
nightly win64
7488645b27ac / 65.0a1 / 20181121100034 / files
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
releases
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge inbound to mozilla-central. a=merge
tools/quitter/bootstrap.js
tools/quitter/chrome.manifest
tools/quitter/install.rdf
tools/quitter/jar.mn
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1544,16 +1544,19 @@ pref("browser.contentblocking.reportBrea
 pref("browser.contentblocking.rejecttrackers.reportBreakage.enabled", true);
 
 pref("browser.contentblocking.reportBreakage.url", "https://tracking-protection-issues.herokuapp.com/new");
 
 pref("browser.contentblocking.introCount", 0);
 
 pref("privacy.trackingprotection.introURL", "https://www.mozilla.org/%LOCALE%/firefox/%VERSION%/tracking-protection/start/");
 
+// Workaround for Google Recaptcha
+pref("urlclassifier.trackingAnnotationSkipURLs", "google.com/recaptcha/,*.google.com/recaptcha/");
+
 // Always enable newtab segregation using containers
 pref("privacy.usercontext.about_newtab_segregation.enabled", true);
 // Enable Contextual Identity Containers
 #ifdef NIGHTLY_BUILD
 pref("privacy.userContext.enabled", true);
 pref("privacy.userContext.ui.enabled", true);
 
 // 0 disables long press, 1 when clicked, the menu is shown, 2 the menu is shown after X milliseconds.
--- a/dom/base/nsContentUtils.cpp
+++ b/dom/base/nsContentUtils.cpp
@@ -11182,16 +11182,32 @@ nsContentUtils::StringifyJSON(JSContext*
   return true;
 }
 
 /* static */ bool
 nsContentUtils::IsURIInPrefList(nsIURI* aURI, const char* aPrefName)
 {
   MOZ_ASSERT(aPrefName);
 
+  nsAutoCString blackList;
+  Preferences::GetCString(aPrefName, blackList);
+  ToLowerCase(blackList);
+  return IsURIInList(aURI, blackList);
+}
+
+/* static */ bool
+nsContentUtils::IsURIInList(nsIURI* aURI, const nsCString& aBlackList)
+{
+#ifdef DEBUG
+  nsAutoCString blackListLowerCase(aBlackList);
+  ToLowerCase(blackListLowerCase);
+  MOZ_ASSERT(blackListLowerCase.Equals(aBlackList),
+             "The aBlackList argument should be lower-case");
+#endif
+
   if (!aURI) {
     return false;
   }
 
   nsAutoCString scheme;
   aURI->GetScheme(scheme);
   if (!scheme.EqualsLiteral("http") &&
       !scheme.EqualsLiteral("https")) {
@@ -11200,46 +11216,43 @@ nsContentUtils::IsURIInPrefList(nsIURI* 
 
   nsAutoCString host;
   aURI->GetHost(host);
   if (host.IsEmpty()) {
     return false;
   }
   ToLowerCase(host);
 
-  nsAutoCString blackList;
-  Preferences::GetCString(aPrefName, blackList);
-  ToLowerCase(blackList);
-  if (blackList.IsEmpty()) {
+  if (aBlackList.IsEmpty()) {
     return false;
   }
 
   // The list is comma separated domain list.  Each item may start with "*.".
   // If starts with "*.", it matches any sub-domains.
 
   for (;;) {
-    int32_t index = blackList.Find(host, false);
+    int32_t index = aBlackList.Find(host, false);
     if (index >= 0 &&
-        static_cast<uint32_t>(index) + host.Length() <= blackList.Length() &&
+        static_cast<uint32_t>(index) + host.Length() <= aBlackList.Length() &&
         // If start of the black list or next to ","?
-        (!index || blackList[index - 1] == ',')) {
+        (!index || aBlackList[index - 1] == ',')) {
       // If end of the black list or immediately before ","?
       size_t indexAfterHost = index + host.Length();
-      if (indexAfterHost == blackList.Length() ||
-          blackList[indexAfterHost] == ',') {
+      if (indexAfterHost == aBlackList.Length() ||
+          aBlackList[indexAfterHost] == ',') {
         return true;
       }
       // If next character is '/', we need to check the path too.
       // We assume the path in blacklist means "/foo" + "*".
-      if (blackList[indexAfterHost] == '/') {
-        int32_t endOfPath = blackList.Find(",", false, indexAfterHost);
+      if (aBlackList[indexAfterHost] == '/') {
+        int32_t endOfPath = aBlackList.Find(",", false, indexAfterHost);
         nsDependentCSubstring::size_type length =
           endOfPath < 0 ? static_cast<nsDependentCSubstring::size_type>(-1) :
                           endOfPath - indexAfterHost;
-        nsDependentCSubstring pathInBlackList(blackList,
+        nsDependentCSubstring pathInBlackList(aBlackList,
                                               indexAfterHost, length);
         nsAutoCString filePath;
         aURI->GetFilePath(filePath);
         ToLowerCase(filePath);
         if (StringBeginsWith(filePath, pathInBlackList)) {
           return true;
         }
       }
--- a/dom/base/nsContentUtils.h
+++ b/dom/base/nsContentUtils.h
@@ -1206,16 +1206,23 @@ public:
                      uint32_t *aColumnOut, nsString& aMessageOut);
 
   static nsresult CalculateBufferSizeForImage(const uint32_t& aStride,
                                             const mozilla::gfx::IntSize& aImageSize,
                                             const mozilla::gfx::SurfaceFormat& aFormat,
                                             size_t* aMaxBufferSize,
                                             size_t* aUsedBufferSize);
 
+  // Returns true if the URI's host is contained in a list which is a comma
+  // separated domain list.  Each item may start with "*.".  If starts with
+  // "*.", it matches any sub-domains.
+  // The aList argument must be a lower-case string.
+  static bool
+  IsURIInList(nsIURI* aURI, const nsCString& aList);
+
   // Returns true if the URI's host is contained in a pref list which is a comma
   // separated domain list.  Each item may start with "*.".  If starts with
   // "*.", it matches any sub-domains.
   static bool
   IsURIInPrefList(nsIURI* aURI, const char* aPrefName);
 
 private:
   /**
--- a/js/src/devtools/automation/winbuildenv.sh
+++ b/js/src/devtools/automation/winbuildenv.sh
@@ -20,16 +20,20 @@ echo "export ORIGINAL_LIB=$LIB"
 echo "export ORIGINAL_LIBPATH=$LIBPATH"
 
 if [ -n "$USE_64BIT" ]; then
   . $topsrcdir/build/win64/mozconfig.vs-latest
 else
   . $topsrcdir/build/win32/mozconfig.vs-latest
 fi
 
+mk_export_correct_style CC
+mk_export_correct_style CXX
+mk_export_correct_style LINKER
+
 # PATH also needs to point to mozmake.exe, which can come from either
 # newer mozilla-build or tooltool.
 if ! which mozmake 2>/dev/null; then
     export PATH="$PATH:$SOURCE/.."
     if ! which mozmake 2>/dev/null; then
   TT_SERVER=${TT_SERVER:-https://tooltool.mozilla-releng.net/}
   ( cd $SOURCE/..; $SOURCE/mach artifact toolchain -v --tooltool-manifest $SOURCE/browser/config/tooltool-manifests/${platform:-win32}/releng.manifest --tooltool-url $TT_SERVER --retry 4${TOOLTOOL_CACHE:+ --cache-dir ${TOOLTOOL_CACHE}}${MOZ_TOOLCHAINS:+ ${MOZ_TOOLCHAINS}})
     fi
--- a/layout/tables/nsTableFrame.cpp
+++ b/layout/tables/nsTableFrame.cpp
@@ -5277,50 +5277,58 @@ CompareBorders(const nsIFrame*  aTableFr
 
 static bool
 Perpendicular(mozilla::LogicalSide aSide1,
               mozilla::LogicalSide aSide2)
 {
   return IsInline(aSide1) != IsInline(aSide2);
 }
 
+// Initial value indicating that BCCornerInfo's ownerStyle hasn't been set yet.
+#define BORDER_STYLE_UNSET 0xF
+
 // XXX allocate this as number-of-cols+1 instead of number-of-cols+1 * number-of-rows+1
 struct BCCornerInfo
 {
   BCCornerInfo() { ownerColor = 0; ownerWidth = subWidth = ownerElem = subSide =
                    subElem = hasDashDot = numSegs = bevel = 0; ownerSide = eLogicalSideBStart;
-                   ownerStyle = 0xFF; subStyle = NS_STYLE_BORDER_STYLE_SOLID;  }
+                   ownerStyle = BORDER_STYLE_UNSET;
+                   subStyle = NS_STYLE_BORDER_STYLE_SOLID; }
+
   void Set(mozilla::LogicalSide aSide,
            BCCellBorder  border);
 
   void Update(mozilla::LogicalSide aSide,
               BCCellBorder  border);
 
   nscolor   ownerColor;     // color of borderOwner
   uint16_t  ownerWidth;     // pixel width of borderOwner
   uint16_t  subWidth;       // pixel width of the largest border intersecting the border perpendicular
                             // to ownerSide
   uint32_t  ownerSide:2;    // LogicalSide (e.g eLogicalSideBStart, etc) of the border
                             // owning the corner relative to the corner
-  uint32_t  ownerElem:3;    // elem type (e.g. eTable, eGroup, etc) owning the corner
-  uint32_t  ownerStyle:8;   // border style of ownerElem
+  uint32_t  ownerElem:4;    // elem type (e.g. eTable, eGroup, etc) owning the corner
+  uint32_t  ownerStyle:4;   // border style of ownerElem
   uint32_t  subSide:2;      // side of border with subWidth relative to the corner
-  uint32_t  subElem:3;      // elem type (e.g. eTable, eGroup, etc) of sub owner
-  uint32_t  subStyle:8;     // border style of subElem
+  uint32_t  subElem:4;      // elem type (e.g. eTable, eGroup, etc) of sub owner
+  uint32_t  subStyle:4;     // border style of subElem
   uint32_t  hasDashDot:1;   // does a dashed, dotted segment enter the corner, they cannot be beveled
   uint32_t  numSegs:3;      // number of segments entering corner
   uint32_t  bevel:1;        // is the corner beveled (uses the above two fields together with subWidth)
-  // one bit is unused
+  // 7 bits are unused
 };
 
 void
 BCCornerInfo::Set(mozilla::LogicalSide aSide,
                   BCCellBorder  aBorder)
 {
-  ownerElem  = aBorder.owner;
+  // FIXME bug 1508921: We mask 4-bit BCBorderOwner enum to 3 bits to preserve
+  // buggy behavior found by the frame_above_rules_all.html mochitest.
+  ownerElem  = aBorder.owner & 0x7;
+
   ownerStyle = aBorder.style;
   ownerWidth = aBorder.width;
   ownerColor = aBorder.color;
   ownerSide  = aSide;
   hasDashDot = 0;
   numSegs    = 0;
   if (aBorder.width > 0) {
     numSegs++;
@@ -5334,30 +5342,30 @@ BCCornerInfo::Set(mozilla::LogicalSide a
   subElem    = eTableOwner;
   subStyle   = NS_STYLE_BORDER_STYLE_SOLID;
 }
 
 void
 BCCornerInfo::Update(mozilla::LogicalSide aSide,
                      BCCellBorder  aBorder)
 {
-  bool existingWins = false;
-  if (0xFF == ownerStyle) { // initial value indiating that it hasn't been set yet
+  if (ownerStyle == BORDER_STYLE_UNSET) {
     Set(aSide, aBorder);
   }
   else {
     bool isInline = IsInline(aSide); // relative to the corner
     BCCellBorder oldBorder, tempBorder;
     oldBorder.owner  = (BCBorderOwner) ownerElem;
     oldBorder.style =  ownerStyle;
     oldBorder.width =  ownerWidth;
     oldBorder.color =  ownerColor;
 
     LogicalSide oldSide  = LogicalSide(ownerSide);
 
+    bool existingWins = false;
     tempBorder = CompareBorders(CELL_CORNER, oldBorder, aBorder, isInline, &existingWins);
 
     ownerElem  = tempBorder.owner;
     ownerStyle = tempBorder.style;
     ownerWidth = tempBorder.width;
     ownerColor = tempBorder.color;
     if (existingWins) { // existing corner is dominant
       if (::Perpendicular(LogicalSide(ownerSide), aSide)) {
--- a/netwerk/base/nsChannelClassifier.cpp
+++ b/netwerk/base/nsChannelClassifier.cpp
@@ -59,16 +59,17 @@ static LazyLogModule gChannelClassifierL
 
 #undef LOG
 #define LOG(args) MOZ_LOG(gChannelClassifierLog, LogLevel::Info, args)
 #define LOG_DEBUG(args) MOZ_LOG(gChannelClassifierLog, LogLevel::Debug, args)
 #define LOG_WARN(args) MOZ_LOG(gChannelClassifierLog, LogLevel::Warning, args)
 #define LOG_ENABLED() MOZ_LOG_TEST(gChannelClassifierLog, LogLevel::Info)
 
 #define URLCLASSIFIER_SKIP_HOSTNAMES       "urlclassifier.skipHostnames"
+#define URLCLASSIFIER_TRACKING_ANNOTATION_SKIP_URLS "urlclassifier.trackingAnnotationSkipURLs"
 #define URLCLASSIFIER_ANNOTATION_TABLE     "urlclassifier.trackingAnnotationTable"
 #define URLCLASSIFIER_ANNOTATION_TABLE_TEST_ENTRIES "urlclassifier.trackingAnnotationTable.testEntries"
 #define URLCLASSIFIER_ANNOTATION_WHITELIST "urlclassifier.trackingAnnotationWhitelistTable"
 #define URLCLASSIFIER_ANNOTATION_WHITELIST_TEST_ENTRIES "urlclassifier.trackingAnnotationWhitelistTable.testEntries"
 #define URLCLASSIFIER_TRACKING_TABLE       "urlclassifier.trackingTable"
 #define URLCLASSIFIER_TRACKING_TABLE_TEST_ENTRIES "urlclassifier.trackingTable.testEntries"
 #define URLCLASSIFIER_TRACKING_WHITELIST   "urlclassifier.trackingWhitelistTable"
 #define URLCLASSIFIER_TRACKING_WHITELIST_TEST_ENTRIES "urlclassifier.trackingWhitelistTable.testEntries"
@@ -95,26 +96,28 @@ public:
   static CachedPrefs* GetInstance();
 
   void Init();
   bool IsAllowListExample() { return sAllowListExample;}
   bool IsLowerNetworkPriority() { return sLowerNetworkPriority;}
   bool IsAnnotateChannelEnabled() { return sAnnotateChannelEnabled;}
 
   nsCString GetSkipHostnames() const { return mSkipHostnames; }
+  nsCString GetSkipTrackingAnnotationURLs() const { return mSkipTrackingAnnotationURLs; }
   nsCString GetAnnotationBlackList() const { return mAnnotationBlacklist; }
   nsCString GetAnnotationBlackListExtraEntries() const { return mAnnotationBlacklistExtraEntries; }
   nsCString GetAnnotationWhiteList() const { return mAnnotationWhitelist; }
   nsCString GetAnnotationWhiteListExtraEntries() const { return mAnnotationWhitelistExtraEntries; }
   nsCString GetTrackingBlackList() const { return mTrackingBlacklist; }
   nsCString GetTrackingBlackListExtraEntries() { return mTrackingBlacklistExtraEntries; }
   nsCString GetTrackingWhiteList() const { return mTrackingWhitelist; }
   nsCString GetTrackingWhiteListExtraEntries() { return mTrackingWhitelistExtraEntries; }
 
   void SetSkipHostnames(const nsACString& aHostnames) { mSkipHostnames = aHostnames; }
+  void SetSkipTrackingAnnotationURLs(const nsACString& aURLs) { mSkipTrackingAnnotationURLs = aURLs; }
   void SetAnnotationBlackList(const nsACString& aList) { mAnnotationBlacklist = aList; }
   void SetAnnotationBlackListExtraEntries(const nsACString& aList) { mAnnotationBlacklistExtraEntries = aList; }
   void SetAnnotationWhiteList(const nsACString& aList) { mAnnotationWhitelist = aList; }
   void SetAnnotationWhiteListExtraEntries(const nsACString& aList) { mAnnotationWhitelistExtraEntries = aList; }
   void SetTrackingBlackList(const nsACString& aList) { mTrackingBlacklist = aList; }
   void SetTrackingBlackListExtraEntries(const nsACString& aList) { mTrackingBlacklistExtraEntries = aList; }
   void SetTrackingWhiteList(const nsACString& aList) { mTrackingWhitelist = aList; }
   void SetTrackingWhiteListExtraEntries(const nsACString& aList) { mTrackingWhitelistExtraEntries = aList; }
@@ -130,16 +133,17 @@ private:
   // list.
   static bool sAnnotateChannelEnabled;
   // Whether the priority of the channels annotated as being on the tracking
   // protection list should be lowered.
   static bool sLowerNetworkPriority;
   static bool sAllowListExample;
 
   nsCString mSkipHostnames;
+  nsCString mSkipTrackingAnnotationURLs;
   nsCString mAnnotationBlacklist;
   nsCString mAnnotationBlacklistExtraEntries;
   nsCString mAnnotationWhitelist;
   nsCString mAnnotationWhitelistExtraEntries;
   nsCString mTrackingBlacklist;
   nsCString mTrackingBlacklistExtraEntries;
   nsCString mTrackingWhitelist;
   nsCString mTrackingWhitelistExtraEntries;
@@ -157,16 +161,21 @@ StaticAutoPtr<CachedPrefs> CachedPrefs::
 void
 CachedPrefs::OnPrefsChange(const char* aPref, CachedPrefs* aPrefs)
 {
   if (!strcmp(aPref, URLCLASSIFIER_SKIP_HOSTNAMES)) {
     nsCString skipHostnames;
     Preferences::GetCString(URLCLASSIFIER_SKIP_HOSTNAMES, skipHostnames);
     ToLowerCase(skipHostnames);
     aPrefs->SetSkipHostnames(skipHostnames);
+  } else if (!strcmp(aPref, URLCLASSIFIER_TRACKING_ANNOTATION_SKIP_URLS)) {
+    nsCString skipTrackingAnnotationURLs;
+    Preferences::GetCString(URLCLASSIFIER_TRACKING_ANNOTATION_SKIP_URLS, skipTrackingAnnotationURLs);
+    ToLowerCase(skipTrackingAnnotationURLs);
+    aPrefs->SetSkipTrackingAnnotationURLs(skipTrackingAnnotationURLs);
   } else if (!strcmp(aPref, URLCLASSIFIER_ANNOTATION_TABLE)) {
     nsAutoCString annotationBlacklist;
     Preferences::GetCString(URLCLASSIFIER_ANNOTATION_TABLE,
                             annotationBlacklist);
     aPrefs->SetAnnotationBlackList(annotationBlacklist);
   } else if (!strcmp(aPref, URLCLASSIFIER_ANNOTATION_TABLE_TEST_ENTRIES)) {
     nsAutoCString annotationBlacklistExtraEntries;
     Preferences::GetCString(URLCLASSIFIER_ANNOTATION_TABLE_TEST_ENTRIES,
@@ -210,16 +219,18 @@ CachedPrefs::Init()
                                "privacy.trackingprotection.annotate_channels");
   Preferences::AddBoolVarCache(&sLowerNetworkPriority,
                                "privacy.trackingprotection.lower_network_priority");
   Preferences::AddBoolVarCache(&sAllowListExample,
                                "channelclassifier.allowlist_example");
   Preferences::RegisterCallbackAndCall(CachedPrefs::OnPrefsChange,
                                        URLCLASSIFIER_SKIP_HOSTNAMES, this);
   Preferences::RegisterCallbackAndCall(CachedPrefs::OnPrefsChange,
+                                       URLCLASSIFIER_TRACKING_ANNOTATION_SKIP_URLS, this);
+  Preferences::RegisterCallbackAndCall(CachedPrefs::OnPrefsChange,
                                        URLCLASSIFIER_ANNOTATION_TABLE, this);
   Preferences::RegisterCallbackAndCall(CachedPrefs::OnPrefsChange,
                                        URLCLASSIFIER_ANNOTATION_TABLE_TEST_ENTRIES, this);
   Preferences::RegisterCallbackAndCall(CachedPrefs::OnPrefsChange,
                                        URLCLASSIFIER_ANNOTATION_WHITELIST, this);
   Preferences::RegisterCallbackAndCall(CachedPrefs::OnPrefsChange,
                                        URLCLASSIFIER_ANNOTATION_WHITELIST_TEST_ENTRIES, this);
   Preferences::RegisterCallbackAndCall(CachedPrefs::OnPrefsChange,
@@ -250,16 +261,17 @@ CachedPrefs::CachedPrefs()
   MOZ_COUNT_CTOR(CachedPrefs);
 }
 
 CachedPrefs::~CachedPrefs()
 {
   MOZ_COUNT_DTOR(CachedPrefs);
 
   Preferences::UnregisterCallback(CachedPrefs::OnPrefsChange, URLCLASSIFIER_SKIP_HOSTNAMES, this);
+  Preferences::UnregisterCallback(CachedPrefs::OnPrefsChange, URLCLASSIFIER_TRACKING_ANNOTATION_SKIP_URLS, this);
   Preferences::UnregisterCallback(CachedPrefs::OnPrefsChange, URLCLASSIFIER_ANNOTATION_TABLE, this);
   Preferences::UnregisterCallback(CachedPrefs::OnPrefsChange, URLCLASSIFIER_ANNOTATION_TABLE_TEST_ENTRIES, this);
   Preferences::UnregisterCallback(CachedPrefs::OnPrefsChange, URLCLASSIFIER_ANNOTATION_WHITELIST, this);
   Preferences::UnregisterCallback(CachedPrefs::OnPrefsChange, URLCLASSIFIER_ANNOTATION_WHITELIST_TEST_ENTRIES, this);
   Preferences::UnregisterCallback(CachedPrefs::OnPrefsChange, URLCLASSIFIER_TRACKING_WHITELIST, this);
   Preferences::UnregisterCallback(CachedPrefs::OnPrefsChange, URLCLASSIFIER_TRACKING_WHITELIST_TEST_ENTRIES, this);
   Preferences::UnregisterCallback(CachedPrefs::OnPrefsChange, URLCLASSIFIER_TRACKING_TABLE, this);
   Preferences::UnregisterCallback(CachedPrefs::OnPrefsChange, URLCLASSIFIER_TRACKING_TABLE_TEST_ENTRIES, this);
@@ -758,16 +770,22 @@ nsChannelClassifier::IsHostnameWhitelist
            this, host.get()));
       return true;
     }
   }
 
   return false;
 }
 
+bool
+nsChannelClassifier::IsTrackingURLWhitelisted(nsIURI *aUri)
+{
+  return nsContentUtils::IsURIInList(aUri, CachedPrefs::GetInstance()->GetSkipTrackingAnnotationURLs());
+}
+
 // Note in the cache entry that this URL was classified, so that future
 // cached loads don't need to be checked.
 void
 nsChannelClassifier::MarkEntryClassified(nsresult status)
 {
     // Should only be called in the parent process.
     MOZ_ASSERT(XRE_IsParentProcess());
 
@@ -1202,35 +1220,44 @@ TrackingURICallback::OnBlacklistResult(n
 }
 
 nsresult
 TrackingURICallback::OnWhitelistResult(nsresult aErrorCode)
 {
   LOG_DEBUG(("TrackingURICallback[%p]::OnWhitelistResult aErrorCode=0x%" PRIx32,
              mChannelClassifier.get(), static_cast<uint32_t>(aErrorCode)));
 
+  // Change our error code if the URL has been whitelisted.
+  nsCOMPtr<nsIChannel> channel = mChannelClassifier->GetChannel();
+  nsCOMPtr<nsIURI> uri;
+  channel->GetURI(getter_AddRefs(uri));
+  if (!uri) {
+    return NS_ERROR_UNEXPECTED;
+  }
+  bool prefBasedWhitelistUsed = false;
+  if (aErrorCode == NS_ERROR_TRACKING_ANNOTATION_URI &&
+      mChannelClassifier->IsTrackingURLWhitelisted(uri)) {
+    aErrorCode = NS_OK;
+    prefBasedWhitelistUsed = true;
+  }
+
   if (NS_SUCCEEDED(aErrorCode)) {
     if (LOG_ENABLED()) {
-      nsCOMPtr<nsIChannel> channel = mChannelClassifier->GetChannel();
-      nsCOMPtr<nsIURI> uri;
-      channel->GetURI(getter_AddRefs(uri));
       nsCString spec = uri->GetSpecOrDefault();
       spec.Truncate(std::min(spec.Length(), sMaxSpecLength));
       LOG(("TrackingURICallback[%p]::OnWhitelistResult uri %s found "
-           "in whitelist so we won't block it", mChannelClassifier.get(),
-           spec.get()));
+           "in %s whitelist so we won't block it", mChannelClassifier.get(),
+           spec.get(), prefBasedWhitelistUsed ? "pref-based" :
+           "tracking protection"));
     }
     mChannelCallback();
     return NS_OK;
   }
 
   if (LOG_ENABLED()) {
-    nsCOMPtr<nsIChannel> channel = mChannelClassifier->GetChannel();
-    nsCOMPtr<nsIURI> uri;
-    channel->GetURI(getter_AddRefs(uri));
     nsCString spec = uri->GetSpecOrDefault();
     spec.Truncate(std::min(spec.Length(), sMaxSpecLength));
     LOG(("TrackingURICallback[%p]::OnWhitelistResult "
          "channel[%p] uri=%s, should not be whitelisted",
          mChannelClassifier.get(), channel.get(), spec.get()));
   }
 
   OnTrackerFound(aErrorCode);
--- a/netwerk/base/nsChannelClassifier.h
+++ b/netwerk/base/nsChannelClassifier.h
@@ -56,16 +56,19 @@ public:
     // and the caller should wait for the result.
     nsresult CheckIsTrackerWithLocalTable(std::function<void()>&& aCallback);
 
     // Helper function to create a whitelist URL.
     nsresult CreateWhiteListURI(nsIURI** aURI) const;
 
     already_AddRefed<nsIChannel> GetChannel();
 
+    // Helper function to check a URI against the tracking skip URL whitelist
+    bool IsTrackingURLWhitelisted(nsIURI *aUri);
+
 private:
     // True if the channel is on the allow list.
     bool mIsAllowListed;
     // True if the channel has been suspended.
     bool mSuspendedChannel;
     nsCOMPtr<nsIChannel> mChannel;
     Maybe<bool> mTrackingProtectionEnabled;
     Maybe<bool> mTrackingAnnotationEnabled;
--- a/testing/awsy/awsy/test_base_memory_usage.py
+++ b/testing/awsy/awsy/test_base_memory_usage.py
@@ -46,20 +46,23 @@ class TestMemoryUsage(AwsyTestCase):
 
     def setUp(self):
         AwsyTestCase.setUp(self)
         self.logger.info("setting up!")
 
         # Override AwsyTestCase value, this is always going to be 1 iteration.
         self._iterations = 1
 
-        self._urls = ['about:blank'] * 4
+        # We don't want to measure the preallocated process, so we load enough
+        # tabs so that it is no longer launched.
+        process_count = self.marionette.get_pref('dom.ipc.processCount')
+        self._urls = ['about:blank'] * process_count
 
-        self.logger.info("areweslimyet run by %d pages, %d iterations, %d perTabPause, %d settleWaitTime"
-                         % (self._pages_to_load, self._iterations, self._perTabPause, self._settleWaitTime))
+        self.logger.info("areweslimyet run by %d pages, %d iterations, %d perTabPause, %d settleWaitTime, %d content processes"
+                         % (self._pages_to_load, self._iterations, self._perTabPause, self._settleWaitTime, process_count))
         self.logger.info("done setting up!")
 
     def tearDown(self):
         self.logger.info("tearing down!")
         AwsyTestCase.tearDown(self)
         self.logger.info("done tearing down!")
 
     def test_open_tabs(self):
--- a/toolkit/components/antitracking/test/browser/browser.ini
+++ b/toolkit/components/antitracking/test/browser/browser.ini
@@ -39,16 +39,17 @@ skip-if = (os == "win" && os_version == 
 [browser_imageCache4-2.js]
 [browser_imageCache8.js]
 [browser_onBeforeRequestNotificationForTrackingResources.js]
 [browser_onModifyRequestNotificationForTrackingResources.js]
 [browser_permissionInNormalWindows.js]
 skip-if = serviceworker_e10s
 [browser_permissionInPrivateWindows.js]
 skip-if = serviceworker_e10s
+[browser_siteSpecificWorkArounds.js]
 [browser_subResources.js]
 skip-if = serviceworker_e10s
 support-files = subResources.sjs
 [browser_script.js]
 support-files = tracker.js
 [browser_userInteraction.js]
 [browser_storageAccessPrivateWindow.js]
 skip-if = serviceworker_e10s
new file mode 100644
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_siteSpecificWorkArounds.js
@@ -0,0 +1,65 @@
+AntiTracking.runTest("localStorage with a tracker that is whitelisted via a pref",
+  async _ => {
+    localStorage.foo = 42;
+    ok(true, "LocalStorage is allowed");
+  },
+  async _ => {
+    localStorage.foo = 42;
+    ok(true, "LocalStorage is allowed");
+  },
+  async _ => {
+    await new Promise(resolve => {
+      Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => resolve());
+    });
+  },
+  [["urlclassifier.trackingAnnotationSkipURLs", "tracking.example.org"]],
+  false, // run the window.open() test
+  false, // run the user interaction test
+  0, // don't expect blocking notifications
+  false); // run in a normal window
+
+AntiTracking.runTest("localStorage with a tracker that is whitelisted via a fancy pref",
+  async _ => {
+    localStorage.foo = 42;
+    ok(true, "LocalStorage is allowed");
+  },
+  async _ => {
+    localStorage.foo = 42;
+    ok(true, "LocalStorage is allowed");
+  },
+  async _ => {
+    await new Promise(resolve => {
+      Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => resolve());
+    });
+  },
+  [["urlclassifier.trackingAnnotationSkipURLs", "foobar.example,*.example.org,baz.example"]],
+  false, // run the window.open() test
+  false, // run the user interaction test
+  0, // don't expect blocking notifications
+  false); // run in a normal window
+
+AntiTracking.runTest("localStorage with a tracker that is whitelisted via a misconfigured pref",
+  async _ => {
+    try {
+      localStorage.foo = 42;
+      ok(false, "LocalStorage cannot be used!");
+    } catch (e) {
+      ok(true, "LocalStorage cannot be used!");
+      is(e.name, "SecurityError", "We want a security error message.");
+    }
+  },
+  async _ => {
+    localStorage.foo = 42;
+    ok(true, "LocalStorage is allowed");
+  },
+  async _ => {
+    await new Promise(resolve => {
+      Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => resolve());
+    });
+  },
+  [["urlclassifier.trackingAnnotationSkipURLs", "*.tracking.example.org"]],
+  false, // run the window.open() test
+  false, // run the user interaction test
+  Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER, // expect blocking notifications
+  false); // run in a normal window
+
--- a/toolkit/components/extensions/WebExtensionPolicy.cpp
+++ b/toolkit/components/extensions/WebExtensionPolicy.cpp
@@ -612,17 +612,18 @@ MozDocumentMatcher::Matches(const DocInf
   if (!mMatchAboutBlank && aDoc.URL().InheritsPrincipal()) {
     return false;
   }
 
   // Top-level about:blank is a special case. We treat it as a match if
   // matchAboutBlank is true and it has the null principal. In all other
   // cases, we test the URL of the principal that it inherits.
   if (mMatchAboutBlank && aDoc.IsTopLevel() &&
-      aDoc.URL().Spec().EqualsLiteral("about:blank") &&
+      (aDoc.URL().Spec().EqualsLiteral("about:blank") ||
+       aDoc.URL().Scheme() == nsGkAtoms::data) &&
       aDoc.Principal() && aDoc.Principal()->GetIsNullPrincipal()) {
     return true;
   }
 
   if (mRestricted && mExtension->IsRestrictedDoc(aDoc)) {
     return false;
   }
 
new file mode 100644
--- /dev/null
+++ b/tools/quitter/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+  globals: {
+    cloneInto: true,
+  }
+};
new file mode 100644
--- /dev/null
+++ b/tools/quitter/background.js
@@ -0,0 +1,9 @@
+"use strict";
+
+/* eslint-env webextensions */
+
+browser.runtime.onMessage.addListener(msg => {
+  if (msg === "quit") {
+    browser.quitter.quit();
+  }
+});
deleted file mode 100644
--- a/tools/quitter/bootstrap.js
+++ /dev/null
@@ -1,42 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-ChromeUtils.import("resource://gre/modules/Services.jsm");
-
-const CHILD_SCRIPT = "chrome://quitter/content/contentscript.js";
-
-const quitterObserver = {
-  init() {
-    Services.mm.addMessageListener("Quitter.Quit", this);
-    Services.mm.loadFrameScript(CHILD_SCRIPT, true);
-  },
-
-  uninit() {
-    Services.mm.removeMessageListener("Quitter.Quit", this);
-    Services.mm.removeDelayedFrameScript(CHILD_SCRIPT, true);
-  },
-
-  /**
-   * messageManager callback function
-   * This will get requests from our API in the window and process them in chrome for it
-   **/
-  receiveMessage(aMessage) {
-    switch (aMessage.name) {
-      case "Quitter.Quit":
-        Services.startup.quit(Ci.nsIAppStartup.eForceQuit);
-        break;
-    }
-  },
-};
-
-function startup(data, reason) {
-  quitterObserver.init();
-}
-
-function shutdown(data, reason) {
-  quitterObserver.uninit();
-}
-
-function install(data, reason) {}
-function uninstall(data, reason) {}
deleted file mode 100644
--- a/tools/quitter/chrome.manifest
+++ /dev/null
@@ -1,1 +0,0 @@
-content quitter chrome/quitter/content/
--- a/tools/quitter/contentscript.js
+++ b/tools/quitter/contentscript.js
@@ -1,35 +1,13 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
-/* eslint-env mozilla/frame-script */
+"use strict";
 
-function Quitter() {
-}
+/* eslint-env webextensions */
 
-Quitter.prototype = {
-  toString() { return "[Quitter]"; },
-  quit() { sendSyncMessage("Quitter.Quit", {}); },
+const Quitter = {
+  quit() { browser.runtime.sendMessage("quit"); },
 };
 
-// This is a frame script, so it may be running in a content process.
-// In any event, it is targeted at a specific "tab", so we listen for
-// the DOMWindowCreated event to be notified about content windows
-// being created in this context.
-
-function QuitterManager() {
-  addEventListener("DOMWindowCreated", this, false);
-}
-
-QuitterManager.prototype = {
-  handleEvent: function handleEvent(aEvent) {
-    var quitter = new Quitter(window);
-    var window = aEvent.target.defaultView;
-    window.wrappedJSObject.Quitter = Cu.cloneInto({
-      toString: quitter.toString.bind(quitter),
-      quit: quitter.quit.bind(quitter),
-    }, window, {cloneFunctions: true});
-  },
-};
-
-var quittermanager = new QuitterManager();
+window.wrappedJSObject.Quitter = cloneInto(Quitter, window, {cloneFunctions: true});
deleted file mode 100644
--- a/tools/quitter/install.rdf
+++ /dev/null
@@ -1,36 +0,0 @@
-<?xml version="1.0"?>
-
-<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
-     xmlns:em="http://www.mozilla.org/2004/em-rdf#">
-
-  <Description about="urn:mozilla:install-manifest">
-    <em:id>quitter@mozilla.org</em:id>
-    <em:version>2018.03.19</em:version>
-    <em:type>2</em:type>
-    <em:bootstrap>true</em:bootstrap>
-
-    <!-- Target Application this extension can install into,
-         with minimum and maximum supported versions. -->
-    <em:targetApplication>
-      <Description>
-        <!-- Firefox -->
-        <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
-        <em:minVersion>45</em:minVersion>
-        <em:maxVersion>*</em:maxVersion>
-      </Description>
-    </em:targetApplication>
-    <em:targetApplication>
-      <Description>
-        <!-- Fennec -->
-        <em:id>{aa3c5121-dab2-40e2-81ca-7ea25febc110}</em:id>
-        <em:minVersion>45</em:minVersion>
-        <em:maxVersion>*</em:maxVersion>
-      </Description>
-    </em:targetApplication>
-
-    <!-- Front End MetaData -->
-    <em:name>Quitter</em:name>
-    <em:description>Adds a quit method that content pages can use to quit the application.</em:description>
-    <em:creator>Mozilla</em:creator>
-  </Description>
-</RDF>
deleted file mode 100644
--- a/tools/quitter/jar.mn
+++ /dev/null
@@ -1,3 +0,0 @@
-quitter.jar:
-% content quitter %content/
-  content/contentscript.js (contentscript.js)
new file mode 100644
--- /dev/null
+++ b/tools/quitter/manifest.json
@@ -0,0 +1,34 @@
+{
+  "manifest_version": 2,
+
+  "applications": {
+    "gecko": {"id": "quitter@mozilla.org"}
+  },
+
+  "name": "Quitter",
+  "description": "Quit",
+  "version": "2018.04.03",
+  "author": "Mozilla",
+
+  "background": {
+    "scripts": ["background.js"]
+  },
+
+  "content_scripts": [{
+    "js": ["contentscript.js"],
+    "run_at": "document_start",
+    "match_about_blank": true,
+    "matches": ["<all_urls>"]
+  }],
+
+  "experiment_apis": {
+    "quitter": {
+      "schema": "schema.json",
+      "parent": {
+        "scopes": ["addon_parent"],
+        "script": "parent.js",
+        "paths": [["quitter", "quit"]]
+      }
+    }
+  }
+}
--- a/tools/quitter/moz.build
+++ b/tools/quitter/moz.build
@@ -1,17 +1,17 @@
 # -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 XPI_NAME = 'quitter'
 
-JAR_MANIFESTS += ['jar.mn']
-
 USE_EXTENSION_MANIFEST = True
 
 FINAL_TARGET_FILES += [
-    'bootstrap.js',
-    'chrome.manifest',
-    'install.rdf',
+    'background.js',
+    'contentscript.js',
+    'manifest.json',
+    'parent.js',
+    'schema.json',
 ]
new file mode 100644
--- /dev/null
+++ b/tools/quitter/parent.js
@@ -0,0 +1,17 @@
+"use strict";
+
+/* globals ExtensionAPI */
+
+ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+this.quitter = class extends ExtensionAPI {
+  getAPI(context) {
+    return {
+      quitter: {
+        quit() {
+          Services.startup.quit(Ci.nsIAppStartup.eForceQuit);
+        },
+      },
+    };
+  }
+};
index f4e7d81117c12ff6c00d54cf191b398765e68ab5..acf9c87e90803540c2ce99e3ca5d4e1d2d8070bc
GIT binary patch
literal 6164
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
GL;4pEgu*!h
new file mode 100644
--- /dev/null
+++ b/tools/quitter/schema.json
@@ -0,0 +1,13 @@
+[
+  {
+    "namespace": "quitter",
+    "functions": [
+      {
+        "name": "quit",
+        "type": "function",
+        "async": true,
+        "parameters": []
+      }
+    ]
+  }
+]