Bug 1058433 - nsPermissionManager now records the mod-time of a permission and allows removal of ones modified since a specified time. r=ehsan
authorMark Hammond <mhammond@skippinet.com.au>
Mon, 15 Sep 2014 11:33:12 +1000
changeset 205304 449fb0bd623fd7cc7174216f935c7108a516e404
parent 205303 54ca5ea4de1dacd96868be7cb520798193461bee
child 205305 02648d3811fe6989a304a42534b7babfd532e5d3
push id27484
push usercbook@mozilla.com
push dateMon, 15 Sep 2014 12:29:22 +0000
treeherdermozilla-central@56cba2986c61 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersehsan
bugs1058433
milestone35.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1058433 - nsPermissionManager now records the mod-time of a permission and allows removal of ones modified since a specified time. r=ehsan
build/automation.py.in
dom/ipc/ContentChild.cpp
extensions/cookie/nsPermissionManager.cpp
extensions/cookie/nsPermissionManager.h
extensions/cookie/test/unit/test_permmanager_defaults.js
extensions/cookie/test/unit/test_permmanager_load_invalid_entries.js
extensions/cookie/test/unit/test_permmanager_removesince.js
extensions/cookie/test/unit/xpcshell.ini
netwerk/base/public/nsIPermissionManager.idl
testing/mozbase/mozprofile/mozprofile/permissions.py
testing/mozbase/mozprofile/tests/permissions.py
--- a/build/automation.py.in
+++ b/build/automation.py.in
@@ -298,33 +298,34 @@ class Automation(object):
   def setupPermissionsDatabase(self, profileDir, permissions):
     # Included for reftest compatibility;
     # see https://bugzilla.mozilla.org/show_bug.cgi?id=688667
 
     # Open database and create table
     permDB = sqlite3.connect(os.path.join(profileDir, "permissions.sqlite"))
     cursor = permDB.cursor();
 
-    cursor.execute("PRAGMA user_version=3");
+    cursor.execute("PRAGMA user_version=4");
 
     # SQL copied from nsPermissionManager.cpp
     cursor.execute("""CREATE TABLE IF NOT EXISTS moz_hosts (
       id INTEGER PRIMARY KEY,
       host TEXT,
       type TEXT,
       permission INTEGER,
       expireType INTEGER,
       expireTime INTEGER,
+      modificationTime INTEGER,
       appId INTEGER,
       isInBrowserElement INTEGER)""")
 
     # Insert desired permissions
     for perm in permissions.keys():
       for host,allow in permissions[perm]:
-        cursor.execute("INSERT INTO moz_hosts values(NULL, ?, ?, ?, 0, 0, 0, 0)",
+        cursor.execute("INSERT INTO moz_hosts values(NULL, ?, ?, ?, 0, 0, 0, 0, 0)",
                        (host, perm, 1 if allow else 2))
 
     # Commit and close
     permDB.commit()
     cursor.close()
 
   def initializeProfile(self, profileDir,
                               extraPrefs=None,
--- a/dom/ipc/ContentChild.cpp
+++ b/dom/ipc/ContentChild.cpp
@@ -1709,22 +1709,26 @@ ContentChild::RecvAddPermission(const IP
     MOZ_ASSERT(secMan);
 
     nsCOMPtr<nsIPrincipal> principal;
     nsresult rv = secMan->GetAppCodebasePrincipal(uri, permission.appId,
                                                 permission.isInBrowserElement,
                                                 getter_AddRefs(principal));
     NS_ENSURE_SUCCESS(rv, true);
 
+    // child processes don't care about modification time.
+    int64_t modificationTime = 0;
+
     permissionManager->AddInternal(principal,
                                    nsCString(permission.type),
                                    permission.capability,
                                    0,
                                    permission.expireType,
                                    permission.expireTime,
+                                   modificationTime,
                                    nsPermissionManager::eNotify,
                                    nsPermissionManager::eNoDBOperation);
 #endif
 
     return true;
 }
 
 bool
--- a/extensions/cookie/nsPermissionManager.cpp
+++ b/extensions/cookie/nsPermissionManager.cpp
@@ -352,17 +352,17 @@ nsPermissionManager::AppClearDataObserve
   nsCOMPtr<nsIObserverService> observerService = do_GetService("@mozilla.org/observer-service;1");
   observerService->AddObserver(new AppClearDataObserver(), "webapps-clear-data", /* holdsWeak= */ false);
 }
 
 ////////////////////////////////////////////////////////////////////////////////
 // nsPermissionManager Implementation
 
 static const char kPermissionsFileName[] = "permissions.sqlite";
-#define HOSTS_SCHEMA_VERSION 3
+#define HOSTS_SCHEMA_VERSION 4
 
 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";
@@ -427,18 +427,22 @@ nsPermissionManager::Init()
 
     for (uint32_t i = 0; i < perms.Length(); i++) {
       const IPC::Permission &perm = perms[i];
 
       nsCOMPtr<nsIPrincipal> principal;
       rv = GetPrincipal(perm.host, perm.appId, perm.isInBrowserElement, getter_AddRefs(principal));
       NS_ENSURE_SUCCESS(rv, rv);
 
+      // The child process doesn't care about modification times - it neither
+      // reads nor writes, nor removes them based on the date - so 0 (which
+      // will end up as now()) is fine.
+      uint64_t modificationTime = 0;
       AddInternal(principal, perm.type, perm.capability, 0, perm.expireType,
-                  perm.expireTime, eNotify, eNoDBOperation);
+                  perm.expireTime, modificationTime, eNotify, eNoDBOperation);
     }
 
     // Stop here; we don't need the DB in the child process
     return NS_OK;
   }
 
   // ignore failure here, since it's non-fatal (we can run fine without
   // persistent storage - e.g. if there's no profile).
@@ -541,32 +545,49 @@ nsPermissionManager::InitDB(bool aRemove
         NS_ENSURE_SUCCESS(rv, rv);
 
         rv = mDBConn->SetSchemaVersion(HOSTS_SCHEMA_VERSION);
         NS_ENSURE_SUCCESS(rv, rv);
       }
 
       // fall through to the next upgrade
 
