From 00a32c79d14f22ff28843f647d59d5ff1450123a Mon Sep 17 00:00:00 2001
From: Mark Hammond <mhammond@skippinet.com.au>
Date: Fri, 12 Sep 2014 13:24:10 +1000
Subject: [PATCH] Bug 506446 - nsPermissionManager now reads a file with
 default permissions. r=bsmedberg

---
 browser/app/default_permissions               |   9 +
 browser/app/jar.mn                            |   4 +
 browser/app/moz.build                         |   2 +
 extensions/cookie/nsPermissionManager.cpp     | 177 ++++++++++++++++--
 extensions/cookie/nsPermissionManager.h       |  12 +-
 .../test/unit/test_permmanager_defaults.js    | 177 ++++++++++++++++++
 extensions/cookie/test/unit/xpcshell.ini      |   1 +
 7 files changed, 366 insertions(+), 16 deletions(-)
 create mode 100644 browser/app/default_permissions
 create mode 100644 browser/app/jar.mn
 create mode 100644 extensions/cookie/test/unit/test_permmanager_defaults.js

diff --git a/browser/app/default_permissions b/browser/app/default_permissions
new file mode 100644
index 0000000000000..dc7ea54726cf4
--- /dev/null
+++ b/browser/app/default_permissions
@@ -0,0 +1,9 @@
+# This file has default permissions for the permission manager.
+# The file-format is strict:
+# * matchtype \t type \t permission \t host
+# * Only "host" is supported for matchtype
+# * type is a string that identifies the type of permission (e.g. "cookie")
+# * permission is an integer between 1 and 15
+# See nsPermissionManager.cpp for more...
+
+# (This file is intentionally blank for the moment...)
diff --git a/browser/app/jar.mn b/browser/app/jar.mn
new file mode 100644
index 0000000000000..e02941263b390
--- /dev/null
+++ b/browser/app/jar.mn
@@ -0,0 +1,4 @@
+browser.jar:
+
+# The file that holds the default permissions (which is loaded by nsPermissionManager) for the browser.
+    default_permissions (default_permissions)
diff --git a/browser/app/moz.build b/browser/app/moz.build
index 3d22eab10b044..e1c51cb5a8a8d 100644
--- a/browser/app/moz.build
+++ b/browser/app/moz.build
@@ -72,3 +72,5 @@ if CONFIG['MOZ_LINKER']:
 
 if CONFIG['HAVE_CLOCK_MONOTONIC']:
     OS_LIBS += CONFIG['REALTIME_LIBS']
+
+JAR_MANIFESTS += ['jar.mn']
diff --git a/extensions/cookie/nsPermissionManager.cpp b/extensions/cookie/nsPermissionManager.cpp
index 6192c4575e53e..2382b9a56b26f 100644
--- a/extensions/cookie/nsPermissionManager.cpp
+++ b/extensions/cookie/nsPermissionManager.cpp
@@ -20,6 +20,7 @@
 #include "nsILineInputStream.h"
 #include "nsIIDNService.h"
 #include "nsAppDirectoryServiceDefs.h"
+#include "nsDirectoryServiceDefs.h"
 #include "prprf.h"
 #include "mozilla/storage.h"
 #include "mozilla/Attributes.h"
@@ -33,6 +34,8 @@
 #include "nsPIDOMWindow.h"
 #include "nsIDocument.h"
 #include "mozilla/net/NeckoMessageUtils.h"
+#include "mozilla/Preferences.h"
+#include "nsReadLine.h"
 
 static nsPermissionManager *gPermissionManager = nullptr;
 
@@ -358,6 +361,12 @@ static const char kPermissionsFileName[] = "permissions.sqlite";
 
 static const char kHostpermFileName[] = "hostperm.1";
 
+// Default permissions are read from a URL - this is the preference we read
+// to find that URL.
+static const char kDefaultsUrlPrefName[] = "permissions.manager.defaultsUrl";
+// If the pref above doesn't exist, the URL we use by default.
+static const char kDefaultsUrl[] = "resource://app/chrome/browser/default_permissions";
+
 static const char kPermissionChangeNotification[] = PERM_CHANGE_NOTIFICATION;
 
 NS_IMPL_ISUPPORTS(nsPermissionManager, nsIPermissionManager, nsIObserver, nsISupportsWeakReference)
