leash up the cookiemonster (cookie eviction should be in lastaccessed order, bug 403372). r=sdwilsh, sr=shaver. also includes unnecessary code removal fix by Marco Bonardo (mak77@supereva.it), bug 403767.

This commit is contained in:
dwitte@stanford.edu 2007-11-16 18:54:51 -08:00
parent 885859c9d1
commit 9fef439251
4 changed files with 165 additions and 83 deletions

View File

@ -89,6 +89,7 @@ nsCookie::Create(const nsACString &aName,
const nsACString &aHost,
const nsACString &aPath,
PRInt64 aExpiry,
PRInt64 aLastAccessed,
PRInt64 aCreationID,
PRBool aIsSession,
PRBool aIsSecure,
@ -119,7 +120,7 @@ nsCookie::Create(const nsACString &aName,
// construct the cookie. placement new, oh yeah!
return new (place) nsCookie(name, value, host, path, end,
aExpiry, aCreationID,
aExpiry, aLastAccessed, aCreationID,
aIsSession, aIsSecure, aIsHttpOnly);
}

View File

@ -70,6 +70,7 @@ class nsCookie : public nsICookie2
const char *aPath,
const char *aEnd,
PRInt64 aExpiry,
PRInt64 aLastAccessed,
PRInt64 aCreationID,
PRBool aIsSession,
PRBool aIsSecure,
@ -81,6 +82,7 @@ class nsCookie : public nsICookie2
, mPath(aPath)
, mEnd(aEnd)
, mExpiry(aExpiry)
, mLastAccessed(aLastAccessed)
, mCreationID(aCreationID)
, mIsSession(aIsSession != PR_FALSE)
, mIsSecure(aIsSecure != PR_FALSE)
@ -96,6 +98,7 @@ class nsCookie : public nsICookie2
const nsACString &aHost,
const nsACString &aPath,
PRInt64 aExpiry,
PRInt64 aLastAccessed,
PRInt64 aCreationID,
PRBool aIsSession,
PRBool aIsSecure,
@ -110,6 +113,7 @@ class nsCookie : public nsICookie2
inline const nsDependentCString RawHost() const { return nsDependentCString(IsDomain() ? mHost + 1 : mHost, mPath - 1); }
inline const nsDependentCString Path() const { return nsDependentCString(mPath, mEnd); }
inline PRInt64 Expiry() const { return mExpiry; }
inline PRInt64 LastAccessed() const { return mLastAccessed; }
inline PRInt64 CreationID() const { return mCreationID; }
// cookie creation time, in seconds
inline PRInt64 CreationTime() const { return mCreationID / PR_USEC_PER_SEC; }
@ -120,6 +124,7 @@ class nsCookie : public nsICookie2
// setters
inline void SetExpiry(PRInt64 aExpiry) { mExpiry = aExpiry; }
inline void SetLastAccessed(PRInt64 aTime) { mLastAccessed = aTime; }
inline void SetIsSession(PRBool aIsSession) { mIsSession = aIsSession; }
// set the creation id manually, overriding the monotonicity checks in Create().
// use with caution!
@ -142,6 +147,7 @@ class nsCookie : public nsICookie2
const char *mPath;
const char *mEnd;
PRInt64 mExpiry;
PRInt64 mLastAccessed;
// creation id is unique for each cookie and approximately represents the cookie
// creation time, in microseconds.
PRInt64 mCreationID;

View File

@ -1,5 +1,4 @@
// vim:ts=2:sw=2:et:
/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* ***** BEGIN LICENSE BLOCK *****
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
*
@ -83,7 +82,9 @@
static const char kHttpOnlyPrefix[] = "#HttpOnly_";
static const char kCookieFileName[] = "cookies.sqlite";
#define COOKIES_SCHEMA_VERSION 1
#define COOKIES_SCHEMA_VERSION 2
static const PRUint32 kCookieStaleThreshold = 60; // 1 minute in seconds
static const char kOldCookieFileName[] = "cookies.txt";
@ -126,7 +127,6 @@ struct nsCookieAttributes
nsCAutoString expires;
nsCAutoString maxage;
PRInt64 expiryTime;
PRInt64 creationID;
PRBool isSession;
PRBool isSecure;
PRBool isHttpOnly;
@ -162,17 +162,17 @@ struct nsListIter
struct nsEnumerationData
{
nsEnumerationData(PRInt64 aCurrentTime,
PRInt64 aOldestID)
PRInt64 aOldestTime)
: currentTime(aCurrentTime)
, oldestID(aOldestID)
, oldestTime(aOldestTime)
, iter(nsnull, nsnull, nsnull) {}
// the current time
PRInt64 currentTime;
// oldest creation id in the cookie list. use aOldestID = LL_MAXINT
// oldest lastAccessed time in the cookie list. use aOldestTime = LL_MAXINT
// to enable this search, LL_MININT to disable it.
PRInt64 oldestID;
PRInt64 oldestTime;
// an iterator object that points to the desired cookie
nsListIter iter;
@ -482,59 +482,65 @@ nsCookieService::InitDB()
PRInt32 dbSchemaVersion;
rv = mDBConn->GetSchemaVersion(&dbSchemaVersion);
NS_ENSURE_SUCCESS(rv, rv);
if (dbSchemaVersion == 0) {
NS_WARNING("couldn't get schema version!");
// the table may be usable; someone might've just clobbered the schema
// version. we can treat this case like a downgrade using the codepath
// below, by verifying the columns we care about are all there. for now,
// re-set the schema version in the db, in case the checks succeed (if
// they don't, we're dropping the table anyway).
rv = mDBConn->SetSchemaVersion(COOKIES_SCHEMA_VERSION);
NS_ENSURE_SUCCESS(rv, rv);
// set this to a large number, to force the downgrade codepath
dbSchemaVersion = PR_INT32_MAX;
}
switch (dbSchemaVersion) {
// upgrading.
// every time you increment the database schema, you need to implement
// the upgrading code from the previous version to the new one.
case 1:
{
// add the lastAccessed column to the table
rv = mDBConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
"ALTER TABLE moz_cookies ADD lastAccessed INTEGER"));
NS_ENSURE_SUCCESS(rv, rv);
if (dbSchemaVersion != COOKIES_SCHEMA_VERSION) {
// migration how-to:
//
// 1. increment COOKIES_SCHEMA_VERSION.
// 2. implement a method that performs up/sidegrade to your version
// from the current version.
// update the schema version
rv = mDBConn->SetSchemaVersion(COOKIES_SCHEMA_VERSION);
NS_ENSURE_SUCCESS(rv, rv);
}
// fall through to the next upgrade
if (dbSchemaVersion > COOKIES_SCHEMA_VERSION) {
// 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.
// NOTE: if you change the code below, make sure the db schema version
// getter above still falls through to this codepath on failure!
case COOKIES_SCHEMA_VERSION:
break;
case 0:
{
NS_WARNING("couldn't get schema version!");
// the table may be usable; someone might've just clobbered the schema
// version. we can treat this case like a downgrade using the codepath
// below, by verifying the columns we care about are all there. for now,
// re-set the schema version in the db, in case the checks succeed (if
// they don't, we're dropping the table anyway).
rv = mDBConn->SetSchemaVersion(COOKIES_SCHEMA_VERSION);
NS_ENSURE_SUCCESS(rv, rv);
}
// fall through to downgrade check
// 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 id, name, value, host, path, expiry, isSecure, isHttpOnly "
"FROM moz_cookies"), getter_AddRefs(stmt));
if (NS_SUCCEEDED(rv)) {
PRBool hasResult;
rv = stmt->ExecuteStep(&hasResult);
}
if (NS_FAILED(rv)) {
// our columns aren't there - drop the table!
rv = mDBConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING("DROP TABLE moz_cookies"));
NS_ENSURE_SUCCESS(rv, rv);
if (NS_SUCCEEDED(rv))
break;
rv = CreateTable();
NS_ENSURE_SUCCESS(rv, rv);
}
// our columns aren't there - drop the table!
rv = mDBConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING("DROP TABLE moz_cookies"));
NS_ENSURE_SUCCESS(rv, rv);
rv = CreateTable();
NS_ENSURE_SUCCESS(rv, rv);
}
break;
}
}
@ -544,14 +550,18 @@ nsCookieService::InitDB()
// cache frequently used statements (for insertion, deletion, and updating)
rv = mDBConn->CreateStatement(NS_LITERAL_CSTRING(
"INSERT INTO moz_cookies "
"(id, name, value, host, path, expiry, isSecure, isHttpOnly) "
"VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)"), getter_AddRefs(mStmtInsert));
"(id, name, value, host, path, expiry, lastAccessed, isSecure, isHttpOnly) "
"VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)"), getter_AddRefs(mStmtInsert));
NS_ENSURE_SUCCESS(rv, rv);
rv = mDBConn->CreateStatement(NS_LITERAL_CSTRING(
"DELETE FROM moz_cookies WHERE id = ?1"), getter_AddRefs(mStmtDelete));
NS_ENSURE_SUCCESS(rv, rv);
rv = mDBConn->CreateStatement(NS_LITERAL_CSTRING(
"UPDATE moz_cookies SET lastAccessed = ?1 WHERE id = ?2"), getter_AddRefs(mStmtUpdate));
NS_ENSURE_SUCCESS(rv, rv);
// check whether to import or just read in the db
if (tableExists)
return Read();
@ -580,7 +590,7 @@ nsCookieService::CreateTable()
return mDBConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
"CREATE TABLE moz_cookies ("
"id INTEGER PRIMARY KEY, name TEXT, value TEXT, host TEXT, path TEXT,"
"expiry INTEGER, isSecure INTEGER, isHttpOnly INTEGER)"));
"expiry INTEGER, lastAccessed INTEGER, isSecure INTEGER, isHttpOnly INTEGER)"));
}
nsCookieService::~nsCookieService()
@ -599,13 +609,14 @@ nsCookieService::Observe(nsISupports *aSubject,
// or is going away because the application is shutting down.
RemoveAllFromMemory();
if (!nsCRT::strcmp(aData, NS_LITERAL_STRING("shutdown-cleanse").get()) && mDBConn)
if (!nsCRT::strcmp(aData, NS_LITERAL_STRING("shutdown-cleanse").get()) && mDBConn) {
// clear the cookie file
if (mDBConn) {
nsresult rv = mDBConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING("DELETE FROM moz_cookies"));
if (NS_FAILED(rv))
NS_WARNING("db delete failed");
}
}
} else if (!strcmp(aTopic, "profile-do-change")) {
// the profile has already changed; init the db from the new location
@ -824,6 +835,7 @@ nsCookieService::Add(const nsACString &aDomain,
nsRefPtr<nsCookie> cookie =
nsCookie::Create(aName, aValue, aDomain, aPath,
aExpiry,
currentTimeInUsec / PR_USEC_PER_SEC,
currentTimeInUsec,
aIsSession,
aIsSecure,
@ -893,7 +905,7 @@ nsCookieService::Read()
// let the reading begin!
nsCOMPtr<mozIStorageStatement> stmt;
rv = mDBConn->CreateStatement(NS_LITERAL_CSTRING(
"SELECT id, name, value, host, path, expiry, isSecure, isHttpOnly "
"SELECT id, name, value, host, path, expiry, lastAccessed, isSecure, isHttpOnly "
"FROM moz_cookies"), getter_AddRefs(stmt));
NS_ENSURE_SUCCESS(rv, rv);
@ -908,13 +920,15 @@ nsCookieService::Read()
stmt->GetUTF8String(4, path);
PRInt64 expiry = stmt->AsInt64(5);
PRBool isSecure = stmt->AsInt32(6);
PRBool isHttpOnly = stmt->AsInt32(7);
PRInt64 lastAccessed = stmt->AsInt64(6);
PRBool isSecure = stmt->AsInt32(7);
PRBool isHttpOnly = stmt->AsInt32(8);
// create a new nsCookie and assign the data.
nsCookie* newCookie =
nsCookie::Create(name, value, host, path,
expiry,
lastAccessed,
creationID,
PR_FALSE,
isSecure,
@ -964,12 +978,11 @@ nsCookieService::ImportCookies(nsIFile *aCookieFile)
PRBool isDomain, isHttpOnly = PR_FALSE;
PRUint32 originalCookieCount = mCookieCount;
// generate a creation id for all the cookies we're going to read in, by
// using the current time and successively decrementing it, to keep
// the most-recently-used cookie ordering. the actual creation time is
// unknown, so this is the best we can do.
PRInt64 creationIDCounter = PR_Now();
PRInt64 currentTime = creationIDCounter / PR_USEC_PER_SEC;
PRInt64 currentTimeInUsec = PR_Now();
PRInt64 currentTime = currentTimeInUsec / PR_USEC_PER_SEC;
// we use lastAccessedCounter to keep cookies in recently-used order,
// so we start by initializing to currentTime (somewhat arbitrary)
PRInt64 lastAccessedCounter = currentTime;
/* file format is:
*
@ -979,7 +992,9 @@ nsCookieService::ImportCookies(nsIFile *aCookieFile)
* isDomain is "TRUE" or "FALSE" (default to "FALSE")
* isSecure is "TRUE" or "FALSE" (default to "TRUE")
* expires is a PRInt64 integer
* note: cookie can contain tabs.
* note 1: cookie can contain tabs.
* note 2: cookies will be stored in order of lastAccessed time:
* most-recently used come first; least-recently-used come last.
*/
/*
@ -1035,13 +1050,17 @@ nsCookieService::ImportCookies(nsIFile *aCookieFile)
}
// create a new nsCookie and assign the data.
// we don't know the cookie creation time, so just use the current time;
// this is okay, since nsCookie::Create() will make sure the creation id
// ends up monotonically increasing.
nsRefPtr<nsCookie> newCookie =
nsCookie::Create(Substring(buffer, nameIndex, cookieIndex - nameIndex - 1),
Substring(buffer, cookieIndex, buffer.Length() - cookieIndex),
host,
Substring(buffer, pathIndex, secureIndex - pathIndex - 1),
expires,
creationIDCounter,
lastAccessedCounter,
currentTimeInUsec,
PR_FALSE,
Substring(buffer, secureIndex, expiresIndex - secureIndex - 1).EqualsLiteral(kTrue),
isHttpOnly);
@ -1049,10 +1068,9 @@ nsCookieService::ImportCookies(nsIFile *aCookieFile)
return NS_ERROR_OUT_OF_MEMORY;
}
// manually set the creation id. this is okay, since nsCookie::Create() will keep
// track of the largest id we've used, and we're setting it to a smaller
// known unique one here.
newCookie->SetCreationID(--creationIDCounter);
// trick: preserve the most-recently-used cookie ordering,
// by successively decrementing the lastAccessed time
lastAccessedCounter--;
if (originalCookieCount == 0)
AddCookieToList(newCookie);
@ -1125,6 +1143,7 @@ nsCookieService::GetCookieInternal(nsIURI *aHostURI,
PRInt64 currentTime = PR_Now() / PR_USEC_PER_SEC;
const char *currentDot = hostFromURI.get();
const char *nextDot = currentDot + 1;
PRBool stale = PR_FALSE;
// begin hash lookup, walking up the subdomain levels.
// we use nextDot to force a lookup of the original host (without leading dot).
@ -1172,8 +1191,10 @@ nsCookieService::GetCookieInternal(nsIURI *aHostURI,
continue;
}
// all checks passed - add to list
// all checks passed - add to list and check if lastAccessed stamp needs updating
foundCookieList.AppendElement(cookie);
if (currentTime - cookie->LastAccessed() > kCookieStaleThreshold)
stale = PR_TRUE;
}
currentDot = nextDot;
@ -1182,13 +1203,31 @@ nsCookieService::GetCookieInternal(nsIURI *aHostURI,
} while (currentDot);
PRInt32 count = foundCookieList.Count();
if (count == 0)
return;
// update lastAccessed timestamps. we only do this if the timestamp is stale
// by a certain amount, to avoid thrashing the db during pageload.
if (stale) {
// start a transaction on the storage db, to optimize updates.
// transaction will automically commit on completion.
mozStorageTransaction transaction(mDBConn, PR_TRUE);
for (PRInt32 i = 0; i < count; ++i) {
cookie = static_cast<nsCookie*>(foundCookieList.ElementAt(i));
if (currentTime - cookie->LastAccessed() > kCookieStaleThreshold)
UpdateCookieInList(cookie, currentTime);
}
}
// return cookies in order of path length; longest to shortest.
// this is required per RFC2109. if cookies match in length,
// then sort by creation time (see bug 236772).
foundCookieList.Sort(compareCookiesForSending, nsnull);
nsCAutoString cookieData;
PRInt32 count = foundCookieList.Count();
for (PRInt32 i = 0; i < count; ++i) {
cookie = static_cast<nsCookie*>(foundCookieList.ElementAt(i));
@ -1242,9 +1281,7 @@ nsCookieService::SetCookieInternal(nsIURI *aHostURI,
// so we can handle them separately.
PRBool newCookie = ParseAttributes(aCookieHeader, cookieAttributes);
// generate a creation id for the cookie
PRInt64 currentTimeInUsec = PR_Now();
cookieAttributes.creationID = currentTimeInUsec;
// calculate expiry time of cookie.
cookieAttributes.isSession = GetExpiry(cookieAttributes, aServerTime,
@ -1278,7 +1315,8 @@ nsCookieService::SetCookieInternal(nsIURI *aHostURI,
cookieAttributes.host,
cookieAttributes.path,
cookieAttributes.expiryTime,
cookieAttributes.creationID,
currentTimeInUsec / PR_USEC_PER_SEC,
currentTimeInUsec,
cookieAttributes.isSession,
cookieAttributes.isSecure,
cookieAttributes.isHttpOnly);
@ -1360,6 +1398,10 @@ nsCookieService::AddInternal(nsCookie *aCookie,
return;
}
// preserve creation time of cookie
if (oldCookie)
aCookie->SetCreationID(oldCookie->CreationID());
} else {
// check if cookie has already expired
if (aCookie->Expiry() <= aCurrentTime) {
@ -1381,7 +1423,7 @@ nsCookieService::AddInternal(nsCookie *aCookie,
// check if we still have to get rid of something
if (mCookieCount >= mMaxNumberOfCookies) {
// find the position of the oldest cookie, and remove it
data.oldestID = LL_MAXINT;
data.oldestTime = LL_MAXINT;
FindOldestCookie(data);
oldCookie = data.iter.current;
RemoveCookieFromList(data.iter);
@ -2091,8 +2133,8 @@ nsCookieService::CountCookiesFromHostInternal(const nsACString &aHost,
++countFromHost;
// check if we've found the oldest cookie so far
if (aData.oldestID > iter.current->CreationID()) {
aData.oldestID = iter.current->CreationID();
if (aData.oldestTime > iter.current->LastAccessed()) {
aData.oldestTime = iter.current->LastAccessed();
aData.iter = iter;
}
}
@ -2205,10 +2247,13 @@ bindCookieParameters(mozIStorageStatement* aStmt, const nsCookie* aCookie)
rv = aStmt->BindInt64Parameter(5, aCookie->Expiry());
if (NS_FAILED(rv)) return rv;
rv = aStmt->BindInt32Parameter(6, aCookie->IsSecure());
rv = aStmt->BindInt64Parameter(6, aCookie->LastAccessed());
if (NS_FAILED(rv)) return rv;
rv = aStmt->BindInt32Parameter(7, aCookie->IsHttpOnly());
rv = aStmt->BindInt32Parameter(7, aCookie->IsSecure());
if (NS_FAILED(rv)) return rv;
rv = aStmt->BindInt32Parameter(8, aCookie->IsHttpOnly());
return rv;
}
@ -2248,6 +2293,33 @@ nsCookieService::AddCookieToList(nsCookie *aCookie, PRBool aWriteToDB)
return PR_TRUE;
}
void
nsCookieService::UpdateCookieInList(nsCookie *aCookie, PRInt64 aLastAccessed)
{
// update the lastAccessed timestamp
aCookie->SetLastAccessed(aLastAccessed);
// if it's a non-session cookie, update it in the db too
if (!aCookie->IsSession() && mStmtUpdate) {
// use our cached sqlite "update" statement
mozStorageStatementScoper scoper(mStmtUpdate);
nsresult rv = mStmtUpdate->BindInt64Parameter(0, aLastAccessed);
if (NS_SUCCEEDED(rv)) {
rv = mStmtUpdate->BindInt64Parameter(1, aCookie->CreationID());
if (NS_SUCCEEDED(rv)) {
PRBool hasResult;
rv = mStmtUpdate->ExecuteStep(&hasResult);
}
}
if (NS_FAILED(rv)) {
NS_WARNING("db update failed!");
COOKIE_LOGSTRING(PR_LOG_WARNING, ("UpdateCookieInList(): updating db gave error %x", rv));
}
}
}
PR_STATIC_CALLBACK(PLDHashOperator)
findOldestCallback(nsCookieEntry *aEntry,
void *aArg)
@ -2255,8 +2327,8 @@ findOldestCallback(nsCookieEntry *aEntry,
nsEnumerationData *data = static_cast<nsEnumerationData*>(aArg);
for (nsListIter iter(aEntry, nsnull, aEntry->Head()); iter.current; ++iter) {
// check if we've found the oldest cookie so far
if (data->oldestID > iter.current->CreationID()) {
data->oldestID = iter.current->CreationID();
if (data->oldestTime > iter.current->LastAccessed()) {
data->oldestTime = iter.current->LastAccessed();
data->iter = iter;
}
}
@ -2268,3 +2340,4 @@ nsCookieService::FindOldestCookie(nsEnumerationData &aData)
{
mHostTable.EnumerateEntries(findOldestCallback, &aData);
}

View File

@ -173,6 +173,7 @@ class nsCookieService : public nsICookieService
void AddInternal(nsCookie *aCookie, PRInt64 aCurrentTime, nsIURI *aHostURI, const char *aCookieHeader, PRBool aFromHttp);
void RemoveCookieFromList(nsListIter &aIter);
PRBool AddCookieToList(nsCookie *aCookie, PRBool aWriteToDB = PR_TRUE);
void UpdateCookieInList(nsCookie *aCookie, PRInt64 aLastAccessed);
static PRBool GetTokenValue(nsASingleFragmentCString::const_char_iterator &aIter, nsASingleFragmentCString::const_char_iterator &aEndIter, nsDependentCSubstring &aTokenString, nsDependentCSubstring &aTokenValue, PRBool &aEqualsFound);
static PRBool ParseAttributes(nsDependentCString &aCookieHeader, nsCookieAttributes &aCookie);
static PRBool IsIPAddress(const nsAFlatCString &aHost);
@ -195,6 +196,7 @@ class nsCookieService : public nsICookieService
nsCOMPtr<mozIStorageConnection> mDBConn;
nsCOMPtr<mozIStorageStatement> mStmtInsert;
nsCOMPtr<mozIStorageStatement> mStmtDelete;
nsCOMPtr<mozIStorageStatement> mStmtUpdate;
nsCOMPtr<nsIObserverService> mObserverService;
nsCOMPtr<nsICookiePermission> mPermissionService;