+    // Version 3->4 is the creation of the modificationTime field.
+    case 3:
+      {
+        rv = mDBConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+              "ALTER TABLE moz_hosts ADD modificationTime INTEGER"));
+        NS_ENSURE_SUCCESS(rv, rv);
+
+        // We leave the modificationTime at zero for all existing records; using
+        // now() would mean, eg, that doing "remove all from the last hour"
+        // within the first hour after migration would remove all permissions.
+
+        rv = mDBConn->SetSchemaVersion(HOSTS_SCHEMA_VERSION);
+        NS_ENSURE_SUCCESS(rv, rv);
+      }
+
+      // fall through to the next upgrade
+
     // current version.
     case HOSTS_SCHEMA_VERSION:
       break;
 
     // downgrading.
     // if columns have been added to the table, we can still use the ones we
     // understand safely. if columns have been deleted or altered, just
     // blow away the table and start from scratch! if you change the way
     // a column is interpreted, make sure you also change its name so this
     // check will catch it.
     default:
       {
         // check if all the expected columns exist
         nsCOMPtr<mozIStorageStatement> stmt;
         rv = mDBConn->CreateStatement(NS_LITERAL_CSTRING(
-          "SELECT host, type, permission, expireType, expireTime, appId, isInBrowserElement FROM moz_hosts"),
+          "SELECT host, type, permission, expireType, expireTime, modificationTime, appId, isInBrowserElement FROM moz_hosts"),
           getter_AddRefs(stmt));
         if (NS_SUCCEEDED(rv))
           break;
 
         // our columns aren't there - drop the table!
         rv = mDBConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING("DROP TABLE moz_hosts"));
         NS_ENSURE_SUCCESS(rv, rv);
 
@@ -578,28 +599,28 @@ nsPermissionManager::InitDB(bool aRemove
   }
 
   // make operations on the table asynchronous, for performance
   mDBConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING("PRAGMA synchronous = OFF"));
 
   // cache frequently used statements (for insertion, deletion, and updating)
   rv = mDBConn->CreateAsyncStatement(NS_LITERAL_CSTRING(
     "INSERT INTO moz_hosts "
-    "(id, host, type, permission, expireType, expireTime, appId, isInBrowserElement) "
-    "VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)"), getter_AddRefs(mStmtInsert));
+    "(id, host, type, permission, expireType, expireTime, modificationTime, appId, isInBrowserElement) "
+    "VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)"), getter_AddRefs(mStmtInsert));
   NS_ENSURE_SUCCESS(rv, rv);
 
   rv = mDBConn->CreateAsyncStatement(NS_LITERAL_CSTRING(
     "DELETE FROM moz_hosts "
     "WHERE id = ?1"), getter_AddRefs(mStmtDelete));
   NS_ENSURE_SUCCESS(rv, rv);
 
   rv = mDBConn->CreateAsyncStatement(NS_LITERAL_CSTRING(
     "UPDATE moz_hosts "
-    "SET permission = ?2, expireType= ?3, expireTime = ?4 WHERE id = ?1"),
+    "SET permission = ?2, expireType= ?3, expireTime = ?4, modificationTime = ?5 WHERE id = ?1"),
     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();
@@ -621,16 +642,17 @@ nsPermissionManager::CreateTable()
   return mDBConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
     "CREATE TABLE moz_hosts ("
       " id INTEGER PRIMARY KEY"
       ",host TEXT"
       ",type TEXT"
       ",permission INTEGER"
       ",expireType INTEGER"
       ",expireTime INTEGER"