@@ -589,6 +598,8 @@ nsPermissionManager::InitDB(bool aRemoveFile)
     getter_AddRefs(mStmtUpdate));
   NS_ENSURE_SUCCESS(rv, rv);
 
+  // Always import default permissions.
+  ImportDefaults();
   // check whether to import or just read in the db
   if (tableExists)
     return Read();
@@ -743,6 +754,12 @@ nsPermissionManager::AddInternal(nsIPrincipal* aPrincipal,
         (aExpireType == nsIPermissionManager::EXPIRE_NEVER ||
          aExpireTime == oldPermissionEntry.mExpireTime))
       op = eOperationNone;
+    else if (oldPermissionEntry.mID == cIDPermissionIsDefault)
+      // The existing permission is one added as a default and the new permission
+      // doesn't exactly match so we are replacing the default.  This is true
+      // even if the new permission is UNKNOWN_ACTION (which means a "logical
+      // remove" of the default)
+      op = eOperationReplacingDefault;
     else if (aPermission == nsIPermissionManager::UNKNOWN_ACTION)
       op = eOperationRemoving;
     else
@@ -869,6 +886,62 @@ nsPermissionManager::AddInternal(nsIPrincipal* aPrincipal,
 
       break;
     }
+  case eOperationReplacingDefault:
+    {
+      // this is handling the case when we have an existing permission
+      // entry that was created as a "default" (and thus isn't in the DB) with
+      // an explicit permission (that may include UNKNOWN_ACTION.)
+      // Note we will *not* get here if we are replacing an already replaced
+      // default value - that is handled as eOperationChanging.
+
+      // So this is a hybrid of eOperationAdding (as we are writing a new entry
+      // to the DB) and eOperationChanging (as we are replacing the in-memory
+      // repr and sending a "changed" notification).
+
+      // We want a new ID even if not writing to the DB, so the modified entry
+      // in memory doesn't have the magic cIDPermissionIsDefault value.
+      id = ++mLargestID;
+
+      // The default permission being replaced can't have session expiry.
+      NS_ENSURE_TRUE(entry->GetPermissions()[index].mExpireType != nsIPermissionManager::EXPIRE_SESSION,
+                     NS_ERROR_UNEXPECTED);
+      // We don't support the new entry having any expiry - supporting that would
+      // make things far more complex and none of the permissions we set as a
+      // default support that.
+      NS_ENSURE_TRUE(aExpireType == EXPIRE_NEVER, NS_ERROR_UNEXPECTED);
+
+      // update the existing entry in memory.
+      entry->GetPermissions()[index].mID = id;
+      entry->GetPermissions()[index].mPermission = aPermission;
+      entry->GetPermissions()[index].mExpireType = aExpireType;
+      entry->GetPermissions()[index].mExpireTime = aExpireTime;
+
+      // If requested, create the entry in the DB.
+      if (aDBOperation == eWriteToDB) {
+        uint32_t appId;
+        rv = aPrincipal->GetAppId(&appId);
+        NS_ENSURE_SUCCESS(rv, rv);
+
+        bool isInBrowserElement;
+        rv = aPrincipal->GetIsInBrowserElement(&isInBrowserElement);
+        NS_ENSURE_SUCCESS(rv, rv);
+
+        UpdateDB(eOperationAdding, mStmtInsert, id, host, aType, aPermission, aExpireType, aExpireTime, appId, isInBrowserElement);
+      }
+
+      if (aNotifyOperation == eNotify) {
+        NotifyObserversWithPermission(host,
+                                      entry->GetKey()->mAppId,
+                                      entry->GetKey()->mIsInBrowserElement,
+                                      mTypeArray[typeIndex],
+                                      aPermission,
+                                      aExpireType,
+                                      aExpireTime,
+                                      MOZ_UTF16("changed"));
+      }
+
+    }
+    break;
   }
 
   return NS_OK;
@@ -944,6 +1017,10 @@ nsPermissionManager::RemoveAllInternal(bool aNotifyObservers)
   // database is authoritative, we do not need confirmation from the
   // on-disk database to notify observers.
   RemoveAllFromMemory();
+
+  // Re-import the defaults
+  ImportDefaults();
+
   if (aNotifyObservers) {
     NotifyObservers(nullptr, MOZ_UTF16("cleared"));
   }
@@ -1262,6 +1339,13 @@ AddPermissionsToList(nsPermissionManager::PermissionHashKey* entry, void *arg)
   for (uint32_t i = 0; i < entry->GetPermissions().Length(); ++i) {
     nsPermissionManager::PermissionEntry& permEntry = entry->GetPermissions()[i];
 
+    // given how "default" permissions work and the possibility of them being
+    // overridden with UNKNOWN_ACTION, we might see this value here - but we
+    // do *not* want to return them via the enumerator.
+    if (permEntry.mPermission == nsIPermissionManager::UNKNOWN_ACTION) {
+      continue;
+    }
+
     nsPermission *perm = new nsPermission(entry->GetKey()->mHost,
                                           entry->GetKey()->mAppId,
                                           entry->GetKey()->mIsInBrowserElement,
@@ -1633,11 +1717,12 @@ nsPermissionManager::Read()
 
 static const char kMatchTypeHost[] = "host";
 
+// Import() will read a file from the profile directory and add them to the
+// database before deleting the file - ie, this is a one-shot operation that
+// will not succeed on subsequent runs as the file imported from is removed.
 nsresult
 nsPermissionManager::Import()
 {
-  ENSURE_NOT_CHILD_PROCESS;
-
   nsresult rv;
 
   nsCOMPtr<nsIFile> permissionsFile;
@@ -1650,14 +1735,73 @@ nsPermissionManager::Import()
   nsCOMPtr<nsIInputStream> fileInputStream;
   rv = NS_NewLocalFileInputStream(getter_AddRefs(fileInputStream),
                                   permissionsFile);
-  if (NS_FAILED(rv)) return rv;
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  rv = _DoImport(fileInputStream, mDBConn);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  // we successfully imported and wrote to the DB - delete the old file.
+  permissionsFile->Remove(false);
+  return NS_OK;
+}
+
+// ImportDefaults will read a URL with default permissions and add them to the
+// in-memory copy of permissions.  The database is *not* written to.
+nsresult
+nsPermissionManager::ImportDefaults()
+{
+  // We allow prefs to override the default permissions URI, mainly as a hook
+  // for testing.
+  nsCString defaultsURL;
+  if (mozilla::Preferences::HasUserValue(kDefaultsUrlPrefName)) {
+    defaultsURL = mozilla::Preferences::GetCString(kDefaultsUrlPrefName);
+  } else {
+    defaultsURL = NS_LITERAL_CSTRING(kDefaultsUrl);
+  }
+
+  nsresult rv;
+  nsCOMPtr<nsIIOService> ioservice =
+    do_GetService("@mozilla.org/network/io-service;1", &rv);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  nsCOMPtr<nsIURI> defaultsURI;
+  rv = NS_NewURI(getter_AddRefs(defaultsURI), defaultsURL,
+                 nullptr, nullptr, ioservice);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  nsCOMPtr<nsIChannel> channel;
+  rv = ioservice->NewChannelFromURI(defaultsURI, getter_AddRefs(channel));
+  NS_ENSURE_SUCCESS(rv, rv);
 
-  nsCOMPtr<nsILineInputStream> lineInputStream = do_QueryInterface(fileInputStream, &rv);
+  nsCOMPtr<nsIInputStream> inputStream;
+  rv = channel->Open(getter_AddRefs(inputStream));
   NS_ENSURE_SUCCESS(rv, rv);
 
+  rv = _DoImport(inputStream, nullptr);
+  inputStream->Close();
+  return rv;
+}
+
+// _DoImport reads the specified stream and adds the parsed elements.  If
+// |conn| is passed, the imported data will be written to the database, but if
+// |conn| is null the data will be added only to the in-memory copy of the
+// database.
+nsresult
+nsPermissionManager::_DoImport(nsIInputStream *inputStream, mozIStorageConnection *conn)
+{
+  ENSURE_NOT_CHILD_PROCESS;
+
+  nsresult rv;
   // start a transaction on the storage db, to optimize insertions.
   // transaction will automically commit on completion
-  mozStorageTransaction transaction(mDBConn, true);
+  // (note the transaction is a no-op if a null connection is passed)
+  mozStorageTransaction transaction(conn, true);
+
+  // The DB operation - we only try and write if a connection was passed.
+  DBOperationType operation = conn ? eWriteToDB : eNoDBOperation;
+  // and if no DB connection was passed we assume this is a "default" permission,
+  // so use the special ID which indicates this.
+  int64_t id = conn ? 0 : cIDPermissionIsDefault;
 
   /* format is:
    * matchtype \t type \t permission \t host
@@ -1666,17 +1810,24 @@ nsPermissionManager::Import()
    * permission is an integer between 1 and 15
    */
 
-  nsAutoCString buffer;
+  // Ideally we'd do this with nsILineInputString, but this is called with an
+  // nsIInputStream that comes from a resource:// URI, which doesn't support
+  // that interface.  So NS_ReadLine to the rescue...
+  nsLineBuffer<char> lineBuffer;
+  nsCString line;
   bool isMore = true;
-  while (isMore && NS_SUCCEEDED(lineInputStream->ReadLine(buffer, &isMore))) {
-    if (buffer.IsEmpty() || buffer.First() == '#') {
+  do {
+    rv = NS_ReadLine(inputStream, &lineBuffer, line, &isMore);
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    if (line.IsEmpty() || line.First() == '#') {
       continue;
     }
 
     nsTArray<nsCString> lineArray;
 
     // Split the line at tabs
-    ParseString(buffer, '\t', lineArray);
+    ParseString(line, '\t', lineArray);
 
     if (lineArray[0].EqualsLiteral(kMatchTypeHost) &&
         lineArray.Length() == 4) {
@@ -1697,14 +1848,12 @@ nsPermissionManager::Import()
       nsresult rv = GetPrincipal(lineArray[3], getter_AddRefs(principal));
       NS_ENSURE_SUCCESS(rv, rv);
 
-      rv = AddInternal(principal, lineArray[1], permission, 0,
-                       nsIPermissionManager::EXPIRE_NEVER, 0, eDontNotify, eWriteToDB);
+      rv = AddInternal(principal, lineArray[1], permission, id,
+                       nsIPermissionManager::EXPIRE_NEVER, 0, eDontNotify, operation);
       NS_ENSURE_SUCCESS(rv, rv);
     }
-  }
 
-  // we're done importing - delete the old file
-  permissionsFile->Remove(false);
+  } while (isMore);
 
   return NS_OK;
 }
diff --git a/extensions/cookie/nsPermissionManager.h b/extensions/cookie/nsPermissionManager.h
index ec8bac866118f..1755d891bc236 100644
--- a/extensions/cookie/nsPermissionManager.h
+++ b/extensions/cookie/nsPermissionManager.h
@@ -11,7 +11,7 @@
 #include "nsIObserverService.h"
 #include "nsWeakReference.h"
 #include "nsCOMPtr.h"
-#include "nsIFile.h"
+#include "nsIInputStream.h"
 #include "nsTHashtable.h"
 #include "nsTArray.h"
 #include "nsString.h"
@@ -175,7 +175,8 @@ public:
     eOperationNone,
     eOperationAdding,
     eOperationRemoving,
-    eOperationChanging
+    eOperationChanging,
+    eOperationReplacingDefault
   };
 
   enum DBOperationType {
@@ -188,6 +189,11 @@ public:
     eNotify
   };
 
+  // A special value for a permission ID that indicates the ID was loaded as
+  // a default value.  These will never be written to the database, but may
+  // be overridden with an explicit permission (including UNKNOWN_ACTION)
+  static const int64_t cIDPermissionIsDefault = -1;
+
   nsresult AddInternal(nsIPrincipal* aPrincipal,
                        const nsAFlatCString &aType,
                        uint32_t aPermission,
@@ -226,6 +232,8 @@ private:
   nsresult InitDB(bool aRemoveFile);
   nsresult CreateTable();
   nsresult Import();
+  nsresult ImportDefaults();
+  nsresult _DoImport(nsIInputStream *inputStream, mozIStorageConnection *aConn);
   nsresult Read();
   void     NotifyObserversWithPermission(const nsACString &aHost,
                                          uint32_t          aAppId,
diff --git a/extensions/cookie/test/unit/test_permmanager_defaults.js b/extensions/cookie/test/unit/test_permmanager_defaults.js
new file mode 100644
index 0000000000000..ff0c454262ae0
--- /dev/null
+++ b/extensions/cookie/test/unit/test_permmanager_defaults.js
@@ -0,0 +1,177 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// The origin we use in most of the tests.
+const TEST_ORIGIN = "example.org";
+const TEST_PERMISSION = "test-permission";
+
+function run_test() {
+  run_next_test();
+}
+
+add_task(function* do_test() {
+  // setup a profile.
+  do_get_profile();
+
+  // create a file in the temp directory with the defaults.
+  let file = do_get_tempdir();
+  file.append("test_default_permissions");
+
+  // write our test data to it.
+  let ostream = Cc["@mozilla.org/network/file-output-stream;1"].
+                createInstance(Ci.nsIFileOutputStream);
+  ostream.init(file, -1, 0666, 0);
+  let conv = Cc["@mozilla.org/intl/converter-output-stream;1"].
+             createInstance(Ci.nsIConverterOutputStream);
+  conv.init(ostream, "UTF-8", 0, 0);
+
+  conv.writeString("# this is a comment\n");
+  conv.writeString("\n"); // a blank line!
+  conv.writeString("host\t" + TEST_PERMISSION + "\t1\t" + TEST_ORIGIN + "\n");
+  ostream.close();
+
+  // Set the preference used by the permission manager so the file is read.
+  Services.prefs.setCharPref("permissions.manager.defaultsUrl", "file://" + file.path);
+
+  // initialize the permission manager service - it will read that default.
+  let pm = Cc["@mozilla.org/permissionmanager;1"].
+           getService(Ci.nsIPermissionManager);
+
+  // test the default permission was applied.
+  let permURI = NetUtil.newURI("http://" + TEST_ORIGIN);
+  let principal = Services.scriptSecurityManager.getNoAppCodebasePrincipal(permURI);
+
+  do_check_eq(Ci.nsIPermissionManager.ALLOW_ACTION,
+              pm.testPermissionFromPrincipal(principal, TEST_PERMISSION));
+
+  // the permission should exist in the enumerator.
+  do_check_eq(Ci.nsIPermissionManager.ALLOW_ACTION, findCapabilityViaEnum());
+  // but should not have been written to the DB
+  yield checkCapabilityViaDB(null);
+
+  // remove all should not throw and the default should remain
+  pm.removeAll();
+
+  do_check_eq(Ci.nsIPermissionManager.ALLOW_ACTION,
+              pm.testPermissionFromPrincipal(principal, TEST_PERMISSION));
+
+  // Asking for this permission to be removed should result in that permission
+  // having UNKNOWN_ACTION
+  pm.removeFromPrincipal(principal, TEST_PERMISSION);
+  do_check_eq(Ci.nsIPermissionManager.UNKNOWN_ACTION,
+              pm.testPermissionFromPrincipal(principal, TEST_PERMISSION));
+  // and we should have this UNKNOWN_ACTION reflected in the DB
+  yield checkCapabilityViaDB(Ci.nsIPermissionManager.UNKNOWN_ACTION);
+  // but the permission should *not* appear in the enumerator.
+  do_check_eq(null, findCapabilityViaEnum());
+
+  // and a subsequent RemoveAll should restore the default
+  pm.removeAll();
+
+  do_check_eq(Ci.nsIPermissionManager.ALLOW_ACTION,
+              pm.testPermissionFromPrincipal(principal, TEST_PERMISSION));
+  // and allow it to again be seen in the enumerator.
+  do_check_eq(Ci.nsIPermissionManager.ALLOW_ACTION, findCapabilityViaEnum());
+
+  // now explicitly add a permission - this too should override the default.
+  pm.addFromPrincipal(principal, TEST_PERMISSION, Ci.nsIPermissionManager.DENY_ACTION);
+
+  // it should be reflected in a permission check, in the enumerator and the DB
+  do_check_eq(Ci.nsIPermissionManager.DENY_ACTION,
+              pm.testPermissionFromPrincipal(principal, TEST_PERMISSION));
+  do_check_eq(Ci.nsIPermissionManager.DENY_ACTION, findCapabilityViaEnum());
+  yield checkCapabilityViaDB(Ci.nsIPermissionManager.DENY_ACTION);
+
+  // explicitly add a different permission - in this case we are no longer
+  // replacing the default, but instead replacing the replacement!
+  pm.addFromPrincipal(principal, TEST_PERMISSION, Ci.nsIPermissionManager.PROMPT_ACTION);
+
+  // it should be reflected in a permission check, in the enumerator and the DB
+  do_check_eq(Ci.nsIPermissionManager.PROMPT_ACTION,
+              pm.testPermissionFromPrincipal(principal, TEST_PERMISSION));
+  do_check_eq(Ci.nsIPermissionManager.PROMPT_ACTION, findCapabilityViaEnum());
+  yield checkCapabilityViaDB(Ci.nsIPermissionManager.PROMPT_ACTION);
+
+  // remove the temp file we created.
+  file.remove(false);
+});
+
+// use an enumerator to find the requested permission.  Returns the permission
+// value (ie, the "capability" in nsIPermission parlance) or null if it can't
+// be found.
+function findCapabilityViaEnum(host = TEST_ORIGIN, type = TEST_PERMISSION) {
+  let result = undefined;
+  let e = Services.perms.enumerator;
+  while (e.hasMoreElements()) {
+    let perm = e.getNext().QueryInterface(Ci.nsIPermission);
+    if (perm.host == host &&
+        perm.type == type) {
+      if (result !== undefined) {
+        // we've already found one previously - that's bad!
+        do_throw("enumerator found multiple entries");
+      }
+      result = perm.capability;
+    }
+  }
+  return result || null;
+}
+
+// A function to check the DB has the specified capability.  As the permission
+// manager uses async DB operations without a completion callback, the
+// distinct possibility exists that our checking of the DB will happen before
+// the permission manager update has completed - so we just retry a few times.
+// Returns a promise.
+function checkCapabilityViaDB(expected, host = TEST_ORIGIN, type = TEST_PERMISSION) {
+  let deferred = Promise.defer();
+  let count = 0;
+  let max = 20;
+  let do_check = () => {
+    let got = findCapabilityViaDB(host, type);
+    if (got == expected) {
+      // the do_check_eq() below will succeed - which is what we want.
+      do_check_eq(got, expected, "The database has the expected value");
+      deferred.resolve();
+      return;
+    }
+    // value isn't correct - see if we've retried enough
+    if (count++ == max) {
+      // the do_check_eq() below will fail - which is what we want.
+      do_check_eq(got, expected, "The database wasn't updated with the expected value");
+      deferred.resolve();
+      return;
+    }
+    // we can retry...
+    do_timeout(100, do_check);
+  }
+  do_check();
+  return deferred.promise;
+}
+
+// use the DB to find the requested permission.   Returns the permission
+// value (ie, the "capability" in nsIPermission parlance) or null if it can't
+// be found.
+function findCapabilityViaDB(host = TEST_ORIGIN, type = TEST_PERMISSION) {
+  let file = Services.dirsvc.get("ProfD", Ci.nsIFile);
+  file.append("permissions.sqlite");
+
+  let storage = Cc["@mozilla.org/storage/service;1"]
+                  .getService(Ci.mozIStorageService);
+
+  let connection = storage.openDatabase(file);
+
+  let query = connection.createStatement(
+      "SELECT permission FROM moz_hosts WHERE host = :host AND type = :type");
+  query.bindByName("host", host);
+  query.bindByName("type", type);
+
+  if (!query.executeStep()) {
+    // no row
+    return null;
+  }
+  let result = query.getInt32(0);
+  if (query.executeStep()) {
+    // this is bad - we never expect more than 1 row here.
+    do_throw("More than 1 row found!")
+  }
+  return result;
+}
diff --git a/extensions/cookie/test/unit/xpcshell.ini b/extensions/cookie/test/unit/xpcshell.ini
index dcf3104909468..070183a2554e7 100644
--- a/extensions/cookie/test/unit/xpcshell.ini
+++ b/extensions/cookie/test/unit/xpcshell.ini
@@ -18,6 +18,7 @@ support-files =
 [test_cookies_thirdparty_session.js]
 [test_domain_eviction.js]
 [test_eviction.js]
+[test_permmanager_defaults.js]
 [test_permmanager_expiration.js]
 [test_permmanager_getPermissionObject.js]
 [test_permmanager_notifications.js]
-- 
GitLab