+      ",modificationTime INTEGER"
       ",appId INTEGER"
       ",isInBrowserElement INTEGER"
     ")"));
 }
 
 NS_IMETHODIMP
 nsPermissionManager::Add(nsIURI     *aURI,
                          const char *aType,
@@ -674,27 +696,31 @@ nsPermissionManager::AddFromPrincipal(ns
     return NS_OK;
   }
 
   // Permissions may not be added to expanded principals.
   if (IsExpandedPrincipal(aPrincipal)) {
     return NS_ERROR_INVALID_ARG;
   }
 
+  // A modificationTime of zero will cause AddInternal to use now().
+  int64_t modificationTime = 0;
+
   return AddInternal(aPrincipal, nsDependentCString(aType), aPermission, 0,
-                     aExpireType, aExpireTime, eNotify, eWriteToDB);
+                     aExpireType, aExpireTime, modificationTime, eNotify, eWriteToDB);
 }
 
 nsresult
 nsPermissionManager::AddInternal(nsIPrincipal* aPrincipal,
                                  const nsAFlatCString &aType,
                                  uint32_t              aPermission,
                                  int64_t               aID,
                                  uint32_t              aExpireType,
                                  int64_t               aExpireTime,
+                                 int64_t               aModificationTime,
                                  NotifyOperationType   aNotifyOperation,
                                  DBOperationType       aDBOperation)
 {
   nsAutoCString host;
   nsresult rv = GetHostForPrincipal(aPrincipal, host);
   NS_ENSURE_SUCCESS(rv, rv);
 
   if (!IsChildProcess()) {
@@ -755,25 +781,37 @@ nsPermissionManager::AddInternal(nsIPrin
          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 (aID == cIDPermissionIsDefault)
+      // We are adding a default permission but a "real" permission already
+      // exists.  This almost-certainly means we just did a removeAllSince and
+      // are re-importing defaults - so we can ignore this.
+      op = eOperationNone;
     else if (aPermission == nsIPermissionManager::UNKNOWN_ACTION)
       op = eOperationRemoving;
     else
       op = eOperationChanging;
   }
 
+  // child processes should *always* be passed a modificationTime of zero.
+  MOZ_ASSERT(!IsChildProcess() || aModificationTime == 0);
+
   // do the work for adding, deleting, or changing a permission:
   // update the in-memory list, write to the db, and notify consumers.
   int64_t id;
+  if (aModificationTime == 0) {
+    aModificationTime = PR_Now() / 1000;
+  }
+
   switch (op) {
   case eOperationNone:
     {
       // nothing to do
       return NS_OK;
     }
 
   case eOperationAdding:
@@ -781,28 +819,30 @@ nsPermissionManager::AddInternal(nsIPrin
       if (aDBOperation == eWriteToDB) {
         // we'll be writing to the database - generate a known unique id
         id = ++mLargestID;
       } else {
         // we're reading from the database - use the id already assigned
         id = aID;
       }
 
-      entry->GetPermissions().AppendElement(PermissionEntry(id, typeIndex, aPermission, aExpireType, aExpireTime));
+      entry->GetPermissions().AppendElement(PermissionEntry(id, typeIndex, aPermission,
+                                                            aExpireType, aExpireTime,
+                                                            aModificationTime));
 
       if (aDBOperation == eWriteToDB && aExpireType != nsIPermissionManager::EXPIRE_SESSION) {
         uint32_t appId;
         rv = aPrincipal->GetAppId(&appId);
         NS_ENSURE_SUCCESS(rv, rv);
 
         bool isInBrowserElement;
         rv = aPrincipal->GetIsInBrowserElement(&isInBrowserElement);
         NS_ENSURE_SUCCESS(rv, rv);
 
-        UpdateDB(op, mStmtInsert, id, host, aType, aPermission, aExpireType, aExpireTime, appId, isInBrowserElement);
+        UpdateDB(op, mStmtInsert, id, host, aType, aPermission, aExpireType, aExpireTime, aModificationTime, appId, isInBrowserElement);
       }
 
       if (aNotifyOperation == eNotify) {
         NotifyObserversWithPermission(host,
                                       entry->GetKey()->mAppId,
                                       entry->GetKey()->mIsInBrowserElement,
                                       mTypeArray[typeIndex],
                                       aPermission,
@@ -819,17 +859,17 @@ nsPermissionManager::AddInternal(nsIPrin
       PermissionEntry oldPermissionEntry = entry->GetPermissions()[index];
       id = oldPermissionEntry.mID;
       entry->GetPermissions().RemoveElementAt(index);
 
       if (aDBOperation == eWriteToDB)
         // We care only about the id here so we pass dummy values for all other
         // parameters.
         UpdateDB(op, mStmtDelete, id, EmptyCString(), EmptyCString(), 0,
-                 nsIPermissionManager::EXPIRE_NEVER, 0, 0, false);
+                 nsIPermissionManager::EXPIRE_NEVER, 0, 0, 0, false);
 
       if (aNotifyOperation == eNotify) {
         NotifyObserversWithPermission(host,
                                       entry->GetKey()->mAppId,
                                       entry->GetKey()->mIsInBrowserElement,
                                       mTypeArray[typeIndex],
                                       oldPermissionEntry.mPermission,
                                       oldPermissionEntry.mExpireType,
@@ -861,22 +901,23 @@ nsPermissionManager::AddInternal(nsIPrin
         entry->GetPermissions()[index].mNonSessionPermission = aPermission;
         entry->GetPermissions()[index].mNonSessionExpireType = aExpireType;
         entry->GetPermissions()[index].mNonSessionExpireTime = aExpireTime;
       }
 
       entry->GetPermissions()[index].mPermission = aPermission;
       entry->GetPermissions()[index].mExpireType = aExpireType;
       entry->GetPermissions()[index].mExpireTime = aExpireTime;
+      entry->GetPermissions()[index].mModificationTime = aModificationTime;
 
       if (aDBOperation == eWriteToDB && aExpireType != nsIPermissionManager::EXPIRE_SESSION)
-        // We care only about the id, the permission and expireType/expireTime here.
+        // We care only about the id, the permission and expireType/expireTime/modificationTime here.
         // We pass dummy values for all other parameters.
         UpdateDB(op, mStmtUpdate, id, EmptyCString(), EmptyCString(),
-                 aPermission, aExpireType, aExpireTime, 0, false);
+                 aPermission, aExpireType, aExpireTime, aModificationTime, 0, false);
 
       if (aNotifyOperation == eNotify) {
         NotifyObserversWithPermission(host,
                                       entry->GetKey()->mAppId,
                                       entry->GetKey()->mIsInBrowserElement,
                                       mTypeArray[typeIndex],
                                       aPermission,
                                       aExpireType,
@@ -910,28 +951,30 @@ nsPermissionManager::AddInternal(nsIPrin
       // 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;
+      entry->GetPermissions()[index].mModificationTime = aModificationTime;
 
       // 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);
+        UpdateDB(eOperationAdding, mStmtInsert, id, host, aType, aPermission,
+                 aExpireType, aExpireTime, aModificationTime, appId, isInBrowserElement);
       }
 
       if (aNotifyOperation == eNotify) {
         NotifyObserversWithPermission(host,
                                       entry->GetKey()->mAppId,
                                       entry->GetKey()->mIsInBrowserElement,
                                       mTypeArray[typeIndex],
                                       aPermission,
@@ -978,27 +1021,35 @@ nsPermissionManager::RemoveFromPrincipal
 
   // AddInternal() handles removal, just let it do the work
   return AddInternal(aPrincipal,
                      nsDependentCString(aType),
                      nsIPermissionManager::UNKNOWN_ACTION,
                      0,
                      nsIPermissionManager::EXPIRE_NEVER,
                      0,
+                     0,
                      eNotify,
                      eWriteToDB);
 }
 
 NS_IMETHODIMP
 nsPermissionManager::RemoveAll()
 {
   ENSURE_NOT_CHILD_PROCESS;
   return RemoveAllInternal(true);
 }
 
+NS_IMETHODIMP
+nsPermissionManager::RemoveAllSince(int64_t aSince)
+{
+  ENSURE_NOT_CHILD_PROCESS;
+  return RemoveAllModifiedSince(aSince);
+}
+
 void
 nsPermissionManager::CloseDB(bool aRebuildOnSuccess)
 {
   // Null the statements, this will finalize them.
   mStmtInsert = nullptr;
   mStmtDelete = nullptr;
   mStmtUpdate = nullptr;
   if (mDBConn) {
@@ -1318,22 +1369,26 @@ nsPermissionManager::GetPermissionHashKe
 
   // No entry, really...
   return nullptr;
 }
 
 // helper struct for passing arguments into hash enumeration callback.
 struct nsGetEnumeratorData
 {
-  nsGetEnumeratorData(nsCOMArray<nsIPermission> *aArray, const nsTArray<nsCString> *aTypes)
+  nsGetEnumeratorData(nsCOMArray<nsIPermission> *aArray,
+                      const nsTArray<nsCString> *aTypes,
+                      int64_t aSince = 0)
    : array(aArray)
-   , types(aTypes) {}
+   , types(aTypes)
+   , since(aSince) {}
 
   nsCOMArray<nsIPermission> *array;
   const nsTArray<nsCString> *types;
+  int64_t since;
 };
 
 static PLDHashOperator
 AddPermissionsToList(nsPermissionManager::PermissionHashKey* entry, void *arg)
 {
   nsGetEnumeratorData *data = static_cast<nsGetEnumeratorData *>(arg);
 
   for (uint32_t i = 0; i < entry->GetPermissions().Length(); ++i) {
@@ -1390,16 +1445,86 @@ NS_IMETHODIMP nsPermissionManager::Obser
   else if (!nsCRT::strcmp(aTopic, "profile-do-change")) {
     // the profile has already changed; init the db from the new location
     InitDB(false);
   }
 
   return NS_OK;
 }
 
+static PLDHashOperator
+AddPermissionsModifiedSinceToList(
+  nsPermissionManager::PermissionHashKey* entry, void* arg)
+{
+  nsGetEnumeratorData* data = static_cast<nsGetEnumeratorData *>(arg);
+
+  for (size_t i = 0; i < entry->GetPermissions().Length(); ++i) {
+    const nsPermissionManager::PermissionEntry& permEntry = entry->GetPermissions()[i];
+
+    if (data->since > permEntry.mModificationTime) {
+      continue;
+    }
+
+    nsPermission* perm = new nsPermission(entry->GetKey()->mHost,
+                                          entry->GetKey()->mAppId,
+                                          entry->GetKey()->mIsInBrowserElement,
+                                          data->types->ElementAt(permEntry.mType),
+                                          permEntry.mPermission,
+                                          permEntry.mExpireType,
+                                          permEntry.mExpireTime);
+
+    data->array->AppendObject(perm);
+  }
+  return PL_DHASH_NEXT;
+}
+
+nsresult
+nsPermissionManager::RemoveAllModifiedSince(int64_t aModificationTime)
+{
+  ENSURE_NOT_CHILD_PROCESS;
+
+  // roll an nsCOMArray of all our permissions, then hand out an enumerator
+  nsCOMArray<nsIPermission> array;
+  nsGetEnumeratorData data(&array, &mTypeArray, aModificationTime);
+
+  mPermissionTable.EnumerateEntries(AddPermissionsModifiedSinceToList, &data);
+
+  for (int32_t i = 0; i<array.Count(); ++i) {
+    nsAutoCString host;
+    bool isInBrowserElement = false;
+    nsAutoCString type;
+    uint32_t appId = 0;
+
+    array[i]->GetHost(host);
+    array[i]->GetIsInBrowserElement(&isInBrowserElement);
+    array[i]->GetType(type);
+    array[i]->GetAppId(&appId);
+
+    nsCOMPtr<nsIPrincipal> principal;
+    if (NS_FAILED(GetPrincipal(host, appId, isInBrowserElement,
+                               getter_AddRefs(principal)))) {
+      NS_ERROR("GetPrincipal() failed!");
+      continue;
+    }
+    // AddInternal handles removal, so let it do the work...
+    AddInternal(
+      principal,
+      type,
+      nsIPermissionManager::UNKNOWN_ACTION,
+      0,
+      nsIPermissionManager::EXPIRE_NEVER, 0, 0,
+      nsPermissionManager::eNotify,
+      nsPermissionManager::eWriteToDB);
+  }
+  // now re-import any defaults as they may now be required if we just deleted
+  // an override.
+  ImportDefaults();
+  return NS_OK;
+}
+
 PLDHashOperator
 nsPermissionManager::GetPermissionsForApp(nsPermissionManager::PermissionHashKey* entry, void* arg)
 {
   GetPermissionsForAppStruct* data = static_cast<GetPermissionsForAppStruct*>(arg);
 
   for (uint32_t i = 0; i < entry->GetPermissions().Length(); ++i) {
     nsPermissionManager::PermissionEntry& permEntry = entry->GetPermissions()[i];
 
@@ -1470,16 +1595,17 @@ nsPermissionManager::RemovePermissionsFo
     }
 
     AddInternal(principal,
                 type,
                 nsIPermissionManager::UNKNOWN_ACTION,
                 0,
                 nsIPermissionManager::EXPIRE_NEVER,
                 0,
+                0,
                 nsPermissionManager::eNotify,
                 nsPermissionManager::eNoDBOperation);
   }
 
   return NS_OK;
 }
 
 PLDHashOperator
@@ -1641,25 +1767,26 @@ nsPermissionManager::Read()
 
     bool hasResult;
     rv = stmtDeleteExpired->ExecuteStep(&hasResult);
     NS_ENSURE_SUCCESS(rv, rv);
   }
 
   nsCOMPtr<mozIStorageStatement> stmt;
   rv = mDBConn->CreateStatement(NS_LITERAL_CSTRING(
-    "SELECT id, host, type, permission, expireType, expireTime, appId, isInBrowserElement "
+    "SELECT id, host, type, permission, expireType, expireTime, modificationTime, appId, isInBrowserElement "
     "FROM moz_hosts"), getter_AddRefs(stmt));
   NS_ENSURE_SUCCESS(rv, rv);
 
   int64_t id;
   nsAutoCString host, type;
   uint32_t permission;
   uint32_t expireType;
   int64_t expireTime;
+  int64_t modificationTime;
   uint32_t appId;
   bool isInBrowserElement;
   bool hasResult;
   bool readError = false;
 
   while (NS_SUCCEEDED(stmt->ExecuteStep(&hasResult)) && hasResult) {
     // explicitly set our entry id counter for use in AddInternal(),
     // and keep track of the largest id so we know where to pick up.
@@ -1677,35 +1804,37 @@ nsPermissionManager::Read()
     if (NS_FAILED(rv)) {
       readError = true;
       continue;
     }
 
     permission = stmt->AsInt32(3);
     expireType = stmt->AsInt32(4);
 
-    // convert into int64_t value (milliseconds)
+    // convert into int64_t values (milliseconds)
     expireTime = stmt->AsInt64(5);
+    modificationTime = stmt->AsInt64(6);
 
-    if (stmt->AsInt64(6) < 0) {
+    if (stmt->AsInt64(7) < 0) {
       readError = true;
       continue;
     }
-    appId = static_cast<uint32_t>(stmt->AsInt64(6));
-    isInBrowserElement = static_cast<bool>(stmt->AsInt32(7));
+
+    appId = static_cast<uint32_t>(stmt->AsInt64(7));
+    isInBrowserElement = static_cast<bool>(stmt->AsInt32(8));
 
     nsCOMPtr<nsIPrincipal> principal;
     nsresult rv = GetPrincipal(host, appId, isInBrowserElement, getter_AddRefs(principal));
     if (NS_FAILED(rv)) {
       readError = true;
       continue;
     }
 
     rv = AddInternal(principal, type, permission, id, expireType, expireTime,
-                     eDontNotify, eNoDBOperation);
+                     modificationTime, eDontNotify, eNoDBOperation);
     if (NS_FAILED(rv)) {
       readError = true;
       continue;
     }
   }
 
   if (readError) {
     NS_ERROR("Error occured while reading the permissions database!");
@@ -1843,18 +1972,24 @@ nsPermissionManager::_DoImport(nsIInputS
         if (NS_FAILED(rv))
           continue;
       }
 
       nsCOMPtr<nsIPrincipal> principal;
       nsresult rv = GetPrincipal(lineArray[3], getter_AddRefs(principal));
       NS_ENSURE_SUCCESS(rv, rv);
 
+      // the import file format doesn't handle modification times, so we use
+      // 0, which AddInternal will convert to now()
+      int64_t modificationTime = 0;
+
       rv = AddInternal(principal, lineArray[1], permission, id,
-                       nsIPermissionManager::EXPIRE_NEVER, 0, eDontNotify, operation);
+                       nsIPermissionManager::EXPIRE_NEVER, 0,
+                       modificationTime,
+                       eDontNotify, operation);
       NS_ENSURE_SUCCESS(rv, rv);
     }
 
   } while (isMore);
 
   return NS_OK;
 }
 
@@ -1875,16 +2010,17 @@ void
 nsPermissionManager::UpdateDB(OperationType aOp,
                               mozIStorageAsyncStatement* aStmt,
                               int64_t aID,
                               const nsACString &aHost,
                               const nsACString &aType,
                               uint32_t aPermission,
                               uint32_t aExpireType,
                               int64_t aExpireTime,
+                              int64_t aModificationTime,
                               uint32_t aAppId,
                               bool aIsInBrowserElement)
 {
   ENSURE_NOT_CHILD_PROCESS_NORET;
 
   nsresult rv;
 
   // no statement is ok - just means we don't have a profile
@@ -1907,20 +2043,23 @@ nsPermissionManager::UpdateDB(OperationT
       if (NS_FAILED(rv)) break;
 
       rv = aStmt->BindInt32ByIndex(4, aExpireType);
       if (NS_FAILED(rv)) break;
 
       rv = aStmt->BindInt64ByIndex(5, aExpireTime);
       if (NS_FAILED(rv)) break;
 
-      rv = aStmt->BindInt64ByIndex(6, aAppId);
+      rv = aStmt->BindInt64ByIndex(6, aModificationTime);
       if (NS_FAILED(rv)) break;
 
-      rv = aStmt->BindInt64ByIndex(7, aIsInBrowserElement);
+      rv = aStmt->BindInt64ByIndex(7, aAppId);
+      if (NS_FAILED(rv)) break;
+
+      rv = aStmt->BindInt64ByIndex(8, aIsInBrowserElement);
       break;
     }
 
   case eOperationRemoving:
     {
       rv = aStmt->BindInt64ByIndex(0, aID);
       break;
     }
@@ -1932,16 +2071,19 @@ nsPermissionManager::UpdateDB(OperationT
 
       rv = aStmt->BindInt32ByIndex(1, aPermission);
       if (NS_FAILED(rv)) break;
 
       rv = aStmt->BindInt32ByIndex(2, aExpireType);
       if (NS_FAILED(rv)) break;
 
       rv = aStmt->BindInt64ByIndex(3, aExpireTime);
+      if (NS_FAILED(rv)) break;
+
+      rv = aStmt->BindInt64ByIndex(4, aModificationTime);
       break;
     }
 
   default:
     {
       NS_NOTREACHED("need a valid operation in UpdateDB()!");
       rv = NS_ERROR_UNEXPECTED;
       break;
--- a/extensions/cookie/nsPermissionManager.h
+++ b/extensions/cookie/nsPermissionManager.h
@@ -32,32 +32,35 @@ class nsPermissionManager MOZ_FINAL : pu
                                       public nsIObserver,
                                       public nsSupportsWeakReference
 {
 public:
   class PermissionEntry
   {
   public:
     PermissionEntry(int64_t aID, uint32_t aType, uint32_t aPermission,
-                    uint32_t aExpireType, int64_t aExpireTime)
+                    uint32_t aExpireType, int64_t aExpireTime,
+                    int64_t aModificationTime)
      : mID(aID)
      , mType(aType)
      , mPermission(aPermission)
      , mExpireType(aExpireType)
      , mExpireTime(aExpireTime)
+     , mModificationTime(aModificationTime)
      , mNonSessionPermission(aPermission)
      , mNonSessionExpireType(aExpireType)
      , mNonSessionExpireTime(aExpireTime)
     {}
 
     int64_t  mID;
     uint32_t mType;
     uint32_t mPermission;
     uint32_t mExpireType;
     int64_t  mExpireTime;
+    int64_t  mModificationTime;
     uint32_t mNonSessionPermission;
     uint32_t mNonSessionExpireType;
     uint32_t mNonSessionExpireTime;
   };
 
   /**
    * PermissionKey is the key used by PermissionHashKey hash table.
    *
@@ -149,17 +152,17 @@ public:
     inline PermissionEntry GetPermission(uint32_t aType) const
     {
       for (uint32_t i = 0; i < mPermissions.Length(); ++i)
         if (mPermissions[i].mType == aType)
           return mPermissions[i];
 
       // unknown permission... return relevant data 
       return PermissionEntry(-1, aType, nsIPermissionManager::UNKNOWN_ACTION,
-                             nsIPermissionManager::EXPIRE_NEVER, 0);
+                             nsIPermissionManager::EXPIRE_NEVER, 0, 0);
     }
 
   private:
     nsAutoTArray<PermissionEntry, 1> mPermissions;
   };
 
   // nsISupports
   NS_DECL_ISUPPORTS
@@ -195,16 +198,17 @@ public:
   static const int64_t cIDPermissionIsDefault = -1;
 
   nsresult AddInternal(nsIPrincipal* aPrincipal,
                        const nsAFlatCString &aType,
                        uint32_t aPermission,
                        int64_t aID,
                        uint32_t aExpireType,
                        int64_t  aExpireTime,
+                       int64_t aModificationTime,
                        NotifyOperationType aNotifyOperation,
                        DBOperationType aDBOperation);
 
   /**
    * Initialize the "webapp-uninstall" observing.
    * Will create a nsPermissionManager instance if needed.
    * That way, we can prevent have nsPermissionManager created at startup just
    * to be able to clear data when an application is uninstalled.
@@ -255,16 +259,17 @@ private:
   static void UpdateDB(OperationType aOp,
                        mozIStorageAsyncStatement* aStmt,
                        int64_t aID,
                        const nsACString& aHost,
                        const nsACString& aType,
                        uint32_t aPermission,
                        uint32_t aExpireType,
                        int64_t aExpireTime,
+                       int64_t aModificationTime,
                        uint32_t aAppId,
                        bool aIsInBrowserElement);
 
   nsresult RemoveExpiredPermissionsForApp(uint32_t aAppId);
 
   /**
    * This struct has to be passed as an argument to GetPermissionsForApp.
    * |appId| and |browserOnly| have to be defined.
@@ -294,16 +299,23 @@ private:
 
   /**
    * This method restores an app's permissions when its session ends.
    */
   static PLDHashOperator
   RemoveExpiredPermissionsForAppEnumerator(PermissionHashKey* entry,
                                            void* nonused);
 
+
+  /**
+   * This method removes all permissions modified after the specified time.
+   */
+  nsresult
+  RemoveAllModifiedSince(int64_t aModificationTime);
+
   nsCOMPtr<nsIObserverService> mObserverService;
   nsCOMPtr<nsIIDNService>      mIDNService;
 
   nsCOMPtr<mozIStorageConnection> mDBConn;
   nsCOMPtr<mozIStorageAsyncStatement> mStmtInsert;
   nsCOMPtr<mozIStorageAsyncStatement> mStmtDelete;
   nsCOMPtr<mozIStorageAsyncStatement> mStmtUpdate;
 
--- a/extensions/cookie/test/unit/test_permmanager_defaults.js
+++ b/extensions/cookie/test/unit/test_permmanager_defaults.js
@@ -1,15 +1,22 @@
 /* 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_ORIGIN_2 = "example.com";
 const TEST_PERMISSION = "test-permission";
 
+function promiseTimeout(delay) {
+  let deferred = Promise.defer();
+  do_timeout(delay, deferred.resolve);
+  return deferred.promise;
+}
+
 function run_test() {
   run_next_test();
 }
 
 add_task(function* do_test() {
   // setup a profile.
   do_get_profile();
 
@@ -23,16 +30,17 @@ add_task(function* do_test() {
   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");
+  conv.writeString("host\t" + TEST_PERMISSION + "\t1\t" + TEST_ORIGIN_2 + "\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);
@@ -87,16 +95,57 @@ add_task(function* do_test() {
   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);
 
+  // --------------------------------------------------------------
+  // check default permissions and removeAllSince work as expected.
+  pm.removeAll(); // ensure only defaults are there.
+
+  let permURI2 = NetUtil.newURI("http://" + TEST_ORIGIN_2);
+  let principal2 = Services.scriptSecurityManager.getNoAppCodebasePrincipal(permURI2);
+
+  // default for both principals is allow.
+  do_check_eq(Ci.nsIPermissionManager.ALLOW_ACTION,
+              pm.testPermissionFromPrincipal(principal, TEST_PERMISSION));
+  do_check_eq(Ci.nsIPermissionManager.ALLOW_ACTION,
+              pm.testPermissionFromPrincipal(principal2, TEST_PERMISSION));
+
+  // Add a default override for TEST_ORIGIN_2 - this one should *not* be
+  // restored in removeAllSince()
+  pm.addFromPrincipal(principal2, TEST_PERMISSION, Ci.nsIPermissionManager.DENY_ACTION);
+  do_check_eq(Ci.nsIPermissionManager.DENY_ACTION,
+              pm.testPermissionFromPrincipal(principal2, TEST_PERMISSION));
+  yield promiseTimeout(20);
+
+  let since = Number(Date.now());
+  yield promiseTimeout(20);
+
+  // explicitly add a permission which overrides the default for the first
+  // principal - this one *should* be removed by removeAllSince.
+  pm.addFromPrincipal(principal, TEST_PERMISSION, Ci.nsIPermissionManager.DENY_ACTION);
+  do_check_eq(Ci.nsIPermissionManager.DENY_ACTION,
+              pm.testPermissionFromPrincipal(principal, TEST_PERMISSION));
+
+  // do a removeAllSince.
+  pm.removeAllSince(since);
+
+  // the default for the first principal should re-appear as we modified it
+  // later then |since|
+  do_check_eq(Ci.nsIPermissionManager.ALLOW_ACTION,
+              pm.testPermissionFromPrincipal(principal, TEST_PERMISSION));
+
+  // but the permission for principal2 should remain as we added that before |since|.
+  do_check_eq(Ci.nsIPermissionManager.DENY_ACTION,
+              pm.testPermissionFromPrincipal(principal2, TEST_PERMISSION));
+
   // 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) {
--- a/extensions/cookie/test/unit/test_permmanager_load_invalid_entries.js
+++ b/extensions/cookie/test/unit/test_permmanager_load_invalid_entries.js
@@ -108,22 +108,34 @@ function run_test() {
       " (id, host, type, permission, expireType, expireTime, appId, isInBrowserElement) " +
       "VALUES (" + i + ", '" + data.host + "', '" + data.type + "', "
                  + data.permission + ", " + data.expireType + ", "
                  + data.expireTime + ", " + data.appId + ", "
                  + data.isInBrowserElement + ")"
     );
   }
 
+  let earliestNow = Number(Date.now());
   // Initialize the permission manager service
   var pm = Cc["@mozilla.org/permissionmanager;1"]
              .getService(Ci.nsIPermissionManager);
+  let latestNow = Number(Date.now());
 
-  // The schema should still be 3. We want this test to be updated for each
-  // schema update.
-  do_check_eq(connection.schemaVersion, 3);
+  // The schema should be upgraded to 4, and a 'modificationTime' column should
+  // exist with all records having a value of 0.
+  do_check_eq(connection.schemaVersion, 4);
+
+  let select = connection.createStatement("SELECT modificationTime FROM moz_hosts")
+  let numMigrated = 0;
+  while (select.executeStep()) {
+    let thisModTime = select.getInt64(0);
+    do_check_true(thisModTime == 0, "new modifiedTime field is correct");
+    numMigrated += 1;
+  }
+  // check we found at least 1 record that was migrated.
+  do_check_true(numMigrated > 0, "we found at least 1 record that was migrated");
 
   // This permission should always be there.
   let principal = Cc["@mozilla.org/scriptsecuritymanager;1"]
                     .getService(Ci.nsIScriptSecurityManager)
                     .getNoAppCodebasePrincipal(NetUtil.newURI("http://example.org"));
   do_check_eq(pm.testPermissionFromPrincipal(principal, 'test-load-invalid-entries'), Ci.nsIPermissionManager.ALLOW_ACTION);
 }
new file mode 100644
--- /dev/null
+++ b/extensions/cookie/test/unit/test_permmanager_removesince.js
@@ -0,0 +1,69 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that removing permissions since a specified time behaves as expected.
+
+let test_generator = do_run_test();
+
+function run_test() {
+  do_test_pending();
+  test_generator.next();
+}
+
+function continue_test()
+{
+  do_run_generator(test_generator);
+}
+
+function do_run_test() {
+  // Set up a profile.
+  let profile = do_get_profile();
+
+  let pm = Services.perms;
+
+  // to help with testing edge-cases, we will arrange for .removeAllSince to
+  // remove *all* permissions from one principal and one permission from another.
+  let permURI1 = NetUtil.newURI("http://example.com");
+  let principal1 = Services.scriptSecurityManager.getNoAppCodebasePrincipal(permURI1);
+
+  let permURI2 = NetUtil.newURI("http://example.org");
+  let principal2 = Services.scriptSecurityManager.getNoAppCodebasePrincipal(permURI2);
+
+  // add a permission now - this isn't going to be removed.
+  pm.addFromPrincipal(principal1, "test/remove-since", 1);
+
+  // sleep briefly, then record the time - we'll remove all since then.
+  do_timeout(20, continue_test);
+  yield;
+
+  let since = Number(Date.now());
+
+  // *sob* - on Windows at least, the now recorded by nsPermissionManager.cpp
+  // might be a couple of ms *earlier* than what JS sees.  So another sleep
+  // to ensure our |since| is greater than the time of the permissions we
+  // are now adding.  Sadly this means we'll never be able to test when since
+  // exactly equals the modTime, but there you go...
+  do_timeout(20, continue_test);
+  yield;
+
+  // add another item - this second one should get nuked.
+  pm.addFromPrincipal(principal1, "test/remove-since-2", 1);
+
+  // add 2 items for the second principal - both will be removed.
+  pm.addFromPrincipal(principal2, "test/remove-since", 1);
+  pm.addFromPrincipal(principal2, "test/remove-since-2", 1);
+
+  // do the removal.
+  pm.removeAllSince(since);
+
+  // principal1 - the first one should remain.
+  do_check_eq(1, pm.testPermissionFromPrincipal(principal1, "test/remove-since"));
+  // but the second should have been removed.
+  do_check_eq(0, pm.testPermissionFromPrincipal(principal1, "test/remove-since-2"));
+
+  // principal2 - both should have been removed.
+  do_check_eq(0, pm.testPermissionFromPrincipal(principal2, "test/remove-since"));
+  do_check_eq(0, pm.testPermissionFromPrincipal(principal2, "test/remove-since-2"));
+
+  do_finish_generator_test(test_generator);
+}
--- a/extensions/cookie/test/unit/xpcshell.ini
+++ b/extensions/cookie/test/unit/xpcshell.ini
@@ -18,16 +18,17 @@ 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]
 [test_permmanager_removeall.js]
+[test_permmanager_removesince.js]
 [test_permmanager_load_invalid_entries.js]
 skip-if = debug == true
 [test_permmanager_idn.js]
 [test_permmanager_subdomains.js]
 [test_permmanager_local_files.js]
 [test_permmanager_mailto.js]
 [test_permmanager_cleardata.js]
 [test_schema_2_migration.js]
--- a/netwerk/base/public/nsIPermissionManager.idl
+++ b/netwerk/base/public/nsIPermissionManager.idl
@@ -32,17 +32,17 @@
 
 interface nsIURI;
 interface nsIObserver;
 interface nsIPrincipal;
 interface nsIDOMWindow;
 interface nsIPermission;
 interface nsISimpleEnumerator;
 
-[scriptable, uuid(c9fec678-f194-43c9-96b0-7bd9dbdd6bb0)]
+[scriptable, uuid(620d9b61-8997-4d13-aa64-ec03341dd75b)]
 interface nsIPermissionManager : nsISupports
 {
   /**
    * Predefined return values for the testPermission method and for
    * the permission param of the add method
    * NOTE: UNKNOWN_ACTION (0) is reserved to represent the
    * default permission when no entry is found for a host, and
    * should not be used by consumers to indicate otherwise.
@@ -128,16 +128,21 @@ interface nsIPermissionManager : nsISupp
   void removeFromPrincipal(in nsIPrincipal principal, in string type);
 
   /**
    * Clear permission information for all websites.
    */
   void removeAll();
 
   /**
+   * Clear all permission information added since the specified time.
+   */
+  void removeAllSince(in int64_t since);
+
+  /**
    * Test whether a website has permission to perform the given action.
    * @param uri     the uri to be tested
    * @param type    a case-sensitive ASCII string, identifying the consumer
    * @param return  see add(), param permission. returns UNKNOWN_ACTION when
    *                there is no stored permission for this uri and / or type.
    */
   uint32_t testPermission(in nsIURI uri,
                           in string type);
--- a/testing/mozbase/mozprofile/mozprofile/permissions.py
+++ b/testing/mozbase/mozprofile/mozprofile/permissions.py
@@ -236,18 +236,22 @@ class Permissions(object):
            type TEXT,
            permission INTEGER,
            expireType INTEGER,
            expireTime INTEGER)""")
 
         rows = cursor.execute("PRAGMA table_info(moz_hosts)")
         count = len(rows.fetchall())
 
+        # if the db contains 9 columns, we're using user_version 4
+        if count == 9:
+            statement = "INSERT INTO moz_hosts values(NULL, ?, ?, ?, 0, 0, 0, 0, 0)"
+            cursor.execute("PRAGMA user_version=4;")
         # if the db contains 8 columns, we're using user_version 3
-        if count == 8:
+        elif count == 8:
             statement = "INSERT INTO moz_hosts values(NULL, ?, ?, ?, 0, 0, 0, 0)"
             cursor.execute("PRAGMA user_version=3;")
         else:
             statement = "INSERT INTO moz_hosts values(NULL, ?, ?, ?, 0, 0)"
             cursor.execute("PRAGMA user_version=2;")
 
         for location in locations:
             # set the permissions
--- a/testing/mozbase/mozprofile/tests/permissions.py
+++ b/testing/mozbase/mozprofile/tests/permissions.py
@@ -35,17 +35,28 @@ http://127.0.0.1:8888           privileg
             self.locations_file.close()
 
     def write_perm_db(self, version=3):
         permDB = sqlite3.connect(os.path.join(self.profile_dir, "permissions.sqlite"))
         cursor = permDB.cursor()
 
         cursor.execute("PRAGMA user_version=%d;" % version)
 
-        if version == 3:
+        if version == 4:
+            cursor.execute("""CREATE TABLE IF NOT EXISTS moz_hosts (
+               id INTEGER PRIMARY KEY,
+               host TEXT,
+               type TEXT,
+               permission INTEGER,
+               expireType INTEGER,
+               expireTime INTEGER,
+               modificationTime INTEGER,
+               appId INTEGER,
+               isInBrowserElement INTEGER)""")
+        elif version == 3:
             cursor.execute("""CREATE TABLE IF NOT EXISTS moz_hosts (
                id INTEGER PRIMARY KEY,
                host TEXT,
                type TEXT,
                permission INTEGER,
                expireType INTEGER,
                expireTime INTEGER,
                appId INTEGER,
@@ -54,17 +65,17 @@ http://127.0.0.1:8888           privileg
             cursor.execute("""CREATE TABLE IF NOT EXISTS moz_hosts (
                id INTEGER PRIMARY KEY,
                host TEXT,
                type TEXT,
                permission INTEGER,
                expireType INTEGER,
                expireTime INTEGER)""")
         else:
-            raise Exception("version must be 2 or 3")
+            raise Exception("version must be 2, 3 or 4")
 
         permDB.commit()
         cursor.close()
 
     def test_create_permissions_db(self):
         perms = Permissions(self.profile_dir, self.locations_file.name)
         perms_db_filename = os.path.join(self.profile_dir, 'permissions.sqlite')
 
@@ -144,22 +155,24 @@ http://127.0.0.1:8888           privileg
 
         con = sqlite3.connect(perms_db_filename)
         cur = con.cursor()
         cur.execute(select_stmt)
         entries = cur.fetchall()
 
         self.assertEqual(len(entries), 3)
 
-        columns = 8 if version == 3 else 6
+        columns = 9 if version == 4 else (8 if version == 3 else 6)
         self.assertEqual(len(entries[0]), columns)
         for x in range(4, columns):
             self.assertEqual(entries[0][x], 0)
 
     def test_existing_permissions_db_v2(self):
         self.verify_user_version(2)
 
     def test_existing_permissions_db_v3(self):
         self.verify_user_version(3)
 
+    def test_existing_permissions_db_v4(self):
+        self.verify_user_version(4)
 
 if __name__ == '__main__':
     unittest.main()