michael@0: /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ michael@0: /* vim: set sw=2 ts=8 et tw=80 : */ michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: #include "mozilla/Attributes.h" michael@0: #include "mozilla/DebugOnly.h" michael@0: #include "mozilla/Likely.h" michael@0: michael@0: #ifdef MOZ_LOGGING michael@0: // this next define has to appear before the include of prlog.h michael@0: #define FORCE_PR_LOG // Allow logging in the release build michael@0: #endif michael@0: michael@0: #include "mozilla/net/CookieServiceChild.h" michael@0: #include "mozilla/net/NeckoCommon.h" michael@0: michael@0: #include "nsCookieService.h" michael@0: #include "nsIServiceManager.h" michael@0: michael@0: #include "nsIIOService.h" michael@0: #include "nsIPrefBranch.h" michael@0: #include "nsIPrefService.h" michael@0: #include "nsICookiePermission.h" michael@0: #include "nsIURI.h" michael@0: #include "nsIURL.h" michael@0: #include "nsIChannel.h" michael@0: #include "nsIFile.h" michael@0: #include "nsIObserverService.h" michael@0: #include "nsILineInputStream.h" michael@0: #include "nsIEffectiveTLDService.h" michael@0: #include "nsIIDNService.h" michael@0: #include "mozIThirdPartyUtil.h" michael@0: michael@0: #include "nsTArray.h" michael@0: #include "nsCOMArray.h" michael@0: #include "nsIMutableArray.h" michael@0: #include "nsArrayEnumerator.h" michael@0: #include "nsEnumeratorUtils.h" michael@0: #include "nsAutoPtr.h" michael@0: #include "nsReadableUtils.h" michael@0: #include "nsCRT.h" michael@0: #include "prprf.h" michael@0: #include "nsNetUtil.h" michael@0: #include "nsNetCID.h" michael@0: #include "nsAppDirectoryServiceDefs.h" michael@0: #include "nsNetCID.h" michael@0: #include "mozilla/storage.h" michael@0: #include "mozilla/AutoRestore.h" michael@0: #include "mozilla/FileUtils.h" michael@0: #include "mozilla/Telemetry.h" michael@0: #include "nsIAppsService.h" michael@0: #include "mozIApplication.h" michael@0: #include "nsIConsoleService.h" michael@0: michael@0: using namespace mozilla; michael@0: using namespace mozilla::net; michael@0: michael@0: // Create key from baseDomain that will access the default cookie namespace. michael@0: // TODO: When we figure out what the API will look like for nsICookieManager{2} michael@0: // on content processes (see bug 777620), change to use the appropriate app michael@0: // namespace. For now those IDLs aren't supported on child processes. michael@0: #define DEFAULT_APP_KEY(baseDomain) \ michael@0: nsCookieKey(baseDomain, NECKO_NO_APP_ID, false) michael@0: michael@0: /****************************************************************************** michael@0: * nsCookieService impl: michael@0: * useful types & constants michael@0: ******************************************************************************/ michael@0: michael@0: static nsCookieService *gCookieService; michael@0: michael@0: // XXX_hack. See bug 178993. michael@0: // This is a hack to hide HttpOnly cookies from older browsers michael@0: static const char kHttpOnlyPrefix[] = "#HttpOnly_"; michael@0: michael@0: #define COOKIES_FILE "cookies.sqlite" michael@0: #define COOKIES_SCHEMA_VERSION 5 michael@0: michael@0: // parameter indexes; see EnsureReadDomain, EnsureReadComplete and michael@0: // ReadCookieDBListener::HandleResult michael@0: #define IDX_NAME 0 michael@0: #define IDX_VALUE 1 michael@0: #define IDX_HOST 2 michael@0: #define IDX_PATH 3 michael@0: #define IDX_EXPIRY 4 michael@0: #define IDX_LAST_ACCESSED 5 michael@0: #define IDX_CREATION_TIME 6 michael@0: #define IDX_SECURE 7 michael@0: #define IDX_HTTPONLY 8 michael@0: #define IDX_BASE_DOMAIN 9 michael@0: #define IDX_APP_ID 10 michael@0: #define IDX_BROWSER_ELEM 11 michael@0: michael@0: static const int64_t kCookieStaleThreshold = 60 * PR_USEC_PER_SEC; // 1 minute in microseconds michael@0: static const int64_t kCookiePurgeAge = michael@0: int64_t(30 * 24 * 60 * 60) * PR_USEC_PER_SEC; // 30 days in microseconds michael@0: michael@0: static const char kOldCookieFileName[] = "cookies.txt"; michael@0: michael@0: #undef LIMIT michael@0: #define LIMIT(x, low, high, default) ((x) >= (low) && (x) <= (high) ? (x) : (default)) michael@0: michael@0: #undef ADD_TEN_PERCENT michael@0: #define ADD_TEN_PERCENT(i) static_cast((i) + (i)/10) michael@0: michael@0: // default limits for the cookie list. these can be tuned by the michael@0: // network.cookie.maxNumber and network.cookie.maxPerHost prefs respectively. michael@0: static const uint32_t kMaxNumberOfCookies = 3000; michael@0: static const uint32_t kMaxCookiesPerHost = 150; michael@0: static const uint32_t kMaxBytesPerCookie = 4096; michael@0: static const uint32_t kMaxBytesPerPath = 1024; michael@0: michael@0: // behavior pref constants michael@0: static const uint32_t BEHAVIOR_ACCEPT = 0; // allow all cookies michael@0: static const uint32_t BEHAVIOR_REJECTFOREIGN = 1; // reject all third-party cookies michael@0: static const uint32_t BEHAVIOR_REJECT = 2; // reject all cookies michael@0: static const uint32_t BEHAVIOR_LIMITFOREIGN = 3; // reject third-party cookies unless the michael@0: // eTLD already has at least one cookie michael@0: michael@0: // pref string constants michael@0: static const char kPrefCookieBehavior[] = "network.cookie.cookieBehavior"; michael@0: static const char kPrefMaxNumberOfCookies[] = "network.cookie.maxNumber"; michael@0: static const char kPrefMaxCookiesPerHost[] = "network.cookie.maxPerHost"; michael@0: static const char kPrefCookiePurgeAge[] = "network.cookie.purgeAge"; michael@0: static const char kPrefThirdPartySession[] = "network.cookie.thirdparty.sessionOnly"; michael@0: michael@0: static void michael@0: bindCookieParameters(mozIStorageBindingParamsArray *aParamsArray, michael@0: const nsCookieKey &aKey, michael@0: const nsCookie *aCookie); michael@0: michael@0: // struct for temporarily storing cookie attributes during header parsing michael@0: struct nsCookieAttributes michael@0: { michael@0: nsAutoCString name; michael@0: nsAutoCString value; michael@0: nsAutoCString host; michael@0: nsAutoCString path; michael@0: nsAutoCString expires; michael@0: nsAutoCString maxage; michael@0: int64_t expiryTime; michael@0: bool isSession; michael@0: bool isSecure; michael@0: bool isHttpOnly; michael@0: }; michael@0: michael@0: // stores the nsCookieEntry entryclass and an index into the cookie array michael@0: // within that entryclass, for purposes of storing an iteration state that michael@0: // points to a certain cookie. michael@0: struct nsListIter michael@0: { michael@0: // default (non-initializing) constructor. michael@0: nsListIter() michael@0: { michael@0: } michael@0: michael@0: // explicit constructor to a given iterator state with entryclass 'aEntry' michael@0: // and index 'aIndex'. michael@0: explicit michael@0: nsListIter(nsCookieEntry *aEntry, nsCookieEntry::IndexType aIndex) michael@0: : entry(aEntry) michael@0: , index(aIndex) michael@0: { michael@0: } michael@0: michael@0: // get the nsCookie * the iterator currently points to. michael@0: nsCookie * Cookie() const michael@0: { michael@0: return entry->GetCookies()[index]; michael@0: } michael@0: michael@0: nsCookieEntry *entry; michael@0: nsCookieEntry::IndexType index; michael@0: }; michael@0: michael@0: /****************************************************************************** michael@0: * Cookie logging handlers michael@0: * used for logging in nsCookieService michael@0: ******************************************************************************/ michael@0: michael@0: // logging handlers michael@0: #ifdef MOZ_LOGGING michael@0: // in order to do logging, the following environment variables need to be set: michael@0: // michael@0: // set NSPR_LOG_MODULES=cookie:3 -- shows rejected cookies michael@0: // set NSPR_LOG_MODULES=cookie:4 -- shows accepted and rejected cookies michael@0: // set NSPR_LOG_FILE=cookie.log michael@0: // michael@0: #include "prlog.h" michael@0: #endif michael@0: michael@0: // define logging macros for convenience michael@0: #define SET_COOKIE true michael@0: #define GET_COOKIE false michael@0: michael@0: #ifdef PR_LOGGING michael@0: static PRLogModuleInfo * michael@0: GetCookieLog() michael@0: { michael@0: static PRLogModuleInfo *sCookieLog; michael@0: if (!sCookieLog) michael@0: sCookieLog = PR_NewLogModule("cookie"); michael@0: return sCookieLog; michael@0: } michael@0: michael@0: #define COOKIE_LOGFAILURE(a, b, c, d) LogFailure(a, b, c, d) michael@0: #define COOKIE_LOGSUCCESS(a, b, c, d, e) LogSuccess(a, b, c, d, e) michael@0: michael@0: #define COOKIE_LOGEVICTED(a, details) \ michael@0: PR_BEGIN_MACRO \ michael@0: if (PR_LOG_TEST(GetCookieLog(), PR_LOG_DEBUG)) \ michael@0: LogEvicted(a, details); \ michael@0: PR_END_MACRO michael@0: michael@0: #define COOKIE_LOGSTRING(lvl, fmt) \ michael@0: PR_BEGIN_MACRO \ michael@0: PR_LOG(GetCookieLog(), lvl, fmt); \ michael@0: PR_LOG(GetCookieLog(), lvl, ("\n")); \ michael@0: PR_END_MACRO michael@0: michael@0: static void michael@0: LogFailure(bool aSetCookie, nsIURI *aHostURI, const char *aCookieString, const char *aReason) michael@0: { michael@0: // if logging isn't enabled, return now to save cycles michael@0: if (!PR_LOG_TEST(GetCookieLog(), PR_LOG_WARNING)) michael@0: return; michael@0: michael@0: nsAutoCString spec; michael@0: if (aHostURI) michael@0: aHostURI->GetAsciiSpec(spec); michael@0: michael@0: PR_LOG(GetCookieLog(), PR_LOG_WARNING, michael@0: ("===== %s =====\n", aSetCookie ? "COOKIE NOT ACCEPTED" : "COOKIE NOT SENT")); michael@0: PR_LOG(GetCookieLog(), PR_LOG_WARNING,("request URL: %s\n", spec.get())); michael@0: if (aSetCookie) michael@0: PR_LOG(GetCookieLog(), PR_LOG_WARNING,("cookie string: %s\n", aCookieString)); michael@0: michael@0: PRExplodedTime explodedTime; michael@0: PR_ExplodeTime(PR_Now(), PR_GMTParameters, &explodedTime); michael@0: char timeString[40]; michael@0: PR_FormatTimeUSEnglish(timeString, 40, "%c GMT", &explodedTime); michael@0: michael@0: PR_LOG(GetCookieLog(), PR_LOG_WARNING,("current time: %s", timeString)); michael@0: PR_LOG(GetCookieLog(), PR_LOG_WARNING,("rejected because %s\n", aReason)); michael@0: PR_LOG(GetCookieLog(), PR_LOG_WARNING,("\n")); michael@0: } michael@0: michael@0: static void michael@0: LogCookie(nsCookie *aCookie) michael@0: { michael@0: PRExplodedTime explodedTime; michael@0: PR_ExplodeTime(PR_Now(), PR_GMTParameters, &explodedTime); michael@0: char timeString[40]; michael@0: PR_FormatTimeUSEnglish(timeString, 40, "%c GMT", &explodedTime); michael@0: michael@0: PR_LOG(GetCookieLog(), PR_LOG_DEBUG,("current time: %s", timeString)); michael@0: michael@0: if (aCookie) { michael@0: PR_LOG(GetCookieLog(), PR_LOG_DEBUG,("----------------\n")); michael@0: PR_LOG(GetCookieLog(), PR_LOG_DEBUG,("name: %s\n", aCookie->Name().get())); michael@0: PR_LOG(GetCookieLog(), PR_LOG_DEBUG,("value: %s\n", aCookie->Value().get())); michael@0: PR_LOG(GetCookieLog(), PR_LOG_DEBUG,("%s: %s\n", aCookie->IsDomain() ? "domain" : "host", aCookie->Host().get())); michael@0: PR_LOG(GetCookieLog(), PR_LOG_DEBUG,("path: %s\n", aCookie->Path().get())); michael@0: michael@0: PR_ExplodeTime(aCookie->Expiry() * int64_t(PR_USEC_PER_SEC), michael@0: PR_GMTParameters, &explodedTime); michael@0: PR_FormatTimeUSEnglish(timeString, 40, "%c GMT", &explodedTime); michael@0: PR_LOG(GetCookieLog(), PR_LOG_DEBUG, michael@0: ("expires: %s%s", timeString, aCookie->IsSession() ? " (at end of session)" : "")); michael@0: michael@0: PR_ExplodeTime(aCookie->CreationTime(), PR_GMTParameters, &explodedTime); michael@0: PR_FormatTimeUSEnglish(timeString, 40, "%c GMT", &explodedTime); michael@0: PR_LOG(GetCookieLog(), PR_LOG_DEBUG,("created: %s", timeString)); michael@0: michael@0: PR_LOG(GetCookieLog(), PR_LOG_DEBUG,("is secure: %s\n", aCookie->IsSecure() ? "true" : "false")); michael@0: PR_LOG(GetCookieLog(), PR_LOG_DEBUG,("is httpOnly: %s\n", aCookie->IsHttpOnly() ? "true" : "false")); michael@0: } michael@0: } michael@0: michael@0: static void michael@0: LogSuccess(bool aSetCookie, nsIURI *aHostURI, const char *aCookieString, nsCookie *aCookie, bool aReplacing) michael@0: { michael@0: // if logging isn't enabled, return now to save cycles michael@0: if (!PR_LOG_TEST(GetCookieLog(), PR_LOG_DEBUG)) { michael@0: return; michael@0: } michael@0: michael@0: nsAutoCString spec; michael@0: if (aHostURI) michael@0: aHostURI->GetAsciiSpec(spec); michael@0: michael@0: PR_LOG(GetCookieLog(), PR_LOG_DEBUG, michael@0: ("===== %s =====\n", aSetCookie ? "COOKIE ACCEPTED" : "COOKIE SENT")); michael@0: PR_LOG(GetCookieLog(), PR_LOG_DEBUG,("request URL: %s\n", spec.get())); michael@0: PR_LOG(GetCookieLog(), PR_LOG_DEBUG,("cookie string: %s\n", aCookieString)); michael@0: if (aSetCookie) michael@0: PR_LOG(GetCookieLog(), PR_LOG_DEBUG,("replaces existing cookie: %s\n", aReplacing ? "true" : "false")); michael@0: michael@0: LogCookie(aCookie); michael@0: michael@0: PR_LOG(GetCookieLog(), PR_LOG_DEBUG,("\n")); michael@0: } michael@0: michael@0: static void michael@0: LogEvicted(nsCookie *aCookie, const char* details) michael@0: { michael@0: PR_LOG(GetCookieLog(), PR_LOG_DEBUG,("===== COOKIE EVICTED =====\n")); michael@0: PR_LOG(GetCookieLog(), PR_LOG_DEBUG,("%s\n", details)); michael@0: michael@0: LogCookie(aCookie); michael@0: michael@0: PR_LOG(GetCookieLog(), PR_LOG_DEBUG,("\n")); michael@0: } michael@0: michael@0: // inline wrappers to make passing in nsAFlatCStrings easier michael@0: static inline void michael@0: LogFailure(bool aSetCookie, nsIURI *aHostURI, const nsAFlatCString &aCookieString, const char *aReason) michael@0: { michael@0: LogFailure(aSetCookie, aHostURI, aCookieString.get(), aReason); michael@0: } michael@0: michael@0: static inline void michael@0: LogSuccess(bool aSetCookie, nsIURI *aHostURI, const nsAFlatCString &aCookieString, nsCookie *aCookie, bool aReplacing) michael@0: { michael@0: LogSuccess(aSetCookie, aHostURI, aCookieString.get(), aCookie, aReplacing); michael@0: } michael@0: michael@0: #else michael@0: #define COOKIE_LOGFAILURE(a, b, c, d) PR_BEGIN_MACRO /* nothing */ PR_END_MACRO michael@0: #define COOKIE_LOGSUCCESS(a, b, c, d, e) PR_BEGIN_MACRO /* nothing */ PR_END_MACRO michael@0: #define COOKIE_LOGEVICTED(a, b) PR_BEGIN_MACRO /* nothing */ PR_END_MACRO michael@0: #define COOKIE_LOGSTRING(a, b) PR_BEGIN_MACRO /* nothing */ PR_END_MACRO michael@0: #endif michael@0: michael@0: #ifdef DEBUG michael@0: #define NS_ASSERT_SUCCESS(res) \ michael@0: PR_BEGIN_MACRO \ michael@0: nsresult __rv = res; /* Do not evaluate |res| more than once! */ \ michael@0: if (NS_FAILED(__rv)) { \ michael@0: char *msg = PR_smprintf("NS_ASSERT_SUCCESS(%s) failed with result 0x%X", \ michael@0: #res, __rv); \ michael@0: NS_ASSERTION(NS_SUCCEEDED(__rv), msg); \ michael@0: PR_smprintf_free(msg); \ michael@0: } \ michael@0: PR_END_MACRO michael@0: #else michael@0: #define NS_ASSERT_SUCCESS(res) PR_BEGIN_MACRO /* nothing */ PR_END_MACRO michael@0: #endif michael@0: michael@0: /****************************************************************************** michael@0: * DBListenerErrorHandler impl: michael@0: * Parent class for our async storage listeners that handles the logging of michael@0: * errors. michael@0: ******************************************************************************/ michael@0: class DBListenerErrorHandler : public mozIStorageStatementCallback michael@0: { michael@0: protected: michael@0: DBListenerErrorHandler(DBState* dbState) : mDBState(dbState) { } michael@0: nsRefPtr mDBState; michael@0: virtual const char *GetOpType() = 0; michael@0: michael@0: public: michael@0: NS_IMETHOD HandleError(mozIStorageError* aError) michael@0: { michael@0: int32_t result = -1; michael@0: aError->GetResult(&result); michael@0: michael@0: #ifdef PR_LOGGING michael@0: nsAutoCString message; michael@0: aError->GetMessage(message); michael@0: COOKIE_LOGSTRING(PR_LOG_WARNING, michael@0: ("DBListenerErrorHandler::HandleError(): Error %d occurred while " michael@0: "performing operation '%s' with message '%s'; rebuilding database.", michael@0: result, GetOpType(), message.get())); michael@0: #endif michael@0: michael@0: // Rebuild the database. michael@0: gCookieService->HandleCorruptDB(mDBState); michael@0: michael@0: return NS_OK; michael@0: } michael@0: }; michael@0: michael@0: /****************************************************************************** michael@0: * InsertCookieDBListener impl: michael@0: * mozIStorageStatementCallback used to track asynchronous insertion operations. michael@0: ******************************************************************************/ michael@0: class InsertCookieDBListener MOZ_FINAL : public DBListenerErrorHandler michael@0: { michael@0: protected: michael@0: virtual const char *GetOpType() { return "INSERT"; } michael@0: michael@0: public: michael@0: NS_DECL_ISUPPORTS michael@0: michael@0: InsertCookieDBListener(DBState* dbState) : DBListenerErrorHandler(dbState) { } michael@0: NS_IMETHOD HandleResult(mozIStorageResultSet*) michael@0: { michael@0: NS_NOTREACHED("Unexpected call to InsertCookieDBListener::HandleResult"); michael@0: return NS_OK; michael@0: } michael@0: NS_IMETHOD HandleCompletion(uint16_t aReason) michael@0: { michael@0: // If we were rebuilding the db and we succeeded, make our corruptFlag say michael@0: // so. michael@0: if (mDBState->corruptFlag == DBState::REBUILDING && michael@0: aReason == mozIStorageStatementCallback::REASON_FINISHED) { michael@0: COOKIE_LOGSTRING(PR_LOG_DEBUG, michael@0: ("InsertCookieDBListener::HandleCompletion(): rebuild complete")); michael@0: mDBState->corruptFlag = DBState::OK; michael@0: } michael@0: return NS_OK; michael@0: } michael@0: }; michael@0: michael@0: NS_IMPL_ISUPPORTS(InsertCookieDBListener, mozIStorageStatementCallback) michael@0: michael@0: /****************************************************************************** michael@0: * UpdateCookieDBListener impl: michael@0: * mozIStorageStatementCallback used to track asynchronous update operations. michael@0: ******************************************************************************/ michael@0: class UpdateCookieDBListener MOZ_FINAL : public DBListenerErrorHandler michael@0: { michael@0: protected: michael@0: virtual const char *GetOpType() { return "UPDATE"; } michael@0: michael@0: public: michael@0: NS_DECL_ISUPPORTS michael@0: michael@0: UpdateCookieDBListener(DBState* dbState) : DBListenerErrorHandler(dbState) { } michael@0: NS_IMETHOD HandleResult(mozIStorageResultSet*) michael@0: { michael@0: NS_NOTREACHED("Unexpected call to UpdateCookieDBListener::HandleResult"); michael@0: return NS_OK; michael@0: } michael@0: NS_IMETHOD HandleCompletion(uint16_t aReason) michael@0: { michael@0: return NS_OK; michael@0: } michael@0: }; michael@0: michael@0: NS_IMPL_ISUPPORTS(UpdateCookieDBListener, mozIStorageStatementCallback) michael@0: michael@0: /****************************************************************************** michael@0: * RemoveCookieDBListener impl: michael@0: * mozIStorageStatementCallback used to track asynchronous removal operations. michael@0: ******************************************************************************/ michael@0: class RemoveCookieDBListener MOZ_FINAL : public DBListenerErrorHandler michael@0: { michael@0: protected: michael@0: virtual const char *GetOpType() { return "REMOVE"; } michael@0: michael@0: public: michael@0: NS_DECL_ISUPPORTS michael@0: michael@0: RemoveCookieDBListener(DBState* dbState) : DBListenerErrorHandler(dbState) { } michael@0: NS_IMETHOD HandleResult(mozIStorageResultSet*) michael@0: { michael@0: NS_NOTREACHED("Unexpected call to RemoveCookieDBListener::HandleResult"); michael@0: return NS_OK; michael@0: } michael@0: NS_IMETHOD HandleCompletion(uint16_t aReason) michael@0: { michael@0: return NS_OK; michael@0: } michael@0: }; michael@0: michael@0: NS_IMPL_ISUPPORTS(RemoveCookieDBListener, mozIStorageStatementCallback) michael@0: michael@0: /****************************************************************************** michael@0: * ReadCookieDBListener impl: michael@0: * mozIStorageStatementCallback used to track asynchronous removal operations. michael@0: ******************************************************************************/ michael@0: class ReadCookieDBListener MOZ_FINAL : public DBListenerErrorHandler michael@0: { michael@0: protected: michael@0: virtual const char *GetOpType() { return "READ"; } michael@0: bool mCanceled; michael@0: michael@0: public: michael@0: NS_DECL_ISUPPORTS michael@0: michael@0: ReadCookieDBListener(DBState* dbState) michael@0: : DBListenerErrorHandler(dbState) michael@0: , mCanceled(false) michael@0: { michael@0: } michael@0: michael@0: void Cancel() { mCanceled = true; } michael@0: michael@0: NS_IMETHOD HandleResult(mozIStorageResultSet *aResult) michael@0: { michael@0: nsCOMPtr row; michael@0: michael@0: while (1) { michael@0: DebugOnly rv = aResult->GetNextRow(getter_AddRefs(row)); michael@0: NS_ASSERT_SUCCESS(rv); michael@0: michael@0: if (!row) michael@0: break; michael@0: michael@0: CookieDomainTuple *tuple = mDBState->hostArray.AppendElement(); michael@0: row->GetUTF8String(IDX_BASE_DOMAIN, tuple->key.mBaseDomain); michael@0: tuple->key.mAppId = static_cast(row->AsInt32(IDX_APP_ID)); michael@0: tuple->key.mInBrowserElement = static_cast(row->AsInt32(IDX_BROWSER_ELEM)); michael@0: tuple->cookie = gCookieService->GetCookieFromRow(row); michael@0: } michael@0: michael@0: return NS_OK; michael@0: } michael@0: NS_IMETHOD HandleCompletion(uint16_t aReason) michael@0: { michael@0: // Process the completion of the read operation. If we have been canceled, michael@0: // we cannot assume that the cookieservice still has an open connection michael@0: // or that it even refers to the same database, so we must return early. michael@0: // Conversely, the cookieservice guarantees that if we have not been michael@0: // canceled, the database connection is still alive and we can safely michael@0: // operate on it. michael@0: michael@0: if (mCanceled) { michael@0: // We may receive a REASON_FINISHED after being canceled; michael@0: // tweak the reason accordingly. michael@0: aReason = mozIStorageStatementCallback::REASON_CANCELED; michael@0: } michael@0: michael@0: switch (aReason) { michael@0: case mozIStorageStatementCallback::REASON_FINISHED: michael@0: gCookieService->AsyncReadComplete(); michael@0: break; michael@0: case mozIStorageStatementCallback::REASON_CANCELED: michael@0: // Nothing more to do here. The partially read data has already been michael@0: // thrown away. michael@0: COOKIE_LOGSTRING(PR_LOG_DEBUG, ("Read canceled")); michael@0: break; michael@0: case mozIStorageStatementCallback::REASON_ERROR: michael@0: // Nothing more to do here. DBListenerErrorHandler::HandleError() michael@0: // can handle it. michael@0: COOKIE_LOGSTRING(PR_LOG_DEBUG, ("Read error")); michael@0: break; michael@0: default: michael@0: NS_NOTREACHED("invalid reason"); michael@0: } michael@0: return NS_OK; michael@0: } michael@0: }; michael@0: michael@0: NS_IMPL_ISUPPORTS(ReadCookieDBListener, mozIStorageStatementCallback) michael@0: michael@0: /****************************************************************************** michael@0: * CloseCookieDBListener imp: michael@0: * Static mozIStorageCompletionCallback used to notify when the database is michael@0: * successfully closed. michael@0: ******************************************************************************/ michael@0: class CloseCookieDBListener MOZ_FINAL : public mozIStorageCompletionCallback michael@0: { michael@0: public: michael@0: CloseCookieDBListener(DBState* dbState) : mDBState(dbState) { } michael@0: nsRefPtr mDBState; michael@0: NS_DECL_ISUPPORTS michael@0: michael@0: NS_IMETHOD Complete(nsresult, nsISupports*) michael@0: { michael@0: gCookieService->HandleDBClosed(mDBState); michael@0: return NS_OK; michael@0: } michael@0: }; michael@0: michael@0: NS_IMPL_ISUPPORTS(CloseCookieDBListener, mozIStorageCompletionCallback) michael@0: michael@0: namespace { michael@0: michael@0: class AppClearDataObserver MOZ_FINAL : public nsIObserver { michael@0: public: michael@0: NS_DECL_ISUPPORTS michael@0: michael@0: // nsIObserver implementation. michael@0: NS_IMETHODIMP michael@0: Observe(nsISupports *aSubject, const char *aTopic, const char16_t *data) michael@0: { michael@0: MOZ_ASSERT(!nsCRT::strcmp(aTopic, TOPIC_WEB_APP_CLEAR_DATA)); michael@0: michael@0: uint32_t appId = NECKO_UNKNOWN_APP_ID; michael@0: bool browserOnly = false; michael@0: nsresult rv = NS_GetAppInfoFromClearDataNotification(aSubject, &appId, michael@0: &browserOnly); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: michael@0: nsCOMPtr cookieManager michael@0: = do_GetService(NS_COOKIEMANAGER_CONTRACTID); michael@0: MOZ_ASSERT(cookieManager); michael@0: return cookieManager->RemoveCookiesForApp(appId, browserOnly); michael@0: } michael@0: }; michael@0: michael@0: NS_IMPL_ISUPPORTS(AppClearDataObserver, nsIObserver) michael@0: michael@0: } // anonymous namespace michael@0: michael@0: size_t michael@0: nsCookieKey::SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const michael@0: { michael@0: return mBaseDomain.SizeOfExcludingThisIfUnshared(aMallocSizeOf); michael@0: } michael@0: michael@0: size_t michael@0: nsCookieEntry::SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const michael@0: { michael@0: size_t amount = nsCookieKey::SizeOfExcludingThis(aMallocSizeOf); michael@0: michael@0: amount += mCookies.SizeOfExcludingThis(aMallocSizeOf); michael@0: for (uint32_t i = 0; i < mCookies.Length(); ++i) { michael@0: amount += mCookies[i]->SizeOfIncludingThis(aMallocSizeOf); michael@0: } michael@0: michael@0: return amount; michael@0: } michael@0: michael@0: static size_t michael@0: HostTableEntrySizeOfExcludingThis(nsCookieEntry *aEntry, michael@0: MallocSizeOf aMallocSizeOf, michael@0: void *arg) michael@0: { michael@0: return aEntry->SizeOfExcludingThis(aMallocSizeOf); michael@0: } michael@0: michael@0: size_t michael@0: CookieDomainTuple::SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const michael@0: { michael@0: size_t amount = 0; michael@0: michael@0: amount += key.SizeOfExcludingThis(aMallocSizeOf); michael@0: amount += cookie->SizeOfIncludingThis(aMallocSizeOf); michael@0: michael@0: return amount; michael@0: } michael@0: michael@0: static size_t michael@0: ReadSetEntrySizeOfExcludingThis(nsCookieKey *aEntry, michael@0: MallocSizeOf aMallocSizeOf, michael@0: void *) michael@0: { michael@0: return aEntry->SizeOfExcludingThis(aMallocSizeOf); michael@0: } michael@0: michael@0: size_t michael@0: DBState::SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const michael@0: { michael@0: size_t amount = 0; michael@0: michael@0: amount += aMallocSizeOf(this); michael@0: amount += hostTable.SizeOfExcludingThis(HostTableEntrySizeOfExcludingThis, michael@0: aMallocSizeOf); michael@0: amount += hostArray.SizeOfExcludingThis(aMallocSizeOf); michael@0: for (uint32_t i = 0; i < hostArray.Length(); ++i) { michael@0: amount += hostArray[i].SizeOfExcludingThis(aMallocSizeOf); michael@0: } michael@0: amount += readSet.SizeOfExcludingThis(ReadSetEntrySizeOfExcludingThis, michael@0: aMallocSizeOf); michael@0: michael@0: return amount; michael@0: } michael@0: michael@0: /****************************************************************************** michael@0: * nsCookieService impl: michael@0: * singleton instance ctor/dtor methods michael@0: ******************************************************************************/ michael@0: michael@0: nsICookieService* michael@0: nsCookieService::GetXPCOMSingleton() michael@0: { michael@0: if (IsNeckoChild()) michael@0: return CookieServiceChild::GetSingleton(); michael@0: michael@0: return GetSingleton(); michael@0: } michael@0: michael@0: nsCookieService* michael@0: nsCookieService::GetSingleton() michael@0: { michael@0: NS_ASSERTION(!IsNeckoChild(), "not a parent process"); michael@0: michael@0: if (gCookieService) { michael@0: NS_ADDREF(gCookieService); michael@0: return gCookieService; michael@0: } michael@0: michael@0: // Create a new singleton nsCookieService. michael@0: // We AddRef only once since XPCOM has rules about the ordering of module michael@0: // teardowns - by the time our module destructor is called, it's too late to michael@0: // Release our members (e.g. nsIObserverService and nsIPrefBranch), since GC michael@0: // cycles have already been completed and would result in serious leaks. michael@0: // See bug 209571. michael@0: gCookieService = new nsCookieService(); michael@0: if (gCookieService) { michael@0: NS_ADDREF(gCookieService); michael@0: if (NS_FAILED(gCookieService->Init())) { michael@0: NS_RELEASE(gCookieService); michael@0: } michael@0: } michael@0: michael@0: return gCookieService; michael@0: } michael@0: michael@0: /* static */ void michael@0: nsCookieService::AppClearDataObserverInit() michael@0: { michael@0: nsCOMPtr observerService = do_GetService("@mozilla.org/observer-service;1"); michael@0: nsCOMPtr obs = new AppClearDataObserver(); michael@0: observerService->AddObserver(obs, TOPIC_WEB_APP_CLEAR_DATA, michael@0: /* holdsWeak= */ false); michael@0: } michael@0: michael@0: /****************************************************************************** michael@0: * nsCookieService impl: michael@0: * public methods michael@0: ******************************************************************************/ michael@0: michael@0: NS_IMPL_ISUPPORTS(nsCookieService, michael@0: nsICookieService, michael@0: nsICookieManager, michael@0: nsICookieManager2, michael@0: nsIObserver, michael@0: nsISupportsWeakReference, michael@0: nsIMemoryReporter) michael@0: michael@0: nsCookieService::nsCookieService() michael@0: : mDBState(nullptr) michael@0: , mCookieBehavior(BEHAVIOR_ACCEPT) michael@0: , mThirdPartySession(false) michael@0: , mMaxNumberOfCookies(kMaxNumberOfCookies) michael@0: , mMaxCookiesPerHost(kMaxCookiesPerHost) michael@0: , mCookiePurgeAge(kCookiePurgeAge) michael@0: { michael@0: } michael@0: michael@0: nsresult michael@0: nsCookieService::Init() michael@0: { michael@0: nsresult rv; michael@0: mTLDService = do_GetService(NS_EFFECTIVETLDSERVICE_CONTRACTID, &rv); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: michael@0: mIDNService = do_GetService(NS_IDNSERVICE_CONTRACTID, &rv); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: michael@0: mThirdPartyUtil = do_GetService(THIRDPARTYUTIL_CONTRACTID); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: michael@0: // init our pref and observer michael@0: nsCOMPtr prefBranch = do_GetService(NS_PREFSERVICE_CONTRACTID); michael@0: if (prefBranch) { michael@0: prefBranch->AddObserver(kPrefCookieBehavior, this, true); michael@0: prefBranch->AddObserver(kPrefMaxNumberOfCookies, this, true); michael@0: prefBranch->AddObserver(kPrefMaxCookiesPerHost, this, true); michael@0: prefBranch->AddObserver(kPrefCookiePurgeAge, this, true); michael@0: prefBranch->AddObserver(kPrefThirdPartySession, this, true); michael@0: PrefChanged(prefBranch); michael@0: } michael@0: michael@0: mStorageService = do_GetService("@mozilla.org/storage/service;1", &rv); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: michael@0: // Init our default, and possibly private DBStates. michael@0: InitDBStates(); michael@0: michael@0: RegisterWeakMemoryReporter(this); michael@0: michael@0: mObserverService = mozilla::services::GetObserverService(); michael@0: NS_ENSURE_STATE(mObserverService); michael@0: mObserverService->AddObserver(this, "profile-before-change", true); michael@0: mObserverService->AddObserver(this, "profile-do-change", true); michael@0: mObserverService->AddObserver(this, "last-pb-context-exited", true); michael@0: michael@0: mPermissionService = do_GetService(NS_COOKIEPERMISSION_CONTRACTID); michael@0: if (!mPermissionService) { michael@0: NS_WARNING("nsICookiePermission implementation not available - some features won't work!"); michael@0: COOKIE_LOGSTRING(PR_LOG_WARNING, ("Init(): nsICookiePermission implementation not available")); michael@0: } michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: void michael@0: nsCookieService::InitDBStates() michael@0: { michael@0: NS_ASSERTION(!mDBState, "already have a DBState"); michael@0: NS_ASSERTION(!mDefaultDBState, "already have a default DBState"); michael@0: NS_ASSERTION(!mPrivateDBState, "already have a private DBState"); michael@0: michael@0: // Create a new default DBState and set our current one. michael@0: mDefaultDBState = new DBState(); michael@0: mDBState = mDefaultDBState; michael@0: michael@0: mPrivateDBState = new DBState(); michael@0: michael@0: // Get our cookie file. michael@0: nsresult rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, michael@0: getter_AddRefs(mDefaultDBState->cookieFile)); michael@0: if (NS_FAILED(rv)) { michael@0: // We've already set up our DBStates appropriately; nothing more to do. michael@0: COOKIE_LOGSTRING(PR_LOG_WARNING, michael@0: ("InitDBStates(): couldn't get cookie file")); michael@0: return; michael@0: } michael@0: mDefaultDBState->cookieFile->AppendNative(NS_LITERAL_CSTRING(COOKIES_FILE)); michael@0: michael@0: // Attempt to open and read the database. If TryInitDB() returns RESULT_RETRY, michael@0: // do so. michael@0: OpenDBResult result = TryInitDB(false); michael@0: if (result == RESULT_RETRY) { michael@0: // Database may be corrupt. Synchronously close the connection, clean up the michael@0: // default DBState, and try again. michael@0: COOKIE_LOGSTRING(PR_LOG_WARNING, ("InitDBStates(): retrying TryInitDB()")); michael@0: CleanupCachedStatements(); michael@0: CleanupDefaultDBConnection(); michael@0: result = TryInitDB(true); michael@0: if (result == RESULT_RETRY) { michael@0: // We're done. Change the code to failure so we clean up below. michael@0: result = RESULT_FAILURE; michael@0: } michael@0: } michael@0: michael@0: if (result == RESULT_FAILURE) { michael@0: COOKIE_LOGSTRING(PR_LOG_WARNING, michael@0: ("InitDBStates(): TryInitDB() failed, closing connection")); michael@0: michael@0: // Connection failure is unrecoverable. Clean up our connection. We can run michael@0: // fine without persistent storage -- e.g. if there's no profile. michael@0: CleanupCachedStatements(); michael@0: CleanupDefaultDBConnection(); michael@0: } michael@0: } michael@0: michael@0: /* Attempt to open and read the database. If 'aRecreateDB' is true, try to michael@0: * move the existing database file out of the way and create a new one. michael@0: * michael@0: * @returns RESULT_OK if opening or creating the database succeeded; michael@0: * RESULT_RETRY if the database cannot be opened, is corrupt, or some michael@0: * other failure occurred that might be resolved by recreating the michael@0: * database; or RESULT_FAILED if there was an unrecoverable error and michael@0: * we must run without a database. michael@0: * michael@0: * If RESULT_RETRY or RESULT_FAILED is returned, the caller should perform michael@0: * cleanup of the default DBState. michael@0: */ michael@0: OpenDBResult michael@0: nsCookieService::TryInitDB(bool aRecreateDB) michael@0: { michael@0: NS_ASSERTION(!mDefaultDBState->dbConn, "nonnull dbConn"); michael@0: NS_ASSERTION(!mDefaultDBState->stmtInsert, "nonnull stmtInsert"); michael@0: NS_ASSERTION(!mDefaultDBState->insertListener, "nonnull insertListener"); michael@0: NS_ASSERTION(!mDefaultDBState->syncConn, "nonnull syncConn"); michael@0: michael@0: // Ditch an existing db, if we've been told to (i.e. it's corrupt). We don't michael@0: // want to delete it outright, since it may be useful for debugging purposes, michael@0: // so we move it out of the way. michael@0: nsresult rv; michael@0: if (aRecreateDB) { michael@0: nsCOMPtr backupFile; michael@0: mDefaultDBState->cookieFile->Clone(getter_AddRefs(backupFile)); michael@0: rv = backupFile->MoveToNative(nullptr, michael@0: NS_LITERAL_CSTRING(COOKIES_FILE ".bak")); michael@0: NS_ENSURE_SUCCESS(rv, RESULT_FAILURE); michael@0: } michael@0: michael@0: // This block provides scope for the Telemetry AutoTimer michael@0: { michael@0: Telemetry::AutoTimer michael@0: telemetry; michael@0: ReadAheadFile(mDefaultDBState->cookieFile); michael@0: michael@0: // open a connection to the cookie database, and only cache our connection michael@0: // and statements upon success. The connection is opened unshared to eliminate michael@0: // cache contention between the main and background threads. michael@0: rv = mStorageService->OpenUnsharedDatabase(mDefaultDBState->cookieFile, michael@0: getter_AddRefs(mDefaultDBState->dbConn)); michael@0: NS_ENSURE_SUCCESS(rv, RESULT_RETRY); michael@0: } michael@0: michael@0: // Set up our listeners. michael@0: mDefaultDBState->insertListener = new InsertCookieDBListener(mDefaultDBState); michael@0: mDefaultDBState->updateListener = new UpdateCookieDBListener(mDefaultDBState); michael@0: mDefaultDBState->removeListener = new RemoveCookieDBListener(mDefaultDBState); michael@0: mDefaultDBState->closeListener = new CloseCookieDBListener(mDefaultDBState); michael@0: michael@0: // Grow cookie db in 512KB increments michael@0: mDefaultDBState->dbConn->SetGrowthIncrement(512 * 1024, EmptyCString()); michael@0: michael@0: bool tableExists = false; michael@0: mDefaultDBState->dbConn->TableExists(NS_LITERAL_CSTRING("moz_cookies"), michael@0: &tableExists); michael@0: if (!tableExists) { michael@0: rv = CreateTable(); michael@0: NS_ENSURE_SUCCESS(rv, RESULT_RETRY); michael@0: michael@0: } else { michael@0: // table already exists; check the schema version before reading michael@0: int32_t dbSchemaVersion; michael@0: rv = mDefaultDBState->dbConn->GetSchemaVersion(&dbSchemaVersion); michael@0: NS_ENSURE_SUCCESS(rv, RESULT_RETRY); michael@0: michael@0: // Start a transaction for the whole migration block. michael@0: mozStorageTransaction transaction(mDefaultDBState->dbConn, true); michael@0: michael@0: switch (dbSchemaVersion) { michael@0: // Upgrading. michael@0: // Every time you increment the database schema, you need to implement michael@0: // the upgrading code from the previous version to the new one. If migration michael@0: // fails for any reason, it's a bug -- so we return RESULT_RETRY such that michael@0: // the original database will be saved, in the hopes that we might one day michael@0: // see it and fix it. michael@0: case 1: michael@0: { michael@0: // Add the lastAccessed column to the table. michael@0: rv = mDefaultDBState->dbConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( michael@0: "ALTER TABLE moz_cookies ADD lastAccessed INTEGER")); michael@0: NS_ENSURE_SUCCESS(rv, RESULT_RETRY); michael@0: } michael@0: // Fall through to the next upgrade. michael@0: michael@0: case 2: michael@0: { michael@0: // Add the baseDomain column and index to the table. michael@0: rv = mDefaultDBState->dbConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( michael@0: "ALTER TABLE moz_cookies ADD baseDomain TEXT")); michael@0: NS_ENSURE_SUCCESS(rv, RESULT_RETRY); michael@0: michael@0: // Compute the baseDomains for the table. This must be done eagerly michael@0: // otherwise we won't be able to synchronously read in individual michael@0: // domains on demand. michael@0: const int64_t SCHEMA2_IDX_ID = 0; michael@0: const int64_t SCHEMA2_IDX_HOST = 1; michael@0: nsCOMPtr select; michael@0: rv = mDefaultDBState->dbConn->CreateStatement(NS_LITERAL_CSTRING( michael@0: "SELECT id, host FROM moz_cookies"), getter_AddRefs(select)); michael@0: NS_ENSURE_SUCCESS(rv, RESULT_RETRY); michael@0: michael@0: nsCOMPtr update; michael@0: rv = mDefaultDBState->dbConn->CreateStatement(NS_LITERAL_CSTRING( michael@0: "UPDATE moz_cookies SET baseDomain = :baseDomain WHERE id = :id"), michael@0: getter_AddRefs(update)); michael@0: NS_ENSURE_SUCCESS(rv, RESULT_RETRY); michael@0: michael@0: nsCString baseDomain, host; michael@0: bool hasResult; michael@0: while (1) { michael@0: rv = select->ExecuteStep(&hasResult); michael@0: NS_ENSURE_SUCCESS(rv, RESULT_RETRY); michael@0: michael@0: if (!hasResult) michael@0: break; michael@0: michael@0: int64_t id = select->AsInt64(SCHEMA2_IDX_ID); michael@0: select->GetUTF8String(SCHEMA2_IDX_HOST, host); michael@0: michael@0: rv = GetBaseDomainFromHost(host, baseDomain); michael@0: NS_ENSURE_SUCCESS(rv, RESULT_RETRY); michael@0: michael@0: mozStorageStatementScoper scoper(update); michael@0: michael@0: rv = update->BindUTF8StringByName(NS_LITERAL_CSTRING("baseDomain"), michael@0: baseDomain); michael@0: NS_ASSERT_SUCCESS(rv); michael@0: rv = update->BindInt64ByName(NS_LITERAL_CSTRING("id"), michael@0: id); michael@0: NS_ASSERT_SUCCESS(rv); michael@0: michael@0: rv = update->ExecuteStep(&hasResult); michael@0: NS_ENSURE_SUCCESS(rv, RESULT_RETRY); michael@0: } michael@0: michael@0: // Create an index on baseDomain. michael@0: rv = mDefaultDBState->dbConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( michael@0: "CREATE INDEX moz_basedomain ON moz_cookies (baseDomain)")); michael@0: NS_ENSURE_SUCCESS(rv, RESULT_RETRY); michael@0: } michael@0: // Fall through to the next upgrade. michael@0: michael@0: case 3: michael@0: { michael@0: // Add the creationTime column to the table, and create a unique index michael@0: // on (name, host, path). Before we do this, we have to purge the table michael@0: // of expired cookies such that we know that the (name, host, path) michael@0: // index is truly unique -- otherwise we can't create the index. Note michael@0: // that we can't just execute a statement to delete all rows where the michael@0: // expiry column is in the past -- doing so would rely on the clock michael@0: // (both now and when previous cookies were set) being monotonic. michael@0: michael@0: // Select the whole table, and order by the fields we're interested in. michael@0: // This means we can simply do a linear traversal of the results and michael@0: // check for duplicates as we go. michael@0: const int64_t SCHEMA3_IDX_ID = 0; michael@0: const int64_t SCHEMA3_IDX_NAME = 1; michael@0: const int64_t SCHEMA3_IDX_HOST = 2; michael@0: const int64_t SCHEMA3_IDX_PATH = 3; michael@0: nsCOMPtr select; michael@0: rv = mDefaultDBState->dbConn->CreateStatement(NS_LITERAL_CSTRING( michael@0: "SELECT id, name, host, path FROM moz_cookies " michael@0: "ORDER BY name ASC, host ASC, path ASC, expiry ASC"), michael@0: getter_AddRefs(select)); michael@0: NS_ENSURE_SUCCESS(rv, RESULT_RETRY); michael@0: michael@0: nsCOMPtr deleteExpired; michael@0: rv = mDefaultDBState->dbConn->CreateStatement(NS_LITERAL_CSTRING( michael@0: "DELETE FROM moz_cookies WHERE id = :id"), michael@0: getter_AddRefs(deleteExpired)); michael@0: NS_ENSURE_SUCCESS(rv, RESULT_RETRY); michael@0: michael@0: // Read the first row. michael@0: bool hasResult; michael@0: rv = select->ExecuteStep(&hasResult); michael@0: NS_ENSURE_SUCCESS(rv, RESULT_RETRY); michael@0: michael@0: if (hasResult) { michael@0: nsCString name1, host1, path1; michael@0: int64_t id1 = select->AsInt64(SCHEMA3_IDX_ID); michael@0: select->GetUTF8String(SCHEMA3_IDX_NAME, name1); michael@0: select->GetUTF8String(SCHEMA3_IDX_HOST, host1); michael@0: select->GetUTF8String(SCHEMA3_IDX_PATH, path1); michael@0: michael@0: nsCString name2, host2, path2; michael@0: while (1) { michael@0: // Read the second row. michael@0: rv = select->ExecuteStep(&hasResult); michael@0: NS_ENSURE_SUCCESS(rv, RESULT_RETRY); michael@0: michael@0: if (!hasResult) michael@0: break; michael@0: michael@0: int64_t id2 = select->AsInt64(SCHEMA3_IDX_ID); michael@0: select->GetUTF8String(SCHEMA3_IDX_NAME, name2); michael@0: select->GetUTF8String(SCHEMA3_IDX_HOST, host2); michael@0: select->GetUTF8String(SCHEMA3_IDX_PATH, path2); michael@0: michael@0: // If the two rows match in (name, host, path), we know the earlier michael@0: // row has an earlier expiry time. Delete it. michael@0: if (name1 == name2 && host1 == host2 && path1 == path2) { michael@0: mozStorageStatementScoper scoper(deleteExpired); michael@0: michael@0: rv = deleteExpired->BindInt64ByName(NS_LITERAL_CSTRING("id"), michael@0: id1); michael@0: NS_ASSERT_SUCCESS(rv); michael@0: michael@0: rv = deleteExpired->ExecuteStep(&hasResult); michael@0: NS_ENSURE_SUCCESS(rv, RESULT_RETRY); michael@0: } michael@0: michael@0: // Make the second row the first for the next iteration. michael@0: name1 = name2; michael@0: host1 = host2; michael@0: path1 = path2; michael@0: id1 = id2; michael@0: } michael@0: } michael@0: michael@0: // Add the creationTime column to the table. michael@0: rv = mDefaultDBState->dbConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( michael@0: "ALTER TABLE moz_cookies ADD creationTime INTEGER")); michael@0: NS_ENSURE_SUCCESS(rv, RESULT_RETRY); michael@0: michael@0: // Copy the id of each row into the new creationTime column. michael@0: rv = mDefaultDBState->dbConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( michael@0: "UPDATE moz_cookies SET creationTime = " michael@0: "(SELECT id WHERE id = moz_cookies.id)")); michael@0: NS_ENSURE_SUCCESS(rv, RESULT_RETRY); michael@0: michael@0: // Create a unique index on (name, host, path) to allow fast lookup. michael@0: rv = mDefaultDBState->dbConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( michael@0: "CREATE UNIQUE INDEX moz_uniqueid " michael@0: "ON moz_cookies (name, host, path)")); michael@0: NS_ENSURE_SUCCESS(rv, RESULT_RETRY); michael@0: } michael@0: // Fall through to the next upgrade. michael@0: michael@0: case 4: michael@0: { michael@0: // We need to add appId/inBrowserElement, plus change a constraint on michael@0: // the table (unique entries now include appId/inBrowserElement): michael@0: // this requires creating a new table and copying the data to it. We michael@0: // then rename the new table to the old name. michael@0: // michael@0: // Why we made this change: appId/inBrowserElement allow "cookie jars" michael@0: // for Firefox OS. We create a separate cookie namespace per {appId, michael@0: // inBrowserElement}. When upgrading, we convert existing cookies michael@0: // (which imply we're on desktop/mobile) to use {0, false}, as that is michael@0: // the only namespace used by a non-Firefox-OS implementation. michael@0: michael@0: // Rename existing table michael@0: rv = mDefaultDBState->dbConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( michael@0: "ALTER TABLE moz_cookies RENAME TO moz_cookies_old")); michael@0: NS_ENSURE_SUCCESS(rv, RESULT_RETRY); michael@0: michael@0: // Drop existing index (CreateTable will create new one for new table) michael@0: rv = mDefaultDBState->dbConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( michael@0: "DROP INDEX moz_basedomain")); michael@0: NS_ENSURE_SUCCESS(rv, RESULT_RETRY); michael@0: michael@0: // Create new table (with new fields and new unique constraint) michael@0: rv = CreateTable(); michael@0: NS_ENSURE_SUCCESS(rv, RESULT_RETRY); michael@0: michael@0: // Copy data from old table, using appId/inBrowser=0 for existing rows michael@0: rv = mDefaultDBState->dbConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( michael@0: "INSERT INTO moz_cookies " michael@0: "(baseDomain, appId, inBrowserElement, name, value, host, path, expiry," michael@0: " lastAccessed, creationTime, isSecure, isHttpOnly) " michael@0: "SELECT baseDomain, 0, 0, name, value, host, path, expiry," michael@0: " lastAccessed, creationTime, isSecure, isHttpOnly " michael@0: "FROM moz_cookies_old")); michael@0: NS_ENSURE_SUCCESS(rv, RESULT_RETRY); michael@0: michael@0: // Drop old table michael@0: rv = mDefaultDBState->dbConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( michael@0: "DROP TABLE moz_cookies_old")); michael@0: NS_ENSURE_SUCCESS(rv, RESULT_RETRY); michael@0: michael@0: COOKIE_LOGSTRING(PR_LOG_DEBUG, michael@0: ("Upgraded database to schema version 5")); michael@0: } michael@0: michael@0: // No more upgrades. Update the schema version. michael@0: rv = mDefaultDBState->dbConn->SetSchemaVersion(COOKIES_SCHEMA_VERSION); michael@0: NS_ENSURE_SUCCESS(rv, RESULT_RETRY); michael@0: michael@0: case COOKIES_SCHEMA_VERSION: michael@0: break; michael@0: michael@0: case 0: michael@0: { michael@0: NS_WARNING("couldn't get schema version!"); michael@0: michael@0: // the table may be usable; someone might've just clobbered the schema michael@0: // version. we can treat this case like a downgrade using the codepath michael@0: // below, by verifying the columns we care about are all there. for now, michael@0: // re-set the schema version in the db, in case the checks succeed (if michael@0: // they don't, we're dropping the table anyway). michael@0: rv = mDefaultDBState->dbConn->SetSchemaVersion(COOKIES_SCHEMA_VERSION); michael@0: NS_ENSURE_SUCCESS(rv, RESULT_RETRY); michael@0: } michael@0: // fall through to downgrade check michael@0: michael@0: // downgrading. michael@0: // if columns have been added to the table, we can still use the ones we michael@0: // understand safely. if columns have been deleted or altered, just michael@0: // blow away the table and start from scratch! if you change the way michael@0: // a column is interpreted, make sure you also change its name so this michael@0: // check will catch it. michael@0: default: michael@0: { michael@0: // check if all the expected columns exist michael@0: nsCOMPtr stmt; michael@0: rv = mDefaultDBState->dbConn->CreateStatement(NS_LITERAL_CSTRING( michael@0: "SELECT " michael@0: "id, " michael@0: "baseDomain, " michael@0: "appId, " michael@0: "inBrowserElement, " michael@0: "name, " michael@0: "value, " michael@0: "host, " michael@0: "path, " michael@0: "expiry, " michael@0: "lastAccessed, " michael@0: "creationTime, " michael@0: "isSecure, " michael@0: "isHttpOnly " michael@0: "FROM moz_cookies"), getter_AddRefs(stmt)); michael@0: if (NS_SUCCEEDED(rv)) michael@0: break; michael@0: michael@0: // our columns aren't there - drop the table! michael@0: rv = mDefaultDBState->dbConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( michael@0: "DROP TABLE moz_cookies")); michael@0: NS_ENSURE_SUCCESS(rv, RESULT_RETRY); michael@0: michael@0: rv = CreateTable(); michael@0: NS_ENSURE_SUCCESS(rv, RESULT_RETRY); michael@0: } michael@0: break; michael@0: } michael@0: } michael@0: michael@0: // make operations on the table asynchronous, for performance michael@0: mDefaultDBState->dbConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( michael@0: "PRAGMA synchronous = OFF")); michael@0: michael@0: // Use write-ahead-logging for performance. We cap the autocheckpoint limit at michael@0: // 16 pages (around 500KB). michael@0: mDefaultDBState->dbConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( michael@0: MOZ_STORAGE_UNIQUIFY_QUERY_STR "PRAGMA journal_mode = WAL")); michael@0: mDefaultDBState->dbConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( michael@0: "PRAGMA wal_autocheckpoint = 16")); michael@0: michael@0: // cache frequently used statements (for insertion, deletion, and updating) michael@0: rv = mDefaultDBState->dbConn->CreateAsyncStatement(NS_LITERAL_CSTRING( michael@0: "INSERT INTO moz_cookies (" michael@0: "baseDomain, " michael@0: "appId, " michael@0: "inBrowserElement, " michael@0: "name, " michael@0: "value, " michael@0: "host, " michael@0: "path, " michael@0: "expiry, " michael@0: "lastAccessed, " michael@0: "creationTime, " michael@0: "isSecure, " michael@0: "isHttpOnly" michael@0: ") VALUES (" michael@0: ":baseDomain, " michael@0: ":appId, " michael@0: ":inBrowserElement, " michael@0: ":name, " michael@0: ":value, " michael@0: ":host, " michael@0: ":path, " michael@0: ":expiry, " michael@0: ":lastAccessed, " michael@0: ":creationTime, " michael@0: ":isSecure, " michael@0: ":isHttpOnly" michael@0: ")"), michael@0: getter_AddRefs(mDefaultDBState->stmtInsert)); michael@0: NS_ENSURE_SUCCESS(rv, RESULT_RETRY); michael@0: michael@0: rv = mDefaultDBState->dbConn->CreateAsyncStatement(NS_LITERAL_CSTRING( michael@0: "DELETE FROM moz_cookies " michael@0: "WHERE name = :name AND host = :host AND path = :path"), michael@0: getter_AddRefs(mDefaultDBState->stmtDelete)); michael@0: NS_ENSURE_SUCCESS(rv, RESULT_RETRY); michael@0: michael@0: rv = mDefaultDBState->dbConn->CreateAsyncStatement(NS_LITERAL_CSTRING( michael@0: "UPDATE moz_cookies SET lastAccessed = :lastAccessed " michael@0: "WHERE name = :name AND host = :host AND path = :path"), michael@0: getter_AddRefs(mDefaultDBState->stmtUpdate)); michael@0: NS_ENSURE_SUCCESS(rv, RESULT_RETRY); michael@0: michael@0: // if we deleted a corrupt db, don't attempt to import - return now michael@0: if (aRecreateDB) michael@0: return RESULT_OK; michael@0: michael@0: // check whether to import or just read in the db michael@0: if (tableExists) michael@0: return Read(); michael@0: michael@0: nsCOMPtr oldCookieFile; michael@0: rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, michael@0: getter_AddRefs(oldCookieFile)); michael@0: if (NS_FAILED(rv)) return RESULT_OK; michael@0: michael@0: // Import cookies, and clean up the old file regardless of success or failure. michael@0: // Note that we have to switch out our DBState temporarily, in case we're in michael@0: // private browsing mode; otherwise ImportCookies() won't be happy. michael@0: DBState* initialState = mDBState; michael@0: mDBState = mDefaultDBState; michael@0: oldCookieFile->AppendNative(NS_LITERAL_CSTRING(kOldCookieFileName)); michael@0: ImportCookies(oldCookieFile); michael@0: oldCookieFile->Remove(false); michael@0: mDBState = initialState; michael@0: michael@0: return RESULT_OK; michael@0: } michael@0: michael@0: // Sets the schema version and creates the moz_cookies table. michael@0: nsresult michael@0: nsCookieService::CreateTable() michael@0: { michael@0: // Set the schema version, before creating the table. michael@0: nsresult rv = mDefaultDBState->dbConn->SetSchemaVersion( michael@0: COOKIES_SCHEMA_VERSION); michael@0: if (NS_FAILED(rv)) return rv; michael@0: michael@0: // Create the table. We default appId/inBrowserElement to 0: this is so if michael@0: // users revert to an older Firefox version that doesn't know about these michael@0: // fields, any cookies set will still work once they upgrade back. michael@0: rv = mDefaultDBState->dbConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( michael@0: "CREATE TABLE moz_cookies (" michael@0: "id INTEGER PRIMARY KEY, " michael@0: "baseDomain TEXT, " michael@0: "appId INTEGER DEFAULT 0, " michael@0: "inBrowserElement INTEGER DEFAULT 0, " michael@0: "name TEXT, " michael@0: "value TEXT, " michael@0: "host TEXT, " michael@0: "path TEXT, " michael@0: "expiry INTEGER, " michael@0: "lastAccessed INTEGER, " michael@0: "creationTime INTEGER, " michael@0: "isSecure INTEGER, " michael@0: "isHttpOnly INTEGER, " michael@0: "CONSTRAINT moz_uniqueid UNIQUE (name, host, path, appId, inBrowserElement)" michael@0: ")")); michael@0: if (NS_FAILED(rv)) return rv; michael@0: michael@0: // Create an index on baseDomain. michael@0: return mDefaultDBState->dbConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( michael@0: "CREATE INDEX moz_basedomain ON moz_cookies (baseDomain, " michael@0: "appId, " michael@0: "inBrowserElement)")); michael@0: } michael@0: michael@0: void michael@0: nsCookieService::CloseDBStates() michael@0: { michael@0: // Null out our private and pointer DBStates regardless. michael@0: mPrivateDBState = nullptr; michael@0: mDBState = nullptr; michael@0: michael@0: // If we don't have a default DBState, we're done. michael@0: if (!mDefaultDBState) michael@0: return; michael@0: michael@0: // Cleanup cached statements before we can close anything. michael@0: CleanupCachedStatements(); michael@0: michael@0: if (mDefaultDBState->dbConn) { michael@0: // Cancel any pending read. No further results will be received by our michael@0: // read listener. michael@0: if (mDefaultDBState->pendingRead) { michael@0: CancelAsyncRead(true); michael@0: } michael@0: michael@0: // Asynchronously close the connection. We will null it below. michael@0: mDefaultDBState->dbConn->AsyncClose(mDefaultDBState->closeListener); michael@0: } michael@0: michael@0: CleanupDefaultDBConnection(); michael@0: michael@0: mDefaultDBState = nullptr; michael@0: } michael@0: michael@0: // Null out the statements. michael@0: // This must be done before closing the connection. michael@0: void michael@0: nsCookieService::CleanupCachedStatements() michael@0: { michael@0: mDefaultDBState->stmtInsert = nullptr; michael@0: mDefaultDBState->stmtDelete = nullptr; michael@0: mDefaultDBState->stmtUpdate = nullptr; michael@0: } michael@0: michael@0: // Null out the listeners, and the database connection itself. This michael@0: // will not null out the statements, cancel a pending read or michael@0: // asynchronously close the connection -- these must be done michael@0: // beforehand if necessary. michael@0: void michael@0: nsCookieService::CleanupDefaultDBConnection() michael@0: { michael@0: MOZ_ASSERT(!mDefaultDBState->stmtInsert, "stmtInsert has been cleaned up"); michael@0: MOZ_ASSERT(!mDefaultDBState->stmtDelete, "stmtDelete has been cleaned up"); michael@0: MOZ_ASSERT(!mDefaultDBState->stmtUpdate, "stmtUpdate has been cleaned up"); michael@0: michael@0: // Null out the database connections. If 'dbConn' has not been used for any michael@0: // asynchronous operations yet, this will synchronously close it; otherwise, michael@0: // it's expected that the caller has performed an AsyncClose prior. michael@0: mDefaultDBState->dbConn = nullptr; michael@0: mDefaultDBState->syncConn = nullptr; michael@0: michael@0: // Manually null out our listeners. This is necessary because they hold a michael@0: // strong ref to the DBState itself. They'll stay alive until whatever michael@0: // statements are still executing complete. michael@0: mDefaultDBState->readListener = nullptr; michael@0: mDefaultDBState->insertListener = nullptr; michael@0: mDefaultDBState->updateListener = nullptr; michael@0: mDefaultDBState->removeListener = nullptr; michael@0: mDefaultDBState->closeListener = nullptr; michael@0: } michael@0: michael@0: void michael@0: nsCookieService::HandleDBClosed(DBState* aDBState) michael@0: { michael@0: COOKIE_LOGSTRING(PR_LOG_DEBUG, michael@0: ("HandleDBClosed(): DBState %x closed", aDBState)); michael@0: michael@0: switch (aDBState->corruptFlag) { michael@0: case DBState::OK: { michael@0: // Database is healthy. Notify of closure. michael@0: mObserverService->NotifyObservers(nullptr, "cookie-db-closed", nullptr); michael@0: break; michael@0: } michael@0: case DBState::CLOSING_FOR_REBUILD: { michael@0: // Our close finished. Start the rebuild, and notify of db closure later. michael@0: RebuildCorruptDB(aDBState); michael@0: break; michael@0: } michael@0: case DBState::REBUILDING: { michael@0: // We encountered an error during rebuild, closed the database, and now michael@0: // here we are. We already have a 'cookies.sqlite.bak' from the original michael@0: // dead database; we don't want to overwrite it, so let's move this one to michael@0: // 'cookies.sqlite.bak-rebuild'. michael@0: nsCOMPtr backupFile; michael@0: aDBState->cookieFile->Clone(getter_AddRefs(backupFile)); michael@0: nsresult rv = backupFile->MoveToNative(nullptr, michael@0: NS_LITERAL_CSTRING(COOKIES_FILE ".bak-rebuild")); michael@0: michael@0: COOKIE_LOGSTRING(PR_LOG_WARNING, michael@0: ("HandleDBClosed(): DBState %x encountered error rebuilding db; move to " michael@0: "'cookies.sqlite.bak-rebuild' gave rv 0x%x", aDBState, rv)); michael@0: mObserverService->NotifyObservers(nullptr, "cookie-db-closed", nullptr); michael@0: break; michael@0: } michael@0: } michael@0: } michael@0: michael@0: void michael@0: nsCookieService::HandleCorruptDB(DBState* aDBState) michael@0: { michael@0: if (mDefaultDBState != aDBState) { michael@0: // We've either closed the state or we've switched profiles. It's getting michael@0: // a bit late to rebuild -- bail instead. michael@0: COOKIE_LOGSTRING(PR_LOG_WARNING, michael@0: ("HandleCorruptDB(): DBState %x is already closed, aborting", aDBState)); michael@0: return; michael@0: } michael@0: michael@0: COOKIE_LOGSTRING(PR_LOG_DEBUG, michael@0: ("HandleCorruptDB(): DBState %x has corruptFlag %u", aDBState, michael@0: aDBState->corruptFlag)); michael@0: michael@0: // Mark the database corrupt, so the close listener can begin reconstructing michael@0: // it. michael@0: switch (mDefaultDBState->corruptFlag) { michael@0: case DBState::OK: { michael@0: // Move to 'closing' state. michael@0: mDefaultDBState->corruptFlag = DBState::CLOSING_FOR_REBUILD; michael@0: michael@0: // Cancel any pending read and close the database. If we do have an michael@0: // in-flight read we want to throw away all the results so far -- we have no michael@0: // idea how consistent the database is. Note that we may have already michael@0: // canceled the read but not emptied our readSet; do so now. michael@0: mDefaultDBState->readSet.Clear(); michael@0: if (mDefaultDBState->pendingRead) { michael@0: CancelAsyncRead(true); michael@0: mDefaultDBState->syncConn = nullptr; michael@0: } michael@0: michael@0: CleanupCachedStatements(); michael@0: mDefaultDBState->dbConn->AsyncClose(mDefaultDBState->closeListener); michael@0: CleanupDefaultDBConnection(); michael@0: break; michael@0: } michael@0: case DBState::CLOSING_FOR_REBUILD: { michael@0: // We had an error while waiting for close completion. That's OK, just michael@0: // ignore it -- we're rebuilding anyway. michael@0: return; michael@0: } michael@0: case DBState::REBUILDING: { michael@0: // We had an error while rebuilding the DB. Game over. Close the database michael@0: // and let the close handler do nothing; then we'll move it out of the way. michael@0: CleanupCachedStatements(); michael@0: if (mDefaultDBState->dbConn) { michael@0: mDefaultDBState->dbConn->AsyncClose(mDefaultDBState->closeListener); michael@0: } michael@0: CleanupDefaultDBConnection(); michael@0: break; michael@0: } michael@0: } michael@0: } michael@0: michael@0: static PLDHashOperator michael@0: RebuildDBCallback(nsCookieEntry *aEntry, michael@0: void *aArg) michael@0: { michael@0: mozIStorageBindingParamsArray* paramsArray = michael@0: static_cast(aArg); michael@0: michael@0: const nsCookieEntry::ArrayType &cookies = aEntry->GetCookies(); michael@0: for (nsCookieEntry::IndexType i = 0; i < cookies.Length(); ++i) { michael@0: nsCookie* cookie = cookies[i]; michael@0: michael@0: if (!cookie->IsSession()) { michael@0: bindCookieParameters(paramsArray, aEntry, cookie); michael@0: } michael@0: } michael@0: michael@0: return PL_DHASH_NEXT; michael@0: } michael@0: michael@0: void michael@0: nsCookieService::RebuildCorruptDB(DBState* aDBState) michael@0: { michael@0: NS_ASSERTION(!aDBState->dbConn, "shouldn't have an open db connection"); michael@0: NS_ASSERTION(aDBState->corruptFlag == DBState::CLOSING_FOR_REBUILD, michael@0: "should be in CLOSING_FOR_REBUILD state"); michael@0: michael@0: aDBState->corruptFlag = DBState::REBUILDING; michael@0: michael@0: if (mDefaultDBState != aDBState) { michael@0: // We've either closed the state or we've switched profiles. It's getting michael@0: // a bit late to rebuild -- bail instead. In any case, we were waiting michael@0: // on rebuild completion to notify of the db closure, which won't happen -- michael@0: // do so now. michael@0: COOKIE_LOGSTRING(PR_LOG_WARNING, michael@0: ("RebuildCorruptDB(): DBState %x is stale, aborting", aDBState)); michael@0: mObserverService->NotifyObservers(nullptr, "cookie-db-closed", nullptr); michael@0: return; michael@0: } michael@0: michael@0: COOKIE_LOGSTRING(PR_LOG_DEBUG, michael@0: ("RebuildCorruptDB(): creating new database")); michael@0: michael@0: // The database has been closed, and we're ready to rebuild. Open a michael@0: // connection. michael@0: OpenDBResult result = TryInitDB(true); michael@0: if (result != RESULT_OK) { michael@0: // We're done. Reset our DB connection and statements, and notify of michael@0: // closure. michael@0: COOKIE_LOGSTRING(PR_LOG_WARNING, michael@0: ("RebuildCorruptDB(): TryInitDB() failed with result %u", result)); michael@0: CleanupCachedStatements(); michael@0: CleanupDefaultDBConnection(); michael@0: mDefaultDBState->corruptFlag = DBState::OK; michael@0: mObserverService->NotifyObservers(nullptr, "cookie-db-closed", nullptr); michael@0: return; michael@0: } michael@0: michael@0: // Notify observers that we're beginning the rebuild. michael@0: mObserverService->NotifyObservers(nullptr, "cookie-db-rebuilding", nullptr); michael@0: michael@0: // Enumerate the hash, and add cookies to the params array. michael@0: mozIStorageAsyncStatement* stmt = aDBState->stmtInsert; michael@0: nsCOMPtr paramsArray; michael@0: stmt->NewBindingParamsArray(getter_AddRefs(paramsArray)); michael@0: aDBState->hostTable.EnumerateEntries(RebuildDBCallback, paramsArray.get()); michael@0: michael@0: // Make sure we've got something to write. If we don't, we're done. michael@0: uint32_t length; michael@0: paramsArray->GetLength(&length); michael@0: if (length == 0) { michael@0: COOKIE_LOGSTRING(PR_LOG_DEBUG, michael@0: ("RebuildCorruptDB(): nothing to write, rebuild complete")); michael@0: mDefaultDBState->corruptFlag = DBState::OK; michael@0: return; michael@0: } michael@0: michael@0: // Execute the statement. If any errors crop up, we won't try again. michael@0: DebugOnly rv = stmt->BindParameters(paramsArray); michael@0: NS_ASSERT_SUCCESS(rv); michael@0: nsCOMPtr handle; michael@0: rv = stmt->ExecuteAsync(aDBState->insertListener, getter_AddRefs(handle)); michael@0: NS_ASSERT_SUCCESS(rv); michael@0: } michael@0: michael@0: nsCookieService::~nsCookieService() michael@0: { michael@0: CloseDBStates(); michael@0: michael@0: UnregisterWeakMemoryReporter(this); michael@0: michael@0: gCookieService = nullptr; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: nsCookieService::Observe(nsISupports *aSubject, michael@0: const char *aTopic, michael@0: const char16_t *aData) michael@0: { michael@0: // check the topic michael@0: if (!strcmp(aTopic, "profile-before-change")) { michael@0: // The profile is about to change, michael@0: // or is going away because the application is shutting down. michael@0: if (mDBState && mDBState->dbConn && michael@0: !nsCRT::strcmp(aData, MOZ_UTF16("shutdown-cleanse"))) { michael@0: // Clear the cookie db if we're in the default DBState. michael@0: RemoveAll(); michael@0: } michael@0: michael@0: // Close the default DB connection and null out our DBStates before michael@0: // changing. michael@0: CloseDBStates(); michael@0: michael@0: } else if (!strcmp(aTopic, "profile-do-change")) { michael@0: NS_ASSERTION(!mDefaultDBState, "shouldn't have a default DBState"); michael@0: NS_ASSERTION(!mPrivateDBState, "shouldn't have a private DBState"); michael@0: michael@0: // the profile has already changed; init the db from the new location. michael@0: // if we are in the private browsing state, however, we do not want to read michael@0: // data into it - we should instead put it into the default state, so it's michael@0: // ready for us if and when we switch back to it. michael@0: InitDBStates(); michael@0: michael@0: } else if (!strcmp(aTopic, NS_PREFBRANCH_PREFCHANGE_TOPIC_ID)) { michael@0: nsCOMPtr prefBranch = do_QueryInterface(aSubject); michael@0: if (prefBranch) michael@0: PrefChanged(prefBranch); michael@0: michael@0: } else if (!strcmp(aTopic, "last-pb-context-exited")) { michael@0: // Flush all the cookies stored by private browsing contexts michael@0: mPrivateDBState = new DBState(); michael@0: } michael@0: michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: nsCookieService::GetCookieString(nsIURI *aHostURI, michael@0: nsIChannel *aChannel, michael@0: char **aCookie) michael@0: { michael@0: return GetCookieStringCommon(aHostURI, aChannel, false, aCookie); michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: nsCookieService::GetCookieStringFromHttp(nsIURI *aHostURI, michael@0: nsIURI *aFirstURI, michael@0: nsIChannel *aChannel, michael@0: char **aCookie) michael@0: { michael@0: return GetCookieStringCommon(aHostURI, aChannel, true, aCookie); michael@0: } michael@0: michael@0: nsresult michael@0: nsCookieService::GetCookieStringCommon(nsIURI *aHostURI, michael@0: nsIChannel *aChannel, michael@0: bool aHttpBound, michael@0: char** aCookie) michael@0: { michael@0: NS_ENSURE_ARG(aHostURI); michael@0: NS_ENSURE_ARG(aCookie); michael@0: michael@0: // Determine whether the request is foreign. Failure is acceptable. michael@0: bool isForeign = true; michael@0: mThirdPartyUtil->IsThirdPartyChannel(aChannel, aHostURI, &isForeign); michael@0: michael@0: // Get app info, if channel is present. Else assume default namespace. michael@0: uint32_t appId = NECKO_NO_APP_ID; michael@0: bool inBrowserElement = false; michael@0: if (aChannel) { michael@0: NS_GetAppInfo(aChannel, &appId, &inBrowserElement); michael@0: } michael@0: michael@0: bool isPrivate = aChannel && NS_UsePrivateBrowsing(aChannel); michael@0: michael@0: nsAutoCString result; michael@0: GetCookieStringInternal(aHostURI, isForeign, aHttpBound, appId, michael@0: inBrowserElement, isPrivate, result); michael@0: *aCookie = result.IsEmpty() ? nullptr : ToNewCString(result); michael@0: return NS_OK; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: nsCookieService::SetCookieString(nsIURI *aHostURI, michael@0: nsIPrompt *aPrompt, michael@0: const char *aCookieHeader, michael@0: nsIChannel *aChannel) michael@0: { michael@0: // The aPrompt argument is deprecated and unused. Avoid introducing new michael@0: // code that uses this argument by warning if the value is non-null. michael@0: MOZ_ASSERT(!aPrompt); michael@0: if (aPrompt) { michael@0: nsCOMPtr aConsoleService = michael@0: do_GetService("@mozilla.org/consoleservice;1"); michael@0: if (aConsoleService) { michael@0: aConsoleService->LogStringMessage( michael@0: MOZ_UTF16("Non-null prompt ignored by nsCookieService.")); michael@0: } michael@0: } michael@0: return SetCookieStringCommon(aHostURI, aCookieHeader, nullptr, aChannel, michael@0: false); michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: nsCookieService::SetCookieStringFromHttp(nsIURI *aHostURI, michael@0: nsIURI *aFirstURI, michael@0: nsIPrompt *aPrompt, michael@0: const char *aCookieHeader, michael@0: const char *aServerTime, michael@0: nsIChannel *aChannel) michael@0: { michael@0: // The aPrompt argument is deprecated and unused. Avoid introducing new michael@0: // code that uses this argument by warning if the value is non-null. michael@0: MOZ_ASSERT(!aPrompt); michael@0: if (aPrompt) { michael@0: nsCOMPtr aConsoleService = michael@0: do_GetService("@mozilla.org/consoleservice;1"); michael@0: if (aConsoleService) { michael@0: aConsoleService->LogStringMessage( michael@0: MOZ_UTF16("Non-null prompt ignored by nsCookieService.")); michael@0: } michael@0: } michael@0: return SetCookieStringCommon(aHostURI, aCookieHeader, aServerTime, aChannel, michael@0: true); michael@0: } michael@0: michael@0: nsresult michael@0: nsCookieService::SetCookieStringCommon(nsIURI *aHostURI, michael@0: const char *aCookieHeader, michael@0: const char *aServerTime, michael@0: nsIChannel *aChannel, michael@0: bool aFromHttp) michael@0: { michael@0: NS_ENSURE_ARG(aHostURI); michael@0: NS_ENSURE_ARG(aCookieHeader); michael@0: michael@0: // Determine whether the request is foreign. Failure is acceptable. michael@0: bool isForeign = true; michael@0: mThirdPartyUtil->IsThirdPartyChannel(aChannel, aHostURI, &isForeign); michael@0: michael@0: // Get app info, if channel is present. Else assume default namespace. michael@0: uint32_t appId = NECKO_NO_APP_ID; michael@0: bool inBrowserElement = false; michael@0: if (aChannel) { michael@0: NS_GetAppInfo(aChannel, &appId, &inBrowserElement); michael@0: } michael@0: michael@0: bool isPrivate = aChannel && NS_UsePrivateBrowsing(aChannel); michael@0: michael@0: nsDependentCString cookieString(aCookieHeader); michael@0: nsDependentCString serverTime(aServerTime ? aServerTime : ""); michael@0: SetCookieStringInternal(aHostURI, isForeign, cookieString, michael@0: serverTime, aFromHttp, appId, inBrowserElement, michael@0: isPrivate, aChannel); michael@0: return NS_OK; michael@0: } michael@0: michael@0: void michael@0: nsCookieService::SetCookieStringInternal(nsIURI *aHostURI, michael@0: bool aIsForeign, michael@0: nsDependentCString &aCookieHeader, michael@0: const nsCString &aServerTime, michael@0: bool aFromHttp, michael@0: uint32_t aAppId, michael@0: bool aInBrowserElement, michael@0: bool aIsPrivate, michael@0: nsIChannel *aChannel) michael@0: { michael@0: NS_ASSERTION(aHostURI, "null host!"); michael@0: michael@0: if (!mDBState) { michael@0: NS_WARNING("No DBState! Profile already closed?"); michael@0: return; michael@0: } michael@0: michael@0: AutoRestore savePrevDBState(mDBState); michael@0: mDBState = aIsPrivate ? mPrivateDBState : mDefaultDBState; michael@0: michael@0: // get the base domain for the host URI. michael@0: // e.g. for "www.bbc.co.uk", this would be "bbc.co.uk". michael@0: // file:// URI's (i.e. with an empty host) are allowed, but any other michael@0: // scheme must have a non-empty host. A trailing dot in the host michael@0: // is acceptable. michael@0: bool requireHostMatch; michael@0: nsAutoCString baseDomain; michael@0: nsresult rv = GetBaseDomain(aHostURI, baseDomain, requireHostMatch); michael@0: if (NS_FAILED(rv)) { michael@0: COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, aCookieHeader, michael@0: "couldn't get base domain from URI"); michael@0: return; michael@0: } michael@0: michael@0: nsCookieKey key(baseDomain, aAppId, aInBrowserElement); michael@0: michael@0: // check default prefs michael@0: CookieStatus cookieStatus = CheckPrefs(aHostURI, aIsForeign, requireHostMatch, michael@0: aCookieHeader.get()); michael@0: // fire a notification if third party or if cookie was rejected michael@0: // (but not if there was an error) michael@0: switch (cookieStatus) { michael@0: case STATUS_REJECTED: michael@0: NotifyRejected(aHostURI); michael@0: if (aIsForeign) { michael@0: NotifyThirdParty(aHostURI, false, aChannel); michael@0: } michael@0: return; // Stop here michael@0: case STATUS_REJECTED_WITH_ERROR: michael@0: return; michael@0: case STATUS_ACCEPTED: // Fallthrough michael@0: case STATUS_ACCEPT_SESSION: michael@0: if (aIsForeign) { michael@0: NotifyThirdParty(aHostURI, true, aChannel); michael@0: } michael@0: break; michael@0: default: michael@0: break; michael@0: } michael@0: michael@0: // parse server local time. this is not just done here for efficiency michael@0: // reasons - if there's an error parsing it, and we need to default it michael@0: // to the current time, we must do it here since the current time in michael@0: // SetCookieInternal() will change for each cookie processed (e.g. if the michael@0: // user is prompted). michael@0: PRTime tempServerTime; michael@0: int64_t serverTime; michael@0: PRStatus result = PR_ParseTimeString(aServerTime.get(), true, michael@0: &tempServerTime); michael@0: if (result == PR_SUCCESS) { michael@0: serverTime = tempServerTime / int64_t(PR_USEC_PER_SEC); michael@0: } else { michael@0: serverTime = PR_Now() / PR_USEC_PER_SEC; michael@0: } michael@0: michael@0: // process each cookie in the header michael@0: while (SetCookieInternal(aHostURI, key, requireHostMatch, cookieStatus, michael@0: aCookieHeader, serverTime, aFromHttp, aChannel)) { michael@0: // document.cookie can only set one cookie at a time michael@0: if (!aFromHttp) michael@0: break; michael@0: } michael@0: } michael@0: michael@0: // notify observers that a cookie was rejected due to the users' prefs. michael@0: void michael@0: nsCookieService::NotifyRejected(nsIURI *aHostURI) michael@0: { michael@0: if (mObserverService) { michael@0: mObserverService->NotifyObservers(aHostURI, "cookie-rejected", nullptr); michael@0: } michael@0: } michael@0: michael@0: // notify observers that a third-party cookie was accepted/rejected michael@0: // if the cookie issuer is unknown, it defaults to "?" michael@0: void michael@0: nsCookieService::NotifyThirdParty(nsIURI *aHostURI, bool aIsAccepted, nsIChannel *aChannel) michael@0: { michael@0: if (!mObserverService) { michael@0: return; michael@0: } michael@0: michael@0: const char* topic; michael@0: michael@0: if (mDBState != mPrivateDBState) { michael@0: // Regular (non-private) browsing michael@0: if (aIsAccepted) { michael@0: topic = "third-party-cookie-accepted"; michael@0: } else { michael@0: topic = "third-party-cookie-rejected"; michael@0: } michael@0: } else { michael@0: // Private browsing michael@0: if (aIsAccepted) { michael@0: topic = "private-third-party-cookie-accepted"; michael@0: } else { michael@0: topic = "private-third-party-cookie-rejected"; michael@0: } michael@0: } michael@0: michael@0: do { michael@0: // Attempt to find the host of aChannel. michael@0: if (!aChannel) { michael@0: break; michael@0: } michael@0: nsCOMPtr channelURI; michael@0: nsresult rv = aChannel->GetURI(getter_AddRefs(channelURI)); michael@0: if (NS_FAILED(rv)) { michael@0: break; michael@0: } michael@0: michael@0: nsAutoCString referringHost; michael@0: rv = channelURI->GetHost(referringHost); michael@0: if (NS_FAILED(rv)) { michael@0: break; michael@0: } michael@0: michael@0: nsAutoString referringHostUTF16 = NS_ConvertUTF8toUTF16(referringHost); michael@0: mObserverService->NotifyObservers(aHostURI, michael@0: topic, michael@0: referringHostUTF16.get()); michael@0: return; michael@0: } while (0); michael@0: michael@0: // This can fail for a number of reasons, in which kind we fallback to "?" michael@0: mObserverService->NotifyObservers(aHostURI, michael@0: topic, michael@0: MOZ_UTF16("?")); michael@0: } michael@0: michael@0: // notify observers that the cookie list changed. there are five possible michael@0: // values for aData: michael@0: // "deleted" means a cookie was deleted. aSubject is the deleted cookie. michael@0: // "added" means a cookie was added. aSubject is the added cookie. michael@0: // "changed" means a cookie was altered. aSubject is the new cookie. michael@0: // "cleared" means the entire cookie list was cleared. aSubject is null. michael@0: // "batch-deleted" means a set of cookies was purged. aSubject is the list of michael@0: // cookies. michael@0: void michael@0: nsCookieService::NotifyChanged(nsISupports *aSubject, michael@0: const char16_t *aData) michael@0: { michael@0: const char* topic = mDBState == mPrivateDBState ? michael@0: "private-cookie-changed" : "cookie-changed"; michael@0: if (mObserverService) michael@0: mObserverService->NotifyObservers(aSubject, topic, aData); michael@0: } michael@0: michael@0: already_AddRefed michael@0: nsCookieService::CreatePurgeList(nsICookie2* aCookie) michael@0: { michael@0: nsCOMPtr removedList = michael@0: do_CreateInstance(NS_ARRAY_CONTRACTID); michael@0: removedList->AppendElement(aCookie, false); michael@0: return removedList.forget(); michael@0: } michael@0: michael@0: /****************************************************************************** michael@0: * nsCookieService: michael@0: * pref observer impl michael@0: ******************************************************************************/ michael@0: michael@0: void michael@0: nsCookieService::PrefChanged(nsIPrefBranch *aPrefBranch) michael@0: { michael@0: int32_t val; michael@0: if (NS_SUCCEEDED(aPrefBranch->GetIntPref(kPrefCookieBehavior, &val))) michael@0: mCookieBehavior = (uint8_t) LIMIT(val, 0, 3, 0); michael@0: michael@0: if (NS_SUCCEEDED(aPrefBranch->GetIntPref(kPrefMaxNumberOfCookies, &val))) michael@0: mMaxNumberOfCookies = (uint16_t) LIMIT(val, 1, 0xFFFF, kMaxNumberOfCookies); michael@0: michael@0: if (NS_SUCCEEDED(aPrefBranch->GetIntPref(kPrefMaxCookiesPerHost, &val))) michael@0: mMaxCookiesPerHost = (uint16_t) LIMIT(val, 1, 0xFFFF, kMaxCookiesPerHost); michael@0: michael@0: if (NS_SUCCEEDED(aPrefBranch->GetIntPref(kPrefCookiePurgeAge, &val))) { michael@0: mCookiePurgeAge = michael@0: int64_t(LIMIT(val, 0, INT32_MAX, INT32_MAX)) * PR_USEC_PER_SEC; michael@0: } michael@0: michael@0: bool boolval; michael@0: if (NS_SUCCEEDED(aPrefBranch->GetBoolPref(kPrefThirdPartySession, &boolval))) michael@0: mThirdPartySession = boolval; michael@0: } michael@0: michael@0: /****************************************************************************** michael@0: * nsICookieManager impl: michael@0: * nsICookieManager michael@0: ******************************************************************************/ michael@0: michael@0: NS_IMETHODIMP michael@0: nsCookieService::RemoveAll() michael@0: { michael@0: if (!mDBState) { michael@0: NS_WARNING("No DBState! Profile already closed?"); michael@0: return NS_ERROR_NOT_AVAILABLE; michael@0: } michael@0: michael@0: RemoveAllFromMemory(); michael@0: michael@0: // clear the cookie file michael@0: if (mDBState->dbConn) { michael@0: NS_ASSERTION(mDBState == mDefaultDBState, "not in default DB state"); michael@0: michael@0: // Cancel any pending read. No further results will be received by our michael@0: // read listener. michael@0: if (mDefaultDBState->pendingRead) { michael@0: CancelAsyncRead(true); michael@0: } michael@0: michael@0: nsCOMPtr stmt; michael@0: nsresult rv = mDefaultDBState->dbConn->CreateAsyncStatement(NS_LITERAL_CSTRING( michael@0: "DELETE FROM moz_cookies"), getter_AddRefs(stmt)); michael@0: if (NS_SUCCEEDED(rv)) { michael@0: nsCOMPtr handle; michael@0: rv = stmt->ExecuteAsync(mDefaultDBState->removeListener, michael@0: getter_AddRefs(handle)); michael@0: NS_ASSERT_SUCCESS(rv); michael@0: } else { michael@0: // Recreate the database. michael@0: COOKIE_LOGSTRING(PR_LOG_DEBUG, michael@0: ("RemoveAll(): corruption detected with rv 0x%x", rv)); michael@0: HandleCorruptDB(mDefaultDBState); michael@0: } michael@0: } michael@0: michael@0: NotifyChanged(nullptr, MOZ_UTF16("cleared")); michael@0: return NS_OK; michael@0: } michael@0: michael@0: static PLDHashOperator michael@0: COMArrayCallback(nsCookieEntry *aEntry, michael@0: void *aArg) michael@0: { michael@0: nsCOMArray *data = static_cast *>(aArg); michael@0: michael@0: const nsCookieEntry::ArrayType &cookies = aEntry->GetCookies(); michael@0: for (nsCookieEntry::IndexType i = 0; i < cookies.Length(); ++i) { michael@0: data->AppendObject(cookies[i]); michael@0: } michael@0: michael@0: return PL_DHASH_NEXT; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: nsCookieService::GetEnumerator(nsISimpleEnumerator **aEnumerator) michael@0: { michael@0: if (!mDBState) { michael@0: NS_WARNING("No DBState! Profile already closed?"); michael@0: return NS_ERROR_NOT_AVAILABLE; michael@0: } michael@0: michael@0: EnsureReadComplete(); michael@0: michael@0: nsCOMArray cookieList(mDBState->cookieCount); michael@0: mDBState->hostTable.EnumerateEntries(COMArrayCallback, &cookieList); michael@0: michael@0: return NS_NewArrayEnumerator(aEnumerator, cookieList); michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: nsCookieService::Add(const nsACString &aHost, michael@0: const nsACString &aPath, michael@0: const nsACString &aName, michael@0: const nsACString &aValue, michael@0: bool aIsSecure, michael@0: bool aIsHttpOnly, michael@0: bool aIsSession, michael@0: int64_t aExpiry) michael@0: { michael@0: if (!mDBState) { michael@0: NS_WARNING("No DBState! Profile already closed?"); michael@0: return NS_ERROR_NOT_AVAILABLE; michael@0: } michael@0: michael@0: // first, normalize the hostname, and fail if it contains illegal characters. michael@0: nsAutoCString host(aHost); michael@0: nsresult rv = NormalizeHost(host); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: michael@0: // get the base domain for the host URI. michael@0: // e.g. for "www.bbc.co.uk", this would be "bbc.co.uk". michael@0: nsAutoCString baseDomain; michael@0: rv = GetBaseDomainFromHost(host, baseDomain); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: michael@0: int64_t currentTimeInUsec = PR_Now(); michael@0: michael@0: nsRefPtr cookie = michael@0: nsCookie::Create(aName, aValue, host, aPath, michael@0: aExpiry, michael@0: currentTimeInUsec, michael@0: nsCookie::GenerateUniqueCreationTime(currentTimeInUsec), michael@0: aIsSession, michael@0: aIsSecure, michael@0: aIsHttpOnly); michael@0: if (!cookie) { michael@0: return NS_ERROR_OUT_OF_MEMORY; michael@0: } michael@0: michael@0: AddInternal(DEFAULT_APP_KEY(baseDomain), cookie, currentTimeInUsec, nullptr, nullptr, true); michael@0: return NS_OK; michael@0: } michael@0: michael@0: michael@0: nsresult michael@0: nsCookieService::Remove(const nsACString& aHost, uint32_t aAppId, michael@0: bool aInBrowserElement, const nsACString& aName, michael@0: const nsACString& aPath, bool aBlocked) michael@0: { michael@0: if (!mDBState) { michael@0: NS_WARNING("No DBState! Profile already closed?"); michael@0: return NS_ERROR_NOT_AVAILABLE; michael@0: } michael@0: michael@0: // first, normalize the hostname, and fail if it contains illegal characters. michael@0: nsAutoCString host(aHost); michael@0: nsresult rv = NormalizeHost(host); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: michael@0: nsAutoCString baseDomain; michael@0: rv = GetBaseDomainFromHost(host, baseDomain); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: michael@0: nsListIter matchIter; michael@0: nsRefPtr cookie; michael@0: if (FindCookie(nsCookieKey(baseDomain, aAppId, aInBrowserElement), michael@0: host, michael@0: PromiseFlatCString(aName), michael@0: PromiseFlatCString(aPath), michael@0: matchIter)) { michael@0: cookie = matchIter.Cookie(); michael@0: RemoveCookieFromList(matchIter); michael@0: } michael@0: michael@0: // check if we need to add the host to the permissions blacklist. michael@0: if (aBlocked && mPermissionService) { michael@0: // strip off the domain dot, if necessary michael@0: if (!host.IsEmpty() && host.First() == '.') michael@0: host.Cut(0, 1); michael@0: michael@0: host.Insert(NS_LITERAL_CSTRING("http://"), 0); michael@0: michael@0: nsCOMPtr uri; michael@0: NS_NewURI(getter_AddRefs(uri), host); michael@0: michael@0: if (uri) michael@0: mPermissionService->SetAccess(uri, nsICookiePermission::ACCESS_DENY); michael@0: } michael@0: michael@0: if (cookie) { michael@0: // Everything's done. Notify observers. michael@0: NotifyChanged(cookie, MOZ_UTF16("deleted")); michael@0: } michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: nsCookieService::Remove(const nsACString &aHost, michael@0: const nsACString &aName, michael@0: const nsACString &aPath, michael@0: bool aBlocked) michael@0: { michael@0: return Remove(aHost, NECKO_NO_APP_ID, false, aName, aPath, aBlocked); michael@0: } michael@0: michael@0: /****************************************************************************** michael@0: * nsCookieService impl: michael@0: * private file I/O functions michael@0: ******************************************************************************/ michael@0: michael@0: // Begin an asynchronous read from the database. michael@0: OpenDBResult michael@0: nsCookieService::Read() michael@0: { michael@0: // Set up a statement for the read. Note that our query specifies that michael@0: // 'baseDomain' not be nullptr -- see below for why. michael@0: nsCOMPtr stmtRead; michael@0: nsresult rv = mDefaultDBState->dbConn->CreateAsyncStatement(NS_LITERAL_CSTRING( michael@0: "SELECT " michael@0: "name, " michael@0: "value, " michael@0: "host, " michael@0: "path, " michael@0: "expiry, " michael@0: "lastAccessed, " michael@0: "creationTime, " michael@0: "isSecure, " michael@0: "isHttpOnly, " michael@0: "baseDomain, " michael@0: "appId, " michael@0: "inBrowserElement " michael@0: "FROM moz_cookies " michael@0: "WHERE baseDomain NOTNULL"), getter_AddRefs(stmtRead)); michael@0: NS_ENSURE_SUCCESS(rv, RESULT_RETRY); michael@0: michael@0: // Set up a statement to delete any rows with a nullptr 'baseDomain' michael@0: // column. This takes care of any cookies set by browsers that don't michael@0: // understand the 'baseDomain' column, where the database schema version michael@0: // is from one that does. (This would occur when downgrading.) michael@0: nsCOMPtr stmtDeleteNull; michael@0: rv = mDefaultDBState->dbConn->CreateAsyncStatement(NS_LITERAL_CSTRING( michael@0: "DELETE FROM moz_cookies WHERE baseDomain ISNULL"), michael@0: getter_AddRefs(stmtDeleteNull)); michael@0: NS_ENSURE_SUCCESS(rv, RESULT_RETRY); michael@0: michael@0: // Start a new connection for sync reads, to reduce contention with the michael@0: // background thread. We need to do this before we kick off write statements, michael@0: // since they can lock the database and prevent connections from being opened. michael@0: rv = mStorageService->OpenUnsharedDatabase(mDefaultDBState->cookieFile, michael@0: getter_AddRefs(mDefaultDBState->syncConn)); michael@0: NS_ENSURE_SUCCESS(rv, RESULT_RETRY); michael@0: michael@0: // Init our readSet hash and execute the statements. Note that, after this michael@0: // point, we cannot fail without altering the cleanup code in InitDBStates() michael@0: // to handle closing of the now-asynchronous connection. michael@0: mDefaultDBState->hostArray.SetCapacity(kMaxNumberOfCookies); michael@0: michael@0: mDefaultDBState->readListener = new ReadCookieDBListener(mDefaultDBState); michael@0: rv = stmtRead->ExecuteAsync(mDefaultDBState->readListener, michael@0: getter_AddRefs(mDefaultDBState->pendingRead)); michael@0: NS_ASSERT_SUCCESS(rv); michael@0: michael@0: nsCOMPtr handle; michael@0: rv = stmtDeleteNull->ExecuteAsync(mDefaultDBState->removeListener, michael@0: getter_AddRefs(handle)); michael@0: NS_ASSERT_SUCCESS(rv); michael@0: michael@0: return RESULT_OK; michael@0: } michael@0: michael@0: // Extract data from a single result row and create an nsCookie. michael@0: // This is templated since 'T' is different for sync vs async results. michael@0: template nsCookie* michael@0: nsCookieService::GetCookieFromRow(T &aRow) michael@0: { michael@0: // Skip reading 'baseDomain' -- up to the caller. michael@0: nsCString name, value, host, path; michael@0: DebugOnly rv = aRow->GetUTF8String(IDX_NAME, name); michael@0: NS_ASSERT_SUCCESS(rv); michael@0: rv = aRow->GetUTF8String(IDX_VALUE, value); michael@0: NS_ASSERT_SUCCESS(rv); michael@0: rv = aRow->GetUTF8String(IDX_HOST, host); michael@0: NS_ASSERT_SUCCESS(rv); michael@0: rv = aRow->GetUTF8String(IDX_PATH, path); michael@0: NS_ASSERT_SUCCESS(rv); michael@0: michael@0: int64_t expiry = aRow->AsInt64(IDX_EXPIRY); michael@0: int64_t lastAccessed = aRow->AsInt64(IDX_LAST_ACCESSED); michael@0: int64_t creationTime = aRow->AsInt64(IDX_CREATION_TIME); michael@0: bool isSecure = 0 != aRow->AsInt32(IDX_SECURE); michael@0: bool isHttpOnly = 0 != aRow->AsInt32(IDX_HTTPONLY); michael@0: michael@0: // Create a new nsCookie and assign the data. michael@0: return nsCookie::Create(name, value, host, path, michael@0: expiry, michael@0: lastAccessed, michael@0: creationTime, michael@0: false, michael@0: isSecure, michael@0: isHttpOnly); michael@0: } michael@0: michael@0: void michael@0: nsCookieService::AsyncReadComplete() michael@0: { michael@0: // We may be in the private browsing DB state, with a pending read on the michael@0: // default DB state. (This would occur if we started up in private browsing michael@0: // mode.) As long as we do all our operations on the default state, we're OK. michael@0: NS_ASSERTION(mDefaultDBState, "no default DBState"); michael@0: NS_ASSERTION(mDefaultDBState->pendingRead, "no pending read"); michael@0: NS_ASSERTION(mDefaultDBState->readListener, "no read listener"); michael@0: michael@0: // Merge the data read on the background thread with the data synchronously michael@0: // read on the main thread. Note that transactions on the cookie table may michael@0: // have occurred on the main thread since, making the background data stale. michael@0: for (uint32_t i = 0; i < mDefaultDBState->hostArray.Length(); ++i) { michael@0: const CookieDomainTuple &tuple = mDefaultDBState->hostArray[i]; michael@0: michael@0: // Tiebreak: if the given base domain has already been read in, ignore michael@0: // the background data. Note that readSet may contain domains that were michael@0: // queried but found not to be in the db -- that's harmless. michael@0: if (mDefaultDBState->readSet.GetEntry(tuple.key)) michael@0: continue; michael@0: michael@0: AddCookieToList(tuple.key, tuple.cookie, mDefaultDBState, nullptr, false); michael@0: } michael@0: michael@0: mDefaultDBState->stmtReadDomain = nullptr; michael@0: mDefaultDBState->pendingRead = nullptr; michael@0: mDefaultDBState->readListener = nullptr; michael@0: mDefaultDBState->syncConn = nullptr; michael@0: mDefaultDBState->hostArray.Clear(); michael@0: mDefaultDBState->readSet.Clear(); michael@0: michael@0: COOKIE_LOGSTRING(PR_LOG_DEBUG, ("Read(): %ld cookies read", michael@0: mDefaultDBState->cookieCount)); michael@0: michael@0: mObserverService->NotifyObservers(nullptr, "cookie-db-read", nullptr); michael@0: } michael@0: michael@0: void michael@0: nsCookieService::CancelAsyncRead(bool aPurgeReadSet) michael@0: { michael@0: // We may be in the private browsing DB state, with a pending read on the michael@0: // default DB state. (This would occur if we started up in private browsing michael@0: // mode.) As long as we do all our operations on the default state, we're OK. michael@0: NS_ASSERTION(mDefaultDBState, "no default DBState"); michael@0: NS_ASSERTION(mDefaultDBState->pendingRead, "no pending read"); michael@0: NS_ASSERTION(mDefaultDBState->readListener, "no read listener"); michael@0: michael@0: // Cancel the pending read, kill the read listener, and empty the array michael@0: // of data already read in on the background thread. michael@0: mDefaultDBState->readListener->Cancel(); michael@0: DebugOnly rv = mDefaultDBState->pendingRead->Cancel(); michael@0: NS_ASSERT_SUCCESS(rv); michael@0: michael@0: mDefaultDBState->stmtReadDomain = nullptr; michael@0: mDefaultDBState->pendingRead = nullptr; michael@0: mDefaultDBState->readListener = nullptr; michael@0: mDefaultDBState->hostArray.Clear(); michael@0: michael@0: // Only clear the 'readSet' table if we no longer need to know what set of michael@0: // data is already accounted for. michael@0: if (aPurgeReadSet) michael@0: mDefaultDBState->readSet.Clear(); michael@0: } michael@0: michael@0: void michael@0: nsCookieService::EnsureReadDomain(const nsCookieKey &aKey) michael@0: { michael@0: NS_ASSERTION(!mDBState->dbConn || mDBState == mDefaultDBState, michael@0: "not in default db state"); michael@0: michael@0: // Fast path 1: nothing to read, or we've already finished reading. michael@0: if (MOZ_LIKELY(!mDBState->dbConn || !mDefaultDBState->pendingRead)) michael@0: return; michael@0: michael@0: // Fast path 2: already read in this particular domain. michael@0: if (MOZ_LIKELY(mDefaultDBState->readSet.GetEntry(aKey))) michael@0: return; michael@0: michael@0: // Read in the data synchronously. michael@0: // see IDX_NAME, etc. for parameter indexes michael@0: nsresult rv; michael@0: if (!mDefaultDBState->stmtReadDomain) { michael@0: // Cache the statement, since it's likely to be used again. michael@0: rv = mDefaultDBState->syncConn->CreateStatement(NS_LITERAL_CSTRING( michael@0: "SELECT " michael@0: "name, " michael@0: "value, " michael@0: "host, " michael@0: "path, " michael@0: "expiry, " michael@0: "lastAccessed, " michael@0: "creationTime, " michael@0: "isSecure, " michael@0: "isHttpOnly " michael@0: "FROM moz_cookies " michael@0: "WHERE baseDomain = :baseDomain " michael@0: " AND appId = :appId " michael@0: " AND inBrowserElement = :inBrowserElement"), michael@0: getter_AddRefs(mDefaultDBState->stmtReadDomain)); michael@0: michael@0: if (NS_FAILED(rv)) { michael@0: // Recreate the database. michael@0: COOKIE_LOGSTRING(PR_LOG_DEBUG, michael@0: ("EnsureReadDomain(): corruption detected when creating statement " michael@0: "with rv 0x%x", rv)); michael@0: HandleCorruptDB(mDefaultDBState); michael@0: return; michael@0: } michael@0: } michael@0: michael@0: NS_ASSERTION(mDefaultDBState->syncConn, "should have a sync db connection"); michael@0: michael@0: mozStorageStatementScoper scoper(mDefaultDBState->stmtReadDomain); michael@0: michael@0: rv = mDefaultDBState->stmtReadDomain->BindUTF8StringByName( michael@0: NS_LITERAL_CSTRING("baseDomain"), aKey.mBaseDomain); michael@0: NS_ASSERT_SUCCESS(rv); michael@0: rv = mDefaultDBState->stmtReadDomain->BindInt32ByName( michael@0: NS_LITERAL_CSTRING("appId"), aKey.mAppId); michael@0: NS_ASSERT_SUCCESS(rv); michael@0: rv = mDefaultDBState->stmtReadDomain->BindInt32ByName( michael@0: NS_LITERAL_CSTRING("inBrowserElement"), aKey.mInBrowserElement ? 1 : 0); michael@0: NS_ASSERT_SUCCESS(rv); michael@0: michael@0: michael@0: bool hasResult; michael@0: nsCString name, value, host, path; michael@0: nsAutoTArray, kMaxCookiesPerHost> array; michael@0: while (1) { michael@0: rv = mDefaultDBState->stmtReadDomain->ExecuteStep(&hasResult); michael@0: if (NS_FAILED(rv)) { michael@0: // Recreate the database. michael@0: COOKIE_LOGSTRING(PR_LOG_DEBUG, michael@0: ("EnsureReadDomain(): corruption detected when reading result " michael@0: "with rv 0x%x", rv)); michael@0: HandleCorruptDB(mDefaultDBState); michael@0: return; michael@0: } michael@0: michael@0: if (!hasResult) michael@0: break; michael@0: michael@0: array.AppendElement(GetCookieFromRow(mDefaultDBState->stmtReadDomain)); michael@0: } michael@0: michael@0: // Add the cookies to the table in a single operation. This makes sure that michael@0: // either all the cookies get added, or in the case of corruption, none. michael@0: for (uint32_t i = 0; i < array.Length(); ++i) { michael@0: AddCookieToList(aKey, array[i], mDefaultDBState, nullptr, false); michael@0: } michael@0: michael@0: // Add it to the hashset of read entries, so we don't read it again. michael@0: mDefaultDBState->readSet.PutEntry(aKey); michael@0: michael@0: COOKIE_LOGSTRING(PR_LOG_DEBUG, michael@0: ("EnsureReadDomain(): %ld cookies read for base domain %s, " michael@0: " appId=%u, inBrowser=%d", array.Length(), aKey.mBaseDomain.get(), michael@0: (unsigned)aKey.mAppId, (int)aKey.mInBrowserElement)); michael@0: } michael@0: michael@0: void michael@0: nsCookieService::EnsureReadComplete() michael@0: { michael@0: NS_ASSERTION(!mDBState->dbConn || mDBState == mDefaultDBState, michael@0: "not in default db state"); michael@0: michael@0: // Fast path 1: nothing to read, or we've already finished reading. michael@0: if (MOZ_LIKELY(!mDBState->dbConn || !mDefaultDBState->pendingRead)) michael@0: return; michael@0: michael@0: // Cancel the pending read, so we don't get any more results. michael@0: CancelAsyncRead(false); michael@0: michael@0: // Read in the data synchronously. michael@0: // see IDX_NAME, etc. for parameter indexes michael@0: nsCOMPtr stmt; michael@0: nsresult rv = mDefaultDBState->syncConn->CreateStatement(NS_LITERAL_CSTRING( michael@0: "SELECT " michael@0: "name, " michael@0: "value, " michael@0: "host, " michael@0: "path, " michael@0: "expiry, " michael@0: "lastAccessed, " michael@0: "creationTime, " michael@0: "isSecure, " michael@0: "isHttpOnly, " michael@0: "baseDomain, " michael@0: "appId, " michael@0: "inBrowserElement " michael@0: "FROM moz_cookies " michael@0: "WHERE baseDomain NOTNULL"), getter_AddRefs(stmt)); michael@0: michael@0: if (NS_FAILED(rv)) { michael@0: // Recreate the database. michael@0: COOKIE_LOGSTRING(PR_LOG_DEBUG, michael@0: ("EnsureReadComplete(): corruption detected when creating statement " michael@0: "with rv 0x%x", rv)); michael@0: HandleCorruptDB(mDefaultDBState); michael@0: return; michael@0: } michael@0: michael@0: nsCString baseDomain, name, value, host, path; michael@0: uint32_t appId; michael@0: bool inBrowserElement, hasResult; michael@0: nsAutoTArray array; michael@0: while (1) { michael@0: rv = stmt->ExecuteStep(&hasResult); michael@0: if (NS_FAILED(rv)) { michael@0: // Recreate the database. michael@0: COOKIE_LOGSTRING(PR_LOG_DEBUG, michael@0: ("EnsureReadComplete(): corruption detected when reading result " michael@0: "with rv 0x%x", rv)); michael@0: HandleCorruptDB(mDefaultDBState); michael@0: return; michael@0: } michael@0: michael@0: if (!hasResult) michael@0: break; michael@0: michael@0: // Make sure we haven't already read the data. michael@0: stmt->GetUTF8String(IDX_BASE_DOMAIN, baseDomain); michael@0: appId = static_cast(stmt->AsInt32(IDX_APP_ID)); michael@0: inBrowserElement = static_cast(stmt->AsInt32(IDX_BROWSER_ELEM)); michael@0: nsCookieKey key(baseDomain, appId, inBrowserElement); michael@0: if (mDefaultDBState->readSet.GetEntry(key)) michael@0: continue; michael@0: michael@0: CookieDomainTuple* tuple = array.AppendElement(); michael@0: tuple->key = key; michael@0: tuple->cookie = GetCookieFromRow(stmt); michael@0: } michael@0: michael@0: // Add the cookies to the table in a single operation. This makes sure that michael@0: // either all the cookies get added, or in the case of corruption, none. michael@0: for (uint32_t i = 0; i < array.Length(); ++i) { michael@0: CookieDomainTuple& tuple = array[i]; michael@0: AddCookieToList(tuple.key, tuple.cookie, mDefaultDBState, nullptr, michael@0: false); michael@0: } michael@0: michael@0: mDefaultDBState->syncConn = nullptr; michael@0: mDefaultDBState->readSet.Clear(); michael@0: michael@0: COOKIE_LOGSTRING(PR_LOG_DEBUG, michael@0: ("EnsureReadComplete(): %ld cookies read", array.Length())); michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: nsCookieService::ImportCookies(nsIFile *aCookieFile) michael@0: { michael@0: if (!mDBState) { michael@0: NS_WARNING("No DBState! Profile already closed?"); michael@0: return NS_ERROR_NOT_AVAILABLE; michael@0: } michael@0: michael@0: // Make sure we're in the default DB state. We don't want people importing michael@0: // cookies into a private browsing session! michael@0: if (mDBState != mDefaultDBState) { michael@0: NS_WARNING("Trying to import cookies in a private browsing session!"); michael@0: return NS_ERROR_NOT_AVAILABLE; michael@0: } michael@0: michael@0: nsresult rv; michael@0: nsCOMPtr fileInputStream; michael@0: rv = NS_NewLocalFileInputStream(getter_AddRefs(fileInputStream), aCookieFile); michael@0: if (NS_FAILED(rv)) return rv; michael@0: michael@0: nsCOMPtr lineInputStream = do_QueryInterface(fileInputStream, &rv); michael@0: if (NS_FAILED(rv)) return rv; michael@0: michael@0: // First, ensure we've read in everything from the database, if we have one. michael@0: EnsureReadComplete(); michael@0: michael@0: static const char kTrue[] = "TRUE"; michael@0: michael@0: nsAutoCString buffer, baseDomain; michael@0: bool isMore = true; michael@0: int32_t hostIndex, isDomainIndex, pathIndex, secureIndex, expiresIndex, nameIndex, cookieIndex; michael@0: nsASingleFragmentCString::char_iterator iter; michael@0: int32_t numInts; michael@0: int64_t expires; michael@0: bool isDomain, isHttpOnly = false; michael@0: uint32_t originalCookieCount = mDefaultDBState->cookieCount; michael@0: michael@0: int64_t currentTimeInUsec = PR_Now(); michael@0: int64_t currentTime = currentTimeInUsec / PR_USEC_PER_SEC; michael@0: // we use lastAccessedCounter to keep cookies in recently-used order, michael@0: // so we start by initializing to currentTime (somewhat arbitrary) michael@0: int64_t lastAccessedCounter = currentTimeInUsec; michael@0: michael@0: /* file format is: michael@0: * michael@0: * host \t isDomain \t path \t secure \t expires \t name \t cookie michael@0: * michael@0: * if this format isn't respected we move onto the next line in the file. michael@0: * isDomain is "TRUE" or "FALSE" (default to "FALSE") michael@0: * isSecure is "TRUE" or "FALSE" (default to "TRUE") michael@0: * expires is a int64_t integer michael@0: * note 1: cookie can contain tabs. michael@0: * note 2: cookies will be stored in order of lastAccessed time: michael@0: * most-recently used come first; least-recently-used come last. michael@0: */ michael@0: michael@0: /* michael@0: * ...but due to bug 178933, we hide HttpOnly cookies from older code michael@0: * in a comment, so they don't expose HttpOnly cookies to JS. michael@0: * michael@0: * The format for HttpOnly cookies is michael@0: * michael@0: * #HttpOnly_host \t isDomain \t path \t secure \t expires \t name \t cookie michael@0: * michael@0: */ michael@0: michael@0: // We will likely be adding a bunch of cookies to the DB, so we use async michael@0: // batching with storage to make this super fast. michael@0: nsCOMPtr paramsArray; michael@0: if (originalCookieCount == 0 && mDefaultDBState->dbConn) { michael@0: mDefaultDBState->stmtInsert->NewBindingParamsArray(getter_AddRefs(paramsArray)); michael@0: } michael@0: michael@0: while (isMore && NS_SUCCEEDED(lineInputStream->ReadLine(buffer, &isMore))) { michael@0: if (StringBeginsWith(buffer, NS_LITERAL_CSTRING(kHttpOnlyPrefix))) { michael@0: isHttpOnly = true; michael@0: hostIndex = sizeof(kHttpOnlyPrefix) - 1; michael@0: } else if (buffer.IsEmpty() || buffer.First() == '#') { michael@0: continue; michael@0: } else { michael@0: isHttpOnly = false; michael@0: hostIndex = 0; michael@0: } michael@0: michael@0: // this is a cheap, cheesy way of parsing a tab-delimited line into michael@0: // string indexes, which can be lopped off into substrings. just for michael@0: // purposes of obfuscation, it also checks that each token was found. michael@0: // todo: use iterators? michael@0: if ((isDomainIndex = buffer.FindChar('\t', hostIndex) + 1) == 0 || michael@0: (pathIndex = buffer.FindChar('\t', isDomainIndex) + 1) == 0 || michael@0: (secureIndex = buffer.FindChar('\t', pathIndex) + 1) == 0 || michael@0: (expiresIndex = buffer.FindChar('\t', secureIndex) + 1) == 0 || michael@0: (nameIndex = buffer.FindChar('\t', expiresIndex) + 1) == 0 || michael@0: (cookieIndex = buffer.FindChar('\t', nameIndex) + 1) == 0) { michael@0: continue; michael@0: } michael@0: michael@0: // check the expirytime first - if it's expired, ignore michael@0: // nullstomp the trailing tab, to avoid copying the string michael@0: buffer.BeginWriting(iter); michael@0: *(iter += nameIndex - 1) = char(0); michael@0: numInts = PR_sscanf(buffer.get() + expiresIndex, "%lld", &expires); michael@0: if (numInts != 1 || expires < currentTime) { michael@0: continue; michael@0: } michael@0: michael@0: isDomain = Substring(buffer, isDomainIndex, pathIndex - isDomainIndex - 1).EqualsLiteral(kTrue); michael@0: const nsASingleFragmentCString &host = Substring(buffer, hostIndex, isDomainIndex - hostIndex - 1); michael@0: // check for bad legacy cookies (domain not starting with a dot, or containing a port), michael@0: // and discard michael@0: if ((isDomain && !host.IsEmpty() && host.First() != '.') || michael@0: host.FindChar(':') != kNotFound) { michael@0: continue; michael@0: } michael@0: michael@0: // compute the baseDomain from the host michael@0: rv = GetBaseDomainFromHost(host, baseDomain); michael@0: if (NS_FAILED(rv)) michael@0: continue; michael@0: michael@0: // pre-existing cookies have appId=0, inBrowser=false michael@0: nsCookieKey key = DEFAULT_APP_KEY(baseDomain); michael@0: michael@0: // Create a new nsCookie and assign the data. We don't know the cookie michael@0: // creation time, so just use the current time to generate a unique one. michael@0: nsRefPtr newCookie = michael@0: nsCookie::Create(Substring(buffer, nameIndex, cookieIndex - nameIndex - 1), michael@0: Substring(buffer, cookieIndex, buffer.Length() - cookieIndex), michael@0: host, michael@0: Substring(buffer, pathIndex, secureIndex - pathIndex - 1), michael@0: expires, michael@0: lastAccessedCounter, michael@0: nsCookie::GenerateUniqueCreationTime(currentTimeInUsec), michael@0: false, michael@0: Substring(buffer, secureIndex, expiresIndex - secureIndex - 1).EqualsLiteral(kTrue), michael@0: isHttpOnly); michael@0: if (!newCookie) { michael@0: return NS_ERROR_OUT_OF_MEMORY; michael@0: } michael@0: michael@0: // trick: preserve the most-recently-used cookie ordering, michael@0: // by successively decrementing the lastAccessed time michael@0: lastAccessedCounter--; michael@0: michael@0: if (originalCookieCount == 0) { michael@0: AddCookieToList(key, newCookie, mDefaultDBState, paramsArray); michael@0: } michael@0: else { michael@0: AddInternal(key, newCookie, currentTimeInUsec, michael@0: nullptr, nullptr, true); michael@0: } michael@0: } michael@0: michael@0: // If we need to write to disk, do so now. michael@0: if (paramsArray) { michael@0: uint32_t length; michael@0: paramsArray->GetLength(&length); michael@0: if (length) { michael@0: rv = mDefaultDBState->stmtInsert->BindParameters(paramsArray); michael@0: NS_ASSERT_SUCCESS(rv); michael@0: nsCOMPtr handle; michael@0: rv = mDefaultDBState->stmtInsert->ExecuteAsync( michael@0: mDefaultDBState->insertListener, getter_AddRefs(handle)); michael@0: NS_ASSERT_SUCCESS(rv); michael@0: } michael@0: } michael@0: michael@0: michael@0: COOKIE_LOGSTRING(PR_LOG_DEBUG, ("ImportCookies(): %ld cookies imported", michael@0: mDefaultDBState->cookieCount)); michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: /****************************************************************************** michael@0: * nsCookieService impl: michael@0: * private GetCookie/SetCookie helpers michael@0: ******************************************************************************/ michael@0: michael@0: // helper function for GetCookieList michael@0: static inline bool ispathdelimiter(char c) { return c == '/' || c == '?' || c == '#' || c == ';'; } michael@0: michael@0: // Comparator class for sorting cookies before sending to a server. michael@0: class CompareCookiesForSending michael@0: { michael@0: public: michael@0: bool Equals(const nsCookie* aCookie1, const nsCookie* aCookie2) const michael@0: { michael@0: return aCookie1->CreationTime() == aCookie2->CreationTime() && michael@0: aCookie2->Path().Length() == aCookie1->Path().Length(); michael@0: } michael@0: michael@0: bool LessThan(const nsCookie* aCookie1, const nsCookie* aCookie2) const michael@0: { michael@0: // compare by cookie path length in accordance with RFC2109 michael@0: int32_t result = aCookie2->Path().Length() - aCookie1->Path().Length(); michael@0: if (result != 0) michael@0: return result < 0; michael@0: michael@0: // when path lengths match, older cookies should be listed first. this is michael@0: // required for backwards compatibility since some websites erroneously michael@0: // depend on receiving cookies in the order in which they were sent to the michael@0: // browser! see bug 236772. michael@0: return aCookie1->CreationTime() < aCookie2->CreationTime(); michael@0: } michael@0: }; michael@0: michael@0: void michael@0: nsCookieService::GetCookieStringInternal(nsIURI *aHostURI, michael@0: bool aIsForeign, michael@0: bool aHttpBound, michael@0: uint32_t aAppId, michael@0: bool aInBrowserElement, michael@0: bool aIsPrivate, michael@0: nsCString &aCookieString) michael@0: { michael@0: NS_ASSERTION(aHostURI, "null host!"); michael@0: michael@0: if (!mDBState) { michael@0: NS_WARNING("No DBState! Profile already closed?"); michael@0: return; michael@0: } michael@0: michael@0: AutoRestore savePrevDBState(mDBState); michael@0: mDBState = aIsPrivate ? mPrivateDBState : mDefaultDBState; michael@0: michael@0: // get the base domain, host, and path from the URI. michael@0: // e.g. for "www.bbc.co.uk", the base domain would be "bbc.co.uk". michael@0: // file:// URI's (i.e. with an empty host) are allowed, but any other michael@0: // scheme must have a non-empty host. A trailing dot in the host michael@0: // is acceptable. michael@0: bool requireHostMatch; michael@0: nsAutoCString baseDomain, hostFromURI, pathFromURI; michael@0: nsresult rv = GetBaseDomain(aHostURI, baseDomain, requireHostMatch); michael@0: if (NS_SUCCEEDED(rv)) michael@0: rv = aHostURI->GetAsciiHost(hostFromURI); michael@0: if (NS_SUCCEEDED(rv)) michael@0: rv = aHostURI->GetPath(pathFromURI); michael@0: if (NS_FAILED(rv)) { michael@0: COOKIE_LOGFAILURE(GET_COOKIE, aHostURI, nullptr, "invalid host/path from URI"); michael@0: return; michael@0: } michael@0: michael@0: // check default prefs michael@0: CookieStatus cookieStatus = CheckPrefs(aHostURI, aIsForeign, requireHostMatch, michael@0: nullptr); michael@0: // for GetCookie(), we don't fire rejection notifications. michael@0: switch (cookieStatus) { michael@0: case STATUS_REJECTED: michael@0: case STATUS_REJECTED_WITH_ERROR: michael@0: return; michael@0: default: michael@0: break; michael@0: } michael@0: michael@0: // check if aHostURI is using an https secure protocol. michael@0: // if it isn't, then we can't send a secure cookie over the connection. michael@0: // if SchemeIs fails, assume an insecure connection, to be on the safe side michael@0: bool isSecure; michael@0: if (NS_FAILED(aHostURI->SchemeIs("https", &isSecure))) { michael@0: isSecure = false; michael@0: } michael@0: michael@0: nsCookie *cookie; michael@0: nsAutoTArray foundCookieList; michael@0: int64_t currentTimeInUsec = PR_Now(); michael@0: int64_t currentTime = currentTimeInUsec / PR_USEC_PER_SEC; michael@0: bool stale = false; michael@0: michael@0: nsCookieKey key(baseDomain, aAppId, aInBrowserElement); michael@0: EnsureReadDomain(key); michael@0: michael@0: // perform the hash lookup michael@0: nsCookieEntry *entry = mDBState->hostTable.GetEntry(key); michael@0: if (!entry) michael@0: return; michael@0: michael@0: // iterate the cookies! michael@0: const nsCookieEntry::ArrayType &cookies = entry->GetCookies(); michael@0: for (nsCookieEntry::IndexType i = 0; i < cookies.Length(); ++i) { michael@0: cookie = cookies[i]; michael@0: michael@0: // check the host, since the base domain lookup is conservative. michael@0: // first, check for an exact host or domain cookie match, e.g. "google.com" michael@0: // or ".google.com"; second a subdomain match, e.g. michael@0: // host = "mail.google.com", cookie domain = ".google.com". michael@0: if (cookie->RawHost() != hostFromURI && michael@0: !(cookie->IsDomain() && StringEndsWith(hostFromURI, cookie->Host()))) michael@0: continue; michael@0: michael@0: // if the cookie is secure and the host scheme isn't, we can't send it michael@0: if (cookie->IsSecure() && !isSecure) michael@0: continue; michael@0: michael@0: // if the cookie is httpOnly and it's not going directly to the HTTP michael@0: // connection, don't send it michael@0: if (cookie->IsHttpOnly() && !aHttpBound) michael@0: continue; michael@0: michael@0: // calculate cookie path length, excluding trailing '/' michael@0: uint32_t cookiePathLen = cookie->Path().Length(); michael@0: if (cookiePathLen > 0 && cookie->Path().Last() == '/') michael@0: --cookiePathLen; michael@0: michael@0: // if the nsIURI path is shorter than the cookie path, don't send it back michael@0: if (!StringBeginsWith(pathFromURI, Substring(cookie->Path(), 0, cookiePathLen))) michael@0: continue; michael@0: michael@0: if (pathFromURI.Length() > cookiePathLen && michael@0: !ispathdelimiter(pathFromURI.CharAt(cookiePathLen))) { michael@0: /* michael@0: * |ispathdelimiter| tests four cases: '/', '?', '#', and ';'. michael@0: * '/' is the "standard" case; the '?' test allows a site at host/abc?def michael@0: * to receive a cookie that has a path attribute of abc. this seems michael@0: * strange but at least one major site (citibank, bug 156725) depends michael@0: * on it. The test for # and ; are put in to proactively avoid problems michael@0: * with other sites - these are the only other chars allowed in the path. michael@0: */ michael@0: continue; michael@0: } michael@0: michael@0: // check if the cookie has expired michael@0: if (cookie->Expiry() <= currentTime) { michael@0: continue; michael@0: } michael@0: michael@0: // all checks passed - add to list and check if lastAccessed stamp needs updating michael@0: foundCookieList.AppendElement(cookie); michael@0: if (currentTimeInUsec - cookie->LastAccessed() > kCookieStaleThreshold) michael@0: stale = true; michael@0: } michael@0: michael@0: int32_t count = foundCookieList.Length(); michael@0: if (count == 0) michael@0: return; michael@0: michael@0: // update lastAccessed timestamps. we only do this if the timestamp is stale michael@0: // by a certain amount, to avoid thrashing the db during pageload. michael@0: if (stale) { michael@0: // Create an array of parameters to bind to our update statement. Batching michael@0: // is OK here since we're updating cookies with no interleaved operations. michael@0: nsCOMPtr paramsArray; michael@0: mozIStorageAsyncStatement* stmt = mDBState->stmtUpdate; michael@0: if (mDBState->dbConn) { michael@0: stmt->NewBindingParamsArray(getter_AddRefs(paramsArray)); michael@0: } michael@0: michael@0: for (int32_t i = 0; i < count; ++i) { michael@0: cookie = foundCookieList.ElementAt(i); michael@0: michael@0: if (currentTimeInUsec - cookie->LastAccessed() > kCookieStaleThreshold) michael@0: UpdateCookieInList(cookie, currentTimeInUsec, paramsArray); michael@0: } michael@0: // Update the database now if necessary. michael@0: if (paramsArray) { michael@0: uint32_t length; michael@0: paramsArray->GetLength(&length); michael@0: if (length) { michael@0: DebugOnly rv = stmt->BindParameters(paramsArray); michael@0: NS_ASSERT_SUCCESS(rv); michael@0: nsCOMPtr handle; michael@0: rv = stmt->ExecuteAsync(mDBState->updateListener, michael@0: getter_AddRefs(handle)); michael@0: NS_ASSERT_SUCCESS(rv); michael@0: } michael@0: } michael@0: } michael@0: michael@0: // return cookies in order of path length; longest to shortest. michael@0: // this is required per RFC2109. if cookies match in length, michael@0: // then sort by creation time (see bug 236772). michael@0: foundCookieList.Sort(CompareCookiesForSending()); michael@0: michael@0: for (int32_t i = 0; i < count; ++i) { michael@0: cookie = foundCookieList.ElementAt(i); michael@0: michael@0: // check if we have anything to write michael@0: if (!cookie->Name().IsEmpty() || !cookie->Value().IsEmpty()) { michael@0: // if we've already added a cookie to the return list, append a "; " so michael@0: // that subsequent cookies are delimited in the final list. michael@0: if (!aCookieString.IsEmpty()) { michael@0: aCookieString.AppendLiteral("; "); michael@0: } michael@0: michael@0: if (!cookie->Name().IsEmpty()) { michael@0: // we have a name and value - write both michael@0: aCookieString += cookie->Name() + NS_LITERAL_CSTRING("=") + cookie->Value(); michael@0: } else { michael@0: // just write value michael@0: aCookieString += cookie->Value(); michael@0: } michael@0: } michael@0: } michael@0: michael@0: if (!aCookieString.IsEmpty()) michael@0: COOKIE_LOGSUCCESS(GET_COOKIE, aHostURI, aCookieString, nullptr, false); michael@0: } michael@0: michael@0: // processes a single cookie, and returns true if there are more cookies michael@0: // to be processed michael@0: bool michael@0: nsCookieService::SetCookieInternal(nsIURI *aHostURI, michael@0: const nsCookieKey &aKey, michael@0: bool aRequireHostMatch, michael@0: CookieStatus aStatus, michael@0: nsDependentCString &aCookieHeader, michael@0: int64_t aServerTime, michael@0: bool aFromHttp, michael@0: nsIChannel *aChannel) michael@0: { michael@0: NS_ASSERTION(aHostURI, "null host!"); michael@0: michael@0: // create a stack-based nsCookieAttributes, to store all the michael@0: // attributes parsed from the cookie michael@0: nsCookieAttributes cookieAttributes; michael@0: michael@0: // init expiryTime such that session cookies won't prematurely expire michael@0: cookieAttributes.expiryTime = INT64_MAX; michael@0: michael@0: // aCookieHeader is an in/out param to point to the next cookie, if michael@0: // there is one. Save the present value for logging purposes michael@0: nsDependentCString savedCookieHeader(aCookieHeader); michael@0: michael@0: // newCookie says whether there are multiple cookies in the header; michael@0: // so we can handle them separately. michael@0: bool newCookie = ParseAttributes(aCookieHeader, cookieAttributes); michael@0: michael@0: int64_t currentTimeInUsec = PR_Now(); michael@0: michael@0: // calculate expiry time of cookie. michael@0: cookieAttributes.isSession = GetExpiry(cookieAttributes, aServerTime, michael@0: currentTimeInUsec / PR_USEC_PER_SEC); michael@0: if (aStatus == STATUS_ACCEPT_SESSION) { michael@0: // force lifetime to session. note that the expiration time, if set above, michael@0: // will still apply. michael@0: cookieAttributes.isSession = true; michael@0: } michael@0: michael@0: // reject cookie if it's over the size limit, per RFC2109 michael@0: if ((cookieAttributes.name.Length() + cookieAttributes.value.Length()) > kMaxBytesPerCookie) { michael@0: COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, savedCookieHeader, "cookie too big (> 4kb)"); michael@0: return newCookie; michael@0: } michael@0: michael@0: if (cookieAttributes.name.FindChar('\t') != kNotFound) { michael@0: COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, savedCookieHeader, "invalid name character"); michael@0: return newCookie; michael@0: } michael@0: michael@0: // domain & path checks michael@0: if (!CheckDomain(cookieAttributes, aHostURI, aKey.mBaseDomain, aRequireHostMatch)) { michael@0: COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, savedCookieHeader, "failed the domain tests"); michael@0: return newCookie; michael@0: } michael@0: if (!CheckPath(cookieAttributes, aHostURI)) { michael@0: COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, savedCookieHeader, "failed the path tests"); michael@0: return newCookie; michael@0: } michael@0: michael@0: // create a new nsCookie and copy attributes michael@0: nsRefPtr cookie = michael@0: nsCookie::Create(cookieAttributes.name, michael@0: cookieAttributes.value, michael@0: cookieAttributes.host, michael@0: cookieAttributes.path, michael@0: cookieAttributes.expiryTime, michael@0: currentTimeInUsec, michael@0: nsCookie::GenerateUniqueCreationTime(currentTimeInUsec), michael@0: cookieAttributes.isSession, michael@0: cookieAttributes.isSecure, michael@0: cookieAttributes.isHttpOnly); michael@0: if (!cookie) michael@0: return newCookie; michael@0: michael@0: // check permissions from site permission list, or ask the user, michael@0: // to determine if we can set the cookie michael@0: if (mPermissionService) { michael@0: bool permission; michael@0: mPermissionService->CanSetCookie(aHostURI, michael@0: aChannel, michael@0: static_cast(static_cast(cookie)), michael@0: &cookieAttributes.isSession, michael@0: &cookieAttributes.expiryTime, michael@0: &permission); michael@0: if (!permission) { michael@0: COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, savedCookieHeader, "cookie rejected by permission manager"); michael@0: NotifyRejected(aHostURI); michael@0: return newCookie; michael@0: } michael@0: michael@0: // update isSession and expiry attributes, in case they changed michael@0: cookie->SetIsSession(cookieAttributes.isSession); michael@0: cookie->SetExpiry(cookieAttributes.expiryTime); michael@0: } michael@0: michael@0: // add the cookie to the list. AddInternal() takes care of logging. michael@0: // we get the current time again here, since it may have changed during prompting michael@0: AddInternal(aKey, cookie, PR_Now(), aHostURI, savedCookieHeader.get(), michael@0: aFromHttp); michael@0: return newCookie; michael@0: } michael@0: michael@0: // this is a backend function for adding a cookie to the list, via SetCookie. michael@0: // also used in the cookie manager, for profile migration from IE. michael@0: // it either replaces an existing cookie; or adds the cookie to the hashtable, michael@0: // and deletes a cookie (if maximum number of cookies has been michael@0: // reached). also performs list maintenance by removing expired cookies. michael@0: void michael@0: nsCookieService::AddInternal(const nsCookieKey &aKey, michael@0: nsCookie *aCookie, michael@0: int64_t aCurrentTimeInUsec, michael@0: nsIURI *aHostURI, michael@0: const char *aCookieHeader, michael@0: bool aFromHttp) michael@0: { michael@0: int64_t currentTime = aCurrentTimeInUsec / PR_USEC_PER_SEC; michael@0: michael@0: // if the new cookie is httponly, make sure we're not coming from script michael@0: if (!aFromHttp && aCookie->IsHttpOnly()) { michael@0: COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, aCookieHeader, michael@0: "cookie is httponly; coming from script"); michael@0: return; michael@0: } michael@0: michael@0: nsListIter matchIter; michael@0: bool foundCookie = FindCookie(aKey, aCookie->Host(), michael@0: aCookie->Name(), aCookie->Path(), matchIter); michael@0: michael@0: nsRefPtr oldCookie; michael@0: nsCOMPtr purgedList; michael@0: if (foundCookie) { michael@0: oldCookie = matchIter.Cookie(); michael@0: michael@0: // Check if the old cookie is stale (i.e. has already expired). If so, we michael@0: // need to be careful about the semantics of removing it and adding the new michael@0: // cookie: we want the behavior wrt adding the new cookie to be the same as michael@0: // if it didn't exist, but we still want to fire a removal notification. michael@0: if (oldCookie->Expiry() <= currentTime) { michael@0: if (aCookie->Expiry() <= currentTime) { michael@0: // The new cookie has expired and the old one is stale. Nothing to do. michael@0: COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, aCookieHeader, michael@0: "cookie has already expired"); michael@0: return; michael@0: } michael@0: michael@0: // Remove the stale cookie. We save notification for later, once all list michael@0: // modifications are complete. michael@0: RemoveCookieFromList(matchIter); michael@0: COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, aCookieHeader, michael@0: "stale cookie was purged"); michael@0: purgedList = CreatePurgeList(oldCookie); michael@0: michael@0: // We've done all we need to wrt removing and notifying the stale cookie. michael@0: // From here on out, we pretend pretend it didn't exist, so that we michael@0: // preserve expected notification semantics when adding the new cookie. michael@0: foundCookie = false; michael@0: michael@0: } else { michael@0: // If the old cookie is httponly, make sure we're not coming from script. michael@0: if (!aFromHttp && oldCookie->IsHttpOnly()) { michael@0: COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, aCookieHeader, michael@0: "previously stored cookie is httponly; coming from script"); michael@0: return; michael@0: } michael@0: michael@0: // Remove the old cookie. michael@0: RemoveCookieFromList(matchIter); michael@0: michael@0: // If the new cookie has expired -- i.e. the intent was simply to delete michael@0: // the old cookie -- then we're done. michael@0: if (aCookie->Expiry() <= currentTime) { michael@0: COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, aCookieHeader, michael@0: "previously stored cookie was deleted"); michael@0: NotifyChanged(oldCookie, MOZ_UTF16("deleted")); michael@0: return; michael@0: } michael@0: michael@0: // Preserve creation time of cookie for ordering purposes. michael@0: aCookie->SetCreationTime(oldCookie->CreationTime()); michael@0: } michael@0: michael@0: } else { michael@0: // check if cookie has already expired michael@0: if (aCookie->Expiry() <= currentTime) { michael@0: COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, aCookieHeader, michael@0: "cookie has already expired"); michael@0: return; michael@0: } michael@0: michael@0: // check if we have to delete an old cookie. michael@0: nsCookieEntry *entry = mDBState->hostTable.GetEntry(aKey); michael@0: if (entry && entry->GetCookies().Length() >= mMaxCookiesPerHost) { michael@0: nsListIter iter; michael@0: FindStaleCookie(entry, currentTime, iter); michael@0: oldCookie = iter.Cookie(); michael@0: michael@0: // remove the oldest cookie from the domain michael@0: RemoveCookieFromList(iter); michael@0: COOKIE_LOGEVICTED(oldCookie, "Too many cookies for this domain"); michael@0: purgedList = CreatePurgeList(oldCookie); michael@0: michael@0: } else if (mDBState->cookieCount >= ADD_TEN_PERCENT(mMaxNumberOfCookies)) { michael@0: int64_t maxAge = aCurrentTimeInUsec - mDBState->cookieOldestTime; michael@0: int64_t purgeAge = ADD_TEN_PERCENT(mCookiePurgeAge); michael@0: if (maxAge >= purgeAge) { michael@0: // we're over both size and age limits by 10%; time to purge the table! michael@0: // do this by: michael@0: // 1) removing expired cookies; michael@0: // 2) evicting the balance of old cookies until we reach the size limit. michael@0: // note that the cookieOldestTime indicator can be pessimistic - if it's michael@0: // older than the actual oldest cookie, we'll just purge more eagerly. michael@0: purgedList = PurgeCookies(aCurrentTimeInUsec); michael@0: } michael@0: } michael@0: } michael@0: michael@0: // Add the cookie to the db. We do not supply a params array for batching michael@0: // because this might result in removals and additions being out of order. michael@0: AddCookieToList(aKey, aCookie, mDBState, nullptr); michael@0: COOKIE_LOGSUCCESS(SET_COOKIE, aHostURI, aCookieHeader, aCookie, foundCookie); michael@0: michael@0: // Now that list mutations are complete, notify observers. We do it here michael@0: // because observers may themselves attempt to mutate the list. michael@0: if (purgedList) { michael@0: NotifyChanged(purgedList, MOZ_UTF16("batch-deleted")); michael@0: } michael@0: michael@0: NotifyChanged(aCookie, foundCookie ? MOZ_UTF16("changed") michael@0: : MOZ_UTF16("added")); michael@0: } michael@0: michael@0: /****************************************************************************** michael@0: * nsCookieService impl: michael@0: * private cookie header parsing functions michael@0: ******************************************************************************/ michael@0: michael@0: // The following comment block elucidates the function of ParseAttributes. michael@0: /****************************************************************************** michael@0: ** Augmented BNF, modified from RFC2109 Section 4.2.2 and RFC2616 Section 2.1 michael@0: ** please note: this BNF deviates from both specifications, and reflects this michael@0: ** implementation. indicates a reference to the defined grammar "bnf". michael@0: michael@0: ** Differences from RFC2109/2616 and explanations: michael@0: 1. implied *LWS michael@0: The grammar described by this specification is word-based. Except michael@0: where noted otherwise, linear white space () can be included michael@0: between any two adjacent words (token or quoted-string), and michael@0: between adjacent words and separators, without changing the michael@0: interpretation of a field. michael@0: according to spec is SP|HT|CR|LF, but here, we allow only SP | HT. michael@0: michael@0: 2. We use CR | LF as cookie separators, not ',' per spec, since ',' is in michael@0: common use inside values. michael@0: michael@0: 3. tokens and values have looser restrictions on allowed characters than michael@0: spec. This is also due to certain characters being in common use inside michael@0: values. We allow only '=' to separate token/value pairs, and ';' to michael@0: terminate tokens or values. is allowed within tokens and values michael@0: (see bug 206022). michael@0: michael@0: 4. where appropriate, full s are allowed, where the spec dictates to michael@0: reject control chars or non-ASCII chars. This is erring on the loose michael@0: side, since there's probably no good reason to enforce this strictness. michael@0: michael@0: 5. cookie is optional, where spec requires it. This is a fairly michael@0: trivial case, but allows the flexibility of setting only a cookie michael@0: with a blank and is required by some sites (see bug 169091). michael@0: michael@0: 6. Attribute "HttpOnly", not covered in the RFCs, is supported michael@0: (see bug 178993). michael@0: michael@0: ** Begin BNF: michael@0: token = 1* michael@0: value = 1* michael@0: separators = ";" | "=" michael@0: value-sep = ";" michael@0: cookie-sep = CR | LF michael@0: allowed-chars = michael@0: OCTET = michael@0: LWS = SP | HT michael@0: NUL = michael@0: CR = michael@0: LF = michael@0: SP = michael@0: HT = michael@0: michael@0: set-cookie = "Set-Cookie:" cookies michael@0: cookies = cookie *( cookie-sep cookie ) michael@0: cookie = [NAME "="] VALUE *(";" cookie-av) ; cookie NAME/VALUE must come first michael@0: NAME = token ; cookie name michael@0: VALUE = value ; cookie value michael@0: cookie-av = token ["=" value] michael@0: michael@0: valid values for cookie-av (checked post-parsing) are: michael@0: cookie-av = "Path" "=" value michael@0: | "Domain" "=" value michael@0: | "Expires" "=" value michael@0: | "Max-Age" "=" value michael@0: | "Comment" "=" value michael@0: | "Version" "=" value michael@0: | "Secure" michael@0: | "HttpOnly" michael@0: michael@0: ******************************************************************************/ michael@0: michael@0: // helper functions for GetTokenValue michael@0: static inline bool iswhitespace (char c) { return c == ' ' || c == '\t'; } michael@0: static inline bool isterminator (char c) { return c == '\n' || c == '\r'; } michael@0: static inline bool isvalueseparator (char c) { return isterminator(c) || c == ';'; } michael@0: static inline bool istokenseparator (char c) { return isvalueseparator(c) || c == '='; } michael@0: michael@0: // Parse a single token/value pair. michael@0: // Returns true if a cookie terminator is found, so caller can parse new cookie. michael@0: bool michael@0: nsCookieService::GetTokenValue(nsASingleFragmentCString::const_char_iterator &aIter, michael@0: nsASingleFragmentCString::const_char_iterator &aEndIter, michael@0: nsDependentCSubstring &aTokenString, michael@0: nsDependentCSubstring &aTokenValue, michael@0: bool &aEqualsFound) michael@0: { michael@0: nsASingleFragmentCString::const_char_iterator start, lastSpace; michael@0: // initialize value string to clear garbage michael@0: aTokenValue.Rebind(aIter, aIter); michael@0: michael@0: // find , including any between the end-of-token and the michael@0: // token separator. we'll remove trailing next michael@0: while (aIter != aEndIter && iswhitespace(*aIter)) michael@0: ++aIter; michael@0: start = aIter; michael@0: while (aIter != aEndIter && !istokenseparator(*aIter)) michael@0: ++aIter; michael@0: michael@0: // remove trailing ; first check we're not at the beginning michael@0: lastSpace = aIter; michael@0: if (lastSpace != start) { michael@0: while (--lastSpace != start && iswhitespace(*lastSpace)) michael@0: continue; michael@0: ++lastSpace; michael@0: } michael@0: aTokenString.Rebind(start, lastSpace); michael@0: michael@0: aEqualsFound = (*aIter == '='); michael@0: if (aEqualsFound) { michael@0: // find michael@0: while (++aIter != aEndIter && iswhitespace(*aIter)) michael@0: continue; michael@0: michael@0: start = aIter; michael@0: michael@0: // process michael@0: // just look for ';' to terminate ('=' allowed) michael@0: while (aIter != aEndIter && !isvalueseparator(*aIter)) michael@0: ++aIter; michael@0: michael@0: // remove trailing ; first check we're not at the beginning michael@0: if (aIter != start) { michael@0: lastSpace = aIter; michael@0: while (--lastSpace != start && iswhitespace(*lastSpace)) michael@0: continue; michael@0: aTokenValue.Rebind(start, ++lastSpace); michael@0: } michael@0: } michael@0: michael@0: // aIter is on ';', or terminator, or EOS michael@0: if (aIter != aEndIter) { michael@0: // if on terminator, increment past & return true to process new cookie michael@0: if (isterminator(*aIter)) { michael@0: ++aIter; michael@0: return true; michael@0: } michael@0: // fall-through: aIter is on ';', increment and return false michael@0: ++aIter; michael@0: } michael@0: return false; michael@0: } michael@0: michael@0: // Parses attributes from cookie header. expires/max-age attributes aren't folded into the michael@0: // cookie struct here, because we don't know which one to use until we've parsed the header. michael@0: bool michael@0: nsCookieService::ParseAttributes(nsDependentCString &aCookieHeader, michael@0: nsCookieAttributes &aCookieAttributes) michael@0: { michael@0: static const char kPath[] = "path"; michael@0: static const char kDomain[] = "domain"; michael@0: static const char kExpires[] = "expires"; michael@0: static const char kMaxage[] = "max-age"; michael@0: static const char kSecure[] = "secure"; michael@0: static const char kHttpOnly[] = "httponly"; michael@0: michael@0: nsASingleFragmentCString::const_char_iterator tempBegin, tempEnd; michael@0: nsASingleFragmentCString::const_char_iterator cookieStart, cookieEnd; michael@0: aCookieHeader.BeginReading(cookieStart); michael@0: aCookieHeader.EndReading(cookieEnd); michael@0: michael@0: aCookieAttributes.isSecure = false; michael@0: aCookieAttributes.isHttpOnly = false; michael@0: michael@0: nsDependentCSubstring tokenString(cookieStart, cookieStart); michael@0: nsDependentCSubstring tokenValue (cookieStart, cookieStart); michael@0: bool newCookie, equalsFound; michael@0: michael@0: // extract cookie & (first attribute), and copy the strings. michael@0: // if we find multiple cookies, return for processing michael@0: // note: if there's no '=', we assume token is . this is required by michael@0: // some sites (see bug 169091). michael@0: // XXX fix the parser to parse according to grammar for this case michael@0: newCookie = GetTokenValue(cookieStart, cookieEnd, tokenString, tokenValue, equalsFound); michael@0: if (equalsFound) { michael@0: aCookieAttributes.name = tokenString; michael@0: aCookieAttributes.value = tokenValue; michael@0: } else { michael@0: aCookieAttributes.value = tokenString; michael@0: } michael@0: michael@0: // extract remaining attributes michael@0: while (cookieStart != cookieEnd && !newCookie) { michael@0: newCookie = GetTokenValue(cookieStart, cookieEnd, tokenString, tokenValue, equalsFound); michael@0: michael@0: if (!tokenValue.IsEmpty()) { michael@0: tokenValue.BeginReading(tempBegin); michael@0: tokenValue.EndReading(tempEnd); michael@0: } michael@0: michael@0: // decide which attribute we have, and copy the string michael@0: if (tokenString.LowerCaseEqualsLiteral(kPath)) michael@0: aCookieAttributes.path = tokenValue; michael@0: michael@0: else if (tokenString.LowerCaseEqualsLiteral(kDomain)) michael@0: aCookieAttributes.host = tokenValue; michael@0: michael@0: else if (tokenString.LowerCaseEqualsLiteral(kExpires)) michael@0: aCookieAttributes.expires = tokenValue; michael@0: michael@0: else if (tokenString.LowerCaseEqualsLiteral(kMaxage)) michael@0: aCookieAttributes.maxage = tokenValue; michael@0: michael@0: // ignore any tokenValue for isSecure; just set the boolean michael@0: else if (tokenString.LowerCaseEqualsLiteral(kSecure)) michael@0: aCookieAttributes.isSecure = true; michael@0: michael@0: // ignore any tokenValue for isHttpOnly (see bug 178993); michael@0: // just set the boolean michael@0: else if (tokenString.LowerCaseEqualsLiteral(kHttpOnly)) michael@0: aCookieAttributes.isHttpOnly = true; michael@0: } michael@0: michael@0: // rebind aCookieHeader, in case we need to process another cookie michael@0: aCookieHeader.Rebind(cookieStart, cookieEnd); michael@0: return newCookie; michael@0: } michael@0: michael@0: /****************************************************************************** michael@0: * nsCookieService impl: michael@0: * private domain & permission compliance enforcement functions michael@0: ******************************************************************************/ michael@0: michael@0: // Get the base domain for aHostURI; e.g. for "www.bbc.co.uk", this would be michael@0: // "bbc.co.uk". Only properly-formed URI's are tolerated, though a trailing michael@0: // dot may be present. If aHostURI is an IP address, an alias such as michael@0: // 'localhost', an eTLD such as 'co.uk', or the empty string, aBaseDomain will michael@0: // be the exact host, and aRequireHostMatch will be true to indicate that michael@0: // substring matches should not be performed. michael@0: nsresult michael@0: nsCookieService::GetBaseDomain(nsIURI *aHostURI, michael@0: nsCString &aBaseDomain, michael@0: bool &aRequireHostMatch) michael@0: { michael@0: // get the base domain. this will fail if the host contains a leading dot, michael@0: // more than one trailing dot, or is otherwise malformed. michael@0: nsresult rv = mTLDService->GetBaseDomain(aHostURI, 0, aBaseDomain); michael@0: aRequireHostMatch = rv == NS_ERROR_HOST_IS_IP_ADDRESS || michael@0: rv == NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS; michael@0: if (aRequireHostMatch) { michael@0: // aHostURI is either an IP address, an alias such as 'localhost', an eTLD michael@0: // such as 'co.uk', or the empty string. use the host as a key in such michael@0: // cases. michael@0: rv = aHostURI->GetAsciiHost(aBaseDomain); michael@0: } michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: michael@0: // aHost (and thus aBaseDomain) may be the string '.'. If so, fail. michael@0: if (aBaseDomain.Length() == 1 && aBaseDomain.Last() == '.') michael@0: return NS_ERROR_INVALID_ARG; michael@0: michael@0: // block any URIs without a host that aren't file:// URIs. michael@0: if (aBaseDomain.IsEmpty()) { michael@0: bool isFileURI = false; michael@0: aHostURI->SchemeIs("file", &isFileURI); michael@0: if (!isFileURI) michael@0: return NS_ERROR_INVALID_ARG; michael@0: } michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: // Get the base domain for aHost; e.g. for "www.bbc.co.uk", this would be michael@0: // "bbc.co.uk". This is done differently than GetBaseDomain(): it is assumed michael@0: // that aHost is already normalized, and it may contain a leading dot michael@0: // (indicating that it represents a domain). A trailing dot may be present. michael@0: // If aHost is an IP address, an alias such as 'localhost', an eTLD such as michael@0: // 'co.uk', or the empty string, aBaseDomain will be the exact host, and a michael@0: // leading dot will be treated as an error. michael@0: nsresult michael@0: nsCookieService::GetBaseDomainFromHost(const nsACString &aHost, michael@0: nsCString &aBaseDomain) michael@0: { michael@0: // aHost must not be the string '.'. michael@0: if (aHost.Length() == 1 && aHost.Last() == '.') michael@0: return NS_ERROR_INVALID_ARG; michael@0: michael@0: // aHost may contain a leading dot; if so, strip it now. michael@0: bool domain = !aHost.IsEmpty() && aHost.First() == '.'; michael@0: michael@0: // get the base domain. this will fail if the host contains a leading dot, michael@0: // more than one trailing dot, or is otherwise malformed. michael@0: nsresult rv = mTLDService->GetBaseDomainFromHost(Substring(aHost, domain), 0, aBaseDomain); michael@0: if (rv == NS_ERROR_HOST_IS_IP_ADDRESS || michael@0: rv == NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS) { michael@0: // aHost is either an IP address, an alias such as 'localhost', an eTLD michael@0: // such as 'co.uk', or the empty string. use the host as a key in such michael@0: // cases; however, we reject any such hosts with a leading dot, since it michael@0: // doesn't make sense for them to be domain cookies. michael@0: if (domain) michael@0: return NS_ERROR_INVALID_ARG; michael@0: michael@0: aBaseDomain = aHost; michael@0: return NS_OK; michael@0: } michael@0: return rv; michael@0: } michael@0: michael@0: // Normalizes the given hostname, component by component. ASCII/ACE michael@0: // components are lower-cased, and UTF-8 components are normalized per michael@0: // RFC 3454 and converted to ACE. michael@0: nsresult michael@0: nsCookieService::NormalizeHost(nsCString &aHost) michael@0: { michael@0: if (!IsASCII(aHost)) { michael@0: nsAutoCString host; michael@0: nsresult rv = mIDNService->ConvertUTF8toACE(aHost, host); michael@0: if (NS_FAILED(rv)) michael@0: return rv; michael@0: michael@0: aHost = host; michael@0: } michael@0: michael@0: ToLowerCase(aHost); michael@0: return NS_OK; michael@0: } michael@0: michael@0: // returns true if 'a' is equal to or a subdomain of 'b', michael@0: // assuming no leading dots are present. michael@0: static inline bool IsSubdomainOf(const nsCString &a, const nsCString &b) michael@0: { michael@0: if (a == b) michael@0: return true; michael@0: if (a.Length() > b.Length()) michael@0: return a[a.Length() - b.Length() - 1] == '.' && StringEndsWith(a, b); michael@0: return false; michael@0: } michael@0: michael@0: CookieStatus michael@0: nsCookieService::CheckPrefs(nsIURI *aHostURI, michael@0: bool aIsForeign, michael@0: bool aRequireHostMatch, michael@0: const char *aCookieHeader) michael@0: { michael@0: nsresult rv; michael@0: michael@0: // don't let ftp sites get/set cookies (could be a security issue) michael@0: bool ftp; michael@0: if (NS_SUCCEEDED(aHostURI->SchemeIs("ftp", &ftp)) && ftp) { michael@0: COOKIE_LOGFAILURE(aCookieHeader ? SET_COOKIE : GET_COOKIE, aHostURI, aCookieHeader, "ftp sites cannot read cookies"); michael@0: return STATUS_REJECTED_WITH_ERROR; michael@0: } michael@0: michael@0: // check the permission list first; if we find an entry, it overrides michael@0: // default prefs. see bug 184059. michael@0: if (mPermissionService) { michael@0: nsCookieAccess access; michael@0: // Not passing an nsIChannel here is probably OK; our implementation michael@0: // doesn't do anything with it anyway. michael@0: rv = mPermissionService->CanAccess(aHostURI, nullptr, &access); michael@0: michael@0: // if we found an entry, use it michael@0: if (NS_SUCCEEDED(rv)) { michael@0: switch (access) { michael@0: case nsICookiePermission::ACCESS_DENY: michael@0: COOKIE_LOGFAILURE(aCookieHeader ? SET_COOKIE : GET_COOKIE, aHostURI, michael@0: aCookieHeader, "cookies are blocked for this site"); michael@0: return STATUS_REJECTED; michael@0: michael@0: case nsICookiePermission::ACCESS_ALLOW: michael@0: return STATUS_ACCEPTED; michael@0: michael@0: case nsICookiePermission::ACCESS_ALLOW_FIRST_PARTY_ONLY: michael@0: if (aIsForeign) { michael@0: COOKIE_LOGFAILURE(aCookieHeader ? SET_COOKIE : GET_COOKIE, aHostURI, michael@0: aCookieHeader, "third party cookies are blocked " michael@0: "for this site"); michael@0: return STATUS_REJECTED; michael@0: michael@0: } michael@0: return STATUS_ACCEPTED; michael@0: michael@0: case nsICookiePermission::ACCESS_LIMIT_THIRD_PARTY: michael@0: if (!aIsForeign) michael@0: return STATUS_ACCEPTED; michael@0: uint32_t priorCookieCount = 0; michael@0: nsAutoCString hostFromURI; michael@0: aHostURI->GetHost(hostFromURI); michael@0: CountCookiesFromHost(hostFromURI, &priorCookieCount); michael@0: if (priorCookieCount == 0) { michael@0: COOKIE_LOGFAILURE(aCookieHeader ? SET_COOKIE : GET_COOKIE, aHostURI, michael@0: aCookieHeader, "third party cookies are blocked " michael@0: "for this site"); michael@0: return STATUS_REJECTED; michael@0: } michael@0: return STATUS_ACCEPTED; michael@0: } michael@0: } michael@0: } michael@0: michael@0: // check default prefs michael@0: if (mCookieBehavior == BEHAVIOR_REJECT) { michael@0: COOKIE_LOGFAILURE(aCookieHeader ? SET_COOKIE : GET_COOKIE, aHostURI, aCookieHeader, "cookies are disabled"); michael@0: return STATUS_REJECTED; michael@0: } michael@0: michael@0: // check if cookie is foreign michael@0: if (aIsForeign) { michael@0: if (mCookieBehavior == BEHAVIOR_ACCEPT && mThirdPartySession) michael@0: return STATUS_ACCEPT_SESSION; michael@0: michael@0: if (mCookieBehavior == BEHAVIOR_REJECTFOREIGN) { michael@0: COOKIE_LOGFAILURE(aCookieHeader ? SET_COOKIE : GET_COOKIE, aHostURI, aCookieHeader, "context is third party"); michael@0: return STATUS_REJECTED; michael@0: } michael@0: michael@0: if (mCookieBehavior == BEHAVIOR_LIMITFOREIGN) { michael@0: uint32_t priorCookieCount = 0; michael@0: nsAutoCString hostFromURI; michael@0: aHostURI->GetHost(hostFromURI); michael@0: CountCookiesFromHost(hostFromURI, &priorCookieCount); michael@0: if (priorCookieCount == 0) { michael@0: COOKIE_LOGFAILURE(aCookieHeader ? SET_COOKIE : GET_COOKIE, aHostURI, aCookieHeader, "context is third party"); michael@0: return STATUS_REJECTED; michael@0: } michael@0: if (mThirdPartySession) michael@0: return STATUS_ACCEPT_SESSION; michael@0: } michael@0: } michael@0: michael@0: // if nothing has complained, accept cookie michael@0: return STATUS_ACCEPTED; michael@0: } michael@0: michael@0: // processes domain attribute, and returns true if host has permission to set for this domain. michael@0: bool michael@0: nsCookieService::CheckDomain(nsCookieAttributes &aCookieAttributes, michael@0: nsIURI *aHostURI, michael@0: const nsCString &aBaseDomain, michael@0: bool aRequireHostMatch) michael@0: { michael@0: // get host from aHostURI michael@0: nsAutoCString hostFromURI; michael@0: aHostURI->GetAsciiHost(hostFromURI); michael@0: michael@0: // if a domain is given, check the host has permission michael@0: if (!aCookieAttributes.host.IsEmpty()) { michael@0: // Tolerate leading '.' characters, but not if it's otherwise an empty host. michael@0: if (aCookieAttributes.host.Length() > 1 && michael@0: aCookieAttributes.host.First() == '.') { michael@0: aCookieAttributes.host.Cut(0, 1); michael@0: } michael@0: michael@0: // switch to lowercase now, to avoid case-insensitive compares everywhere michael@0: ToLowerCase(aCookieAttributes.host); michael@0: michael@0: // check whether the host is either an IP address, an alias such as michael@0: // 'localhost', an eTLD such as 'co.uk', or the empty string. in these michael@0: // cases, require an exact string match for the domain, and leave the cookie michael@0: // as a non-domain one. bug 105917 originally noted the requirement to deal michael@0: // with IP addresses. michael@0: if (aRequireHostMatch) michael@0: return hostFromURI.Equals(aCookieAttributes.host); michael@0: michael@0: // ensure the proposed domain is derived from the base domain; and also michael@0: // that the host domain is derived from the proposed domain (per RFC2109). michael@0: if (IsSubdomainOf(aCookieAttributes.host, aBaseDomain) && michael@0: IsSubdomainOf(hostFromURI, aCookieAttributes.host)) { michael@0: // prepend a dot to indicate a domain cookie michael@0: aCookieAttributes.host.Insert(NS_LITERAL_CSTRING("."), 0); michael@0: return true; michael@0: } michael@0: michael@0: /* michael@0: * note: RFC2109 section 4.3.2 requires that we check the following: michael@0: * that the portion of host not in domain does not contain a dot. michael@0: * this prevents hosts of the form x.y.co.nz from setting cookies in the michael@0: * entire .co.nz domain. however, it's only a only a partial solution and michael@0: * it breaks sites (IE doesn't enforce it), so we don't perform this check. michael@0: */ michael@0: return false; michael@0: } michael@0: michael@0: // no domain specified, use hostFromURI michael@0: aCookieAttributes.host = hostFromURI; michael@0: return true; michael@0: } michael@0: michael@0: bool michael@0: nsCookieService::CheckPath(nsCookieAttributes &aCookieAttributes, michael@0: nsIURI *aHostURI) michael@0: { michael@0: // if a path is given, check the host has permission michael@0: if (aCookieAttributes.path.IsEmpty() || aCookieAttributes.path.First() != '/') { michael@0: // strip down everything after the last slash to get the path, michael@0: // ignoring slashes in the query string part. michael@0: // if we can QI to nsIURL, that'll take care of the query string portion. michael@0: // otherwise, it's not an nsIURL and can't have a query string, so just find the last slash. michael@0: nsCOMPtr hostURL = do_QueryInterface(aHostURI); michael@0: if (hostURL) { michael@0: hostURL->GetDirectory(aCookieAttributes.path); michael@0: } else { michael@0: aHostURI->GetPath(aCookieAttributes.path); michael@0: int32_t slash = aCookieAttributes.path.RFindChar('/'); michael@0: if (slash != kNotFound) { michael@0: aCookieAttributes.path.Truncate(slash + 1); michael@0: } michael@0: } michael@0: michael@0: #if 0 michael@0: } else { michael@0: /** michael@0: * The following test is part of the RFC2109 spec. Loosely speaking, it says that a site michael@0: * cannot set a cookie for a path that it is not on. See bug 155083. However this patch michael@0: * broke several sites -- nordea (bug 155768) and citibank (bug 156725). So this test has michael@0: * been disabled, unless we can evangelize these sites. michael@0: */ michael@0: // get path from aHostURI michael@0: nsAutoCString pathFromURI; michael@0: if (NS_FAILED(aHostURI->GetPath(pathFromURI)) || michael@0: !StringBeginsWith(pathFromURI, aCookieAttributes.path)) { michael@0: return false; michael@0: } michael@0: #endif michael@0: } michael@0: michael@0: if (aCookieAttributes.path.Length() > kMaxBytesPerPath || michael@0: aCookieAttributes.path.FindChar('\t') != kNotFound ) michael@0: return false; michael@0: michael@0: return true; michael@0: } michael@0: michael@0: bool michael@0: nsCookieService::GetExpiry(nsCookieAttributes &aCookieAttributes, michael@0: int64_t aServerTime, michael@0: int64_t aCurrentTime) michael@0: { michael@0: /* Determine when the cookie should expire. This is done by taking the difference between michael@0: * the server time and the time the server wants the cookie to expire, and adding that michael@0: * difference to the client time. This localizes the client time regardless of whether or michael@0: * not the TZ environment variable was set on the client. michael@0: * michael@0: * Note: We need to consider accounting for network lag here, per RFC. michael@0: */ michael@0: int64_t delta; michael@0: michael@0: // check for max-age attribute first; this overrides expires attribute michael@0: if (!aCookieAttributes.maxage.IsEmpty()) { michael@0: // obtain numeric value of maxageAttribute michael@0: int64_t maxage; michael@0: int32_t numInts = PR_sscanf(aCookieAttributes.maxage.get(), "%lld", &maxage); michael@0: michael@0: // default to session cookie if the conversion failed michael@0: if (numInts != 1) { michael@0: return true; michael@0: } michael@0: michael@0: delta = maxage; michael@0: michael@0: // check for expires attribute michael@0: } else if (!aCookieAttributes.expires.IsEmpty()) { michael@0: PRTime expires; michael@0: michael@0: // parse expiry time michael@0: if (PR_ParseTimeString(aCookieAttributes.expires.get(), true, &expires) != PR_SUCCESS) { michael@0: return true; michael@0: } michael@0: michael@0: delta = expires / int64_t(PR_USEC_PER_SEC) - aServerTime; michael@0: michael@0: // default to session cookie if no attributes found michael@0: } else { michael@0: return true; michael@0: } michael@0: michael@0: // if this addition overflows, expiryTime will be less than currentTime michael@0: // and the cookie will be expired - that's okay. michael@0: aCookieAttributes.expiryTime = aCurrentTime + delta; michael@0: michael@0: return false; michael@0: } michael@0: michael@0: /****************************************************************************** michael@0: * nsCookieService impl: michael@0: * private cookielist management functions michael@0: ******************************************************************************/ michael@0: michael@0: void michael@0: nsCookieService::RemoveAllFromMemory() michael@0: { michael@0: // clearing the hashtable will call each nsCookieEntry's dtor, michael@0: // which releases all their respective children. michael@0: mDBState->hostTable.Clear(); michael@0: mDBState->cookieCount = 0; michael@0: mDBState->cookieOldestTime = INT64_MAX; michael@0: } michael@0: michael@0: // stores temporary data for enumerating over the hash entries, michael@0: // since enumeration is done using callback functions michael@0: struct nsPurgeData michael@0: { michael@0: typedef nsTArray ArrayType; michael@0: michael@0: nsPurgeData(int64_t aCurrentTime, michael@0: int64_t aPurgeTime, michael@0: ArrayType &aPurgeList, michael@0: nsIMutableArray *aRemovedList, michael@0: mozIStorageBindingParamsArray *aParamsArray) michael@0: : currentTime(aCurrentTime) michael@0: , purgeTime(aPurgeTime) michael@0: , oldestTime(INT64_MAX) michael@0: , purgeList(aPurgeList) michael@0: , removedList(aRemovedList) michael@0: , paramsArray(aParamsArray) michael@0: { michael@0: } michael@0: michael@0: // the current time, in seconds michael@0: int64_t currentTime; michael@0: michael@0: // lastAccessed time older than which cookies are eligible for purge michael@0: int64_t purgeTime; michael@0: michael@0: // lastAccessed time of the oldest cookie found during purge, to update our indicator michael@0: int64_t oldestTime; michael@0: michael@0: // list of cookies over the age limit, for purging michael@0: ArrayType &purgeList; michael@0: michael@0: // list of all cookies we've removed, for notification michael@0: nsIMutableArray *removedList; michael@0: michael@0: // The array of parameters to be bound to the statement for deletion later. michael@0: mozIStorageBindingParamsArray *paramsArray; michael@0: }; michael@0: michael@0: // comparator class for lastaccessed times of cookies. michael@0: class CompareCookiesByAge { michael@0: public: michael@0: bool Equals(const nsListIter &a, const nsListIter &b) const michael@0: { michael@0: return a.Cookie()->LastAccessed() == b.Cookie()->LastAccessed() && michael@0: a.Cookie()->CreationTime() == b.Cookie()->CreationTime(); michael@0: } michael@0: michael@0: bool LessThan(const nsListIter &a, const nsListIter &b) const michael@0: { michael@0: // compare by lastAccessed time, and tiebreak by creationTime. michael@0: int64_t result = a.Cookie()->LastAccessed() - b.Cookie()->LastAccessed(); michael@0: if (result != 0) michael@0: return result < 0; michael@0: michael@0: return a.Cookie()->CreationTime() < b.Cookie()->CreationTime(); michael@0: } michael@0: }; michael@0: michael@0: // comparator class for sorting cookies by entry and index. michael@0: class CompareCookiesByIndex { michael@0: public: michael@0: bool Equals(const nsListIter &a, const nsListIter &b) const michael@0: { michael@0: NS_ASSERTION(a.entry != b.entry || a.index != b.index, michael@0: "cookie indexes should never be equal"); michael@0: return false; michael@0: } michael@0: michael@0: bool LessThan(const nsListIter &a, const nsListIter &b) const michael@0: { michael@0: // compare by entryclass pointer, then by index. michael@0: if (a.entry != b.entry) michael@0: return a.entry < b.entry; michael@0: michael@0: return a.index < b.index; michael@0: } michael@0: }; michael@0: michael@0: PLDHashOperator michael@0: purgeCookiesCallback(nsCookieEntry *aEntry, michael@0: void *aArg) michael@0: { michael@0: nsPurgeData &data = *static_cast(aArg); michael@0: michael@0: const nsCookieEntry::ArrayType &cookies = aEntry->GetCookies(); michael@0: mozIStorageBindingParamsArray *array = data.paramsArray; michael@0: for (nsCookieEntry::IndexType i = 0; i < cookies.Length(); ) { michael@0: nsListIter iter(aEntry, i); michael@0: nsCookie *cookie = cookies[i]; michael@0: michael@0: // check if the cookie has expired michael@0: if (cookie->Expiry() <= data.currentTime) { michael@0: data.removedList->AppendElement(cookie, false); michael@0: COOKIE_LOGEVICTED(cookie, "Cookie expired"); michael@0: michael@0: // remove from list; do not increment our iterator michael@0: gCookieService->RemoveCookieFromList(iter, array); michael@0: michael@0: } else { michael@0: // check if the cookie is over the age limit michael@0: if (cookie->LastAccessed() <= data.purgeTime) { michael@0: data.purgeList.AppendElement(iter); michael@0: michael@0: } else if (cookie->LastAccessed() < data.oldestTime) { michael@0: // reset our indicator michael@0: data.oldestTime = cookie->LastAccessed(); michael@0: } michael@0: michael@0: ++i; michael@0: } michael@0: } michael@0: return PL_DHASH_NEXT; michael@0: } michael@0: michael@0: // purges expired and old cookies in a batch operation. michael@0: already_AddRefed michael@0: nsCookieService::PurgeCookies(int64_t aCurrentTimeInUsec) michael@0: { michael@0: NS_ASSERTION(mDBState->hostTable.Count() > 0, "table is empty"); michael@0: EnsureReadComplete(); michael@0: michael@0: #ifdef PR_LOGGING michael@0: uint32_t initialCookieCount = mDBState->cookieCount; michael@0: COOKIE_LOGSTRING(PR_LOG_DEBUG, michael@0: ("PurgeCookies(): beginning purge with %ld cookies and %lld oldest age", michael@0: mDBState->cookieCount, aCurrentTimeInUsec - mDBState->cookieOldestTime)); michael@0: #endif michael@0: michael@0: nsAutoTArray purgeList; michael@0: michael@0: nsCOMPtr removedList = do_CreateInstance(NS_ARRAY_CONTRACTID); michael@0: michael@0: // Create a params array to batch the removals. This is OK here because michael@0: // all the removals are in order, and there are no interleaved additions. michael@0: mozIStorageAsyncStatement *stmt = mDBState->stmtDelete; michael@0: nsCOMPtr paramsArray; michael@0: if (mDBState->dbConn) { michael@0: stmt->NewBindingParamsArray(getter_AddRefs(paramsArray)); michael@0: } michael@0: michael@0: nsPurgeData data(aCurrentTimeInUsec / PR_USEC_PER_SEC, michael@0: aCurrentTimeInUsec - mCookiePurgeAge, purgeList, removedList, paramsArray); michael@0: mDBState->hostTable.EnumerateEntries(purgeCookiesCallback, &data); michael@0: michael@0: #ifdef PR_LOGGING michael@0: uint32_t postExpiryCookieCount = mDBState->cookieCount; michael@0: #endif michael@0: michael@0: // now we have a list of iterators for cookies over the age limit. michael@0: // sort them by age, and then we'll see how many to remove... michael@0: purgeList.Sort(CompareCookiesByAge()); michael@0: michael@0: // only remove old cookies until we reach the max cookie limit, no more. michael@0: uint32_t excess = mDBState->cookieCount > mMaxNumberOfCookies ? michael@0: mDBState->cookieCount - mMaxNumberOfCookies : 0; michael@0: if (purgeList.Length() > excess) { michael@0: // We're not purging everything in the list, so update our indicator. michael@0: data.oldestTime = purgeList[excess].Cookie()->LastAccessed(); michael@0: michael@0: purgeList.SetLength(excess); michael@0: } michael@0: michael@0: // sort the list again, this time grouping cookies with a common entryclass michael@0: // together, and with ascending index. this allows us to iterate backwards michael@0: // over the list removing cookies, without having to adjust indexes as we go. michael@0: purgeList.Sort(CompareCookiesByIndex()); michael@0: for (nsPurgeData::ArrayType::index_type i = purgeList.Length(); i--; ) { michael@0: nsCookie *cookie = purgeList[i].Cookie(); michael@0: removedList->AppendElement(cookie, false); michael@0: COOKIE_LOGEVICTED(cookie, "Cookie too old"); michael@0: michael@0: RemoveCookieFromList(purgeList[i], paramsArray); michael@0: } michael@0: michael@0: // Update the database if we have entries to purge. michael@0: if (paramsArray) { michael@0: uint32_t length; michael@0: paramsArray->GetLength(&length); michael@0: if (length) { michael@0: DebugOnly rv = stmt->BindParameters(paramsArray); michael@0: NS_ASSERT_SUCCESS(rv); michael@0: nsCOMPtr handle; michael@0: rv = stmt->ExecuteAsync(mDBState->removeListener, getter_AddRefs(handle)); michael@0: NS_ASSERT_SUCCESS(rv); michael@0: } michael@0: } michael@0: michael@0: // reset the oldest time indicator michael@0: mDBState->cookieOldestTime = data.oldestTime; michael@0: michael@0: COOKIE_LOGSTRING(PR_LOG_DEBUG, michael@0: ("PurgeCookies(): %ld expired; %ld purged; %ld remain; %lld oldest age", michael@0: initialCookieCount - postExpiryCookieCount, michael@0: postExpiryCookieCount - mDBState->cookieCount, michael@0: mDBState->cookieCount, michael@0: aCurrentTimeInUsec - mDBState->cookieOldestTime)); michael@0: michael@0: return removedList.forget(); michael@0: } michael@0: michael@0: // find whether a given cookie has been previously set. this is provided by the michael@0: // nsICookieManager2 interface. michael@0: NS_IMETHODIMP michael@0: nsCookieService::CookieExists(nsICookie2 *aCookie, michael@0: bool *aFoundCookie) michael@0: { michael@0: NS_ENSURE_ARG_POINTER(aCookie); michael@0: michael@0: if (!mDBState) { michael@0: NS_WARNING("No DBState! Profile already closed?"); michael@0: return NS_ERROR_NOT_AVAILABLE; michael@0: } michael@0: michael@0: nsAutoCString host, name, path; michael@0: nsresult rv = aCookie->GetHost(host); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: rv = aCookie->GetName(name); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: rv = aCookie->GetPath(path); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: michael@0: nsAutoCString baseDomain; michael@0: rv = GetBaseDomainFromHost(host, baseDomain); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: michael@0: nsListIter iter; michael@0: *aFoundCookie = FindCookie(DEFAULT_APP_KEY(baseDomain), host, name, path, iter); michael@0: return NS_OK; michael@0: } michael@0: michael@0: // For a given base domain, find either an expired cookie or the oldest cookie michael@0: // by lastAccessed time. michael@0: void michael@0: nsCookieService::FindStaleCookie(nsCookieEntry *aEntry, michael@0: int64_t aCurrentTime, michael@0: nsListIter &aIter) michael@0: { michael@0: aIter.entry = nullptr; michael@0: michael@0: int64_t oldestTime = 0; michael@0: const nsCookieEntry::ArrayType &cookies = aEntry->GetCookies(); michael@0: for (nsCookieEntry::IndexType i = 0; i < cookies.Length(); ++i) { michael@0: nsCookie *cookie = cookies[i]; michael@0: michael@0: // If we found an expired cookie, we're done. michael@0: if (cookie->Expiry() <= aCurrentTime) { michael@0: aIter.entry = aEntry; michael@0: aIter.index = i; michael@0: return; michael@0: } michael@0: michael@0: // Check if we've found the oldest cookie so far. michael@0: if (!aIter.entry || oldestTime > cookie->LastAccessed()) { michael@0: oldestTime = cookie->LastAccessed(); michael@0: aIter.entry = aEntry; michael@0: aIter.index = i; michael@0: } michael@0: } michael@0: } michael@0: michael@0: // count the number of cookies stored by a particular host. this is provided by the michael@0: // nsICookieManager2 interface. michael@0: NS_IMETHODIMP michael@0: nsCookieService::CountCookiesFromHost(const nsACString &aHost, michael@0: uint32_t *aCountFromHost) michael@0: { michael@0: if (!mDBState) { michael@0: NS_WARNING("No DBState! Profile already closed?"); michael@0: return NS_ERROR_NOT_AVAILABLE; michael@0: } michael@0: michael@0: // first, normalize the hostname, and fail if it contains illegal characters. michael@0: nsAutoCString host(aHost); michael@0: nsresult rv = NormalizeHost(host); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: michael@0: nsAutoCString baseDomain; michael@0: rv = GetBaseDomainFromHost(host, baseDomain); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: michael@0: nsCookieKey key = DEFAULT_APP_KEY(baseDomain); michael@0: EnsureReadDomain(key); michael@0: michael@0: // Return a count of all cookies, including expired. michael@0: nsCookieEntry *entry = mDBState->hostTable.GetEntry(key); michael@0: *aCountFromHost = entry ? entry->GetCookies().Length() : 0; michael@0: return NS_OK; michael@0: } michael@0: michael@0: // get an enumerator of cookies stored by a particular host. this is provided by the michael@0: // nsICookieManager2 interface. michael@0: NS_IMETHODIMP michael@0: nsCookieService::GetCookiesFromHost(const nsACString &aHost, michael@0: nsISimpleEnumerator **aEnumerator) michael@0: { michael@0: if (!mDBState) { michael@0: NS_WARNING("No DBState! Profile already closed?"); michael@0: return NS_ERROR_NOT_AVAILABLE; michael@0: } michael@0: michael@0: // first, normalize the hostname, and fail if it contains illegal characters. michael@0: nsAutoCString host(aHost); michael@0: nsresult rv = NormalizeHost(host); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: michael@0: nsAutoCString baseDomain; michael@0: rv = GetBaseDomainFromHost(host, baseDomain); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: michael@0: nsCookieKey key = DEFAULT_APP_KEY(baseDomain); michael@0: EnsureReadDomain(key); michael@0: michael@0: nsCookieEntry *entry = mDBState->hostTable.GetEntry(key); michael@0: if (!entry) michael@0: return NS_NewEmptyEnumerator(aEnumerator); michael@0: michael@0: nsCOMArray cookieList(mMaxCookiesPerHost); michael@0: const nsCookieEntry::ArrayType &cookies = entry->GetCookies(); michael@0: for (nsCookieEntry::IndexType i = 0; i < cookies.Length(); ++i) { michael@0: cookieList.AppendObject(cookies[i]); michael@0: } michael@0: michael@0: return NS_NewArrayEnumerator(aEnumerator, cookieList); michael@0: } michael@0: michael@0: namespace { michael@0: michael@0: /** michael@0: * This structure is used as a in/out parameter when enumerating the cookies michael@0: * for an app. michael@0: * It will contain the app id and onlyBrowserElement flag information as input michael@0: * and will contain the array of matching cookies as output. michael@0: */ michael@0: struct GetCookiesForAppStruct { michael@0: uint32_t appId; michael@0: bool onlyBrowserElement; michael@0: nsCOMArray cookies; michael@0: michael@0: GetCookiesForAppStruct() MOZ_DELETE; michael@0: GetCookiesForAppStruct(uint32_t aAppId, bool aOnlyBrowserElement) michael@0: : appId(aAppId) michael@0: , onlyBrowserElement(aOnlyBrowserElement) michael@0: {} michael@0: }; michael@0: michael@0: } // anonymous namespace michael@0: michael@0: /* static */ PLDHashOperator michael@0: nsCookieService::GetCookiesForApp(nsCookieEntry* entry, void* arg) michael@0: { michael@0: GetCookiesForAppStruct* data = static_cast(arg); michael@0: michael@0: if (entry->mAppId != data->appId || michael@0: (data->onlyBrowserElement && !entry->mInBrowserElement)) { michael@0: return PL_DHASH_NEXT; michael@0: } michael@0: michael@0: const nsCookieEntry::ArrayType& cookies = entry->GetCookies(); michael@0: michael@0: for (nsCookieEntry::IndexType i = 0; i < cookies.Length(); ++i) { michael@0: data->cookies.AppendObject(cookies[i]); michael@0: } michael@0: michael@0: return PL_DHASH_NEXT; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: nsCookieService::GetCookiesForApp(uint32_t aAppId, bool aOnlyBrowserElement, michael@0: nsISimpleEnumerator** aEnumerator) michael@0: { michael@0: if (!mDBState) { michael@0: NS_WARNING("No DBState! Profile already closed?"); michael@0: return NS_ERROR_NOT_AVAILABLE; michael@0: } michael@0: michael@0: NS_ENSURE_TRUE(aAppId != NECKO_UNKNOWN_APP_ID, NS_ERROR_INVALID_ARG); michael@0: michael@0: GetCookiesForAppStruct data(aAppId, aOnlyBrowserElement); michael@0: mDBState->hostTable.EnumerateEntries(GetCookiesForApp, &data); michael@0: michael@0: return NS_NewArrayEnumerator(aEnumerator, data.cookies); michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: nsCookieService::RemoveCookiesForApp(uint32_t aAppId, bool aOnlyBrowserElement) michael@0: { michael@0: nsCOMPtr enumerator; michael@0: nsresult rv = GetCookiesForApp(aAppId, aOnlyBrowserElement, michael@0: getter_AddRefs(enumerator)); michael@0: michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: michael@0: bool hasMore; michael@0: while (NS_SUCCEEDED(enumerator->HasMoreElements(&hasMore)) && hasMore) { michael@0: nsCOMPtr supports; michael@0: nsCOMPtr cookie; michael@0: rv = enumerator->GetNext(getter_AddRefs(supports)); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: michael@0: cookie = do_QueryInterface(supports); michael@0: michael@0: nsAutoCString host; michael@0: cookie->GetHost(host); michael@0: michael@0: nsAutoCString name; michael@0: cookie->GetName(name); michael@0: michael@0: nsAutoCString path; michael@0: cookie->GetPath(path); michael@0: michael@0: // nsICookie do not carry the appId/inBrowserElement information. michael@0: // That means we have to guess. This is easy for appId but not for michael@0: // inBrowserElement flag. michael@0: // A simple solution is to always ask to remove the cookie with michael@0: // inBrowserElement = true and only ask for the other one to be removed if michael@0: // we happen to be in the case of !aOnlyBrowserElement. michael@0: // Anyway, with this solution, we will likely be looking for unexistant michael@0: // cookies. michael@0: // michael@0: // NOTE: we could make this better by getting nsCookieEntry objects instead michael@0: // of plain nsICookie. michael@0: Remove(host, aAppId, true, name, path, false); michael@0: if (!aOnlyBrowserElement) { michael@0: Remove(host, aAppId, false, name, path, false); michael@0: } michael@0: } michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: // find an exact cookie specified by host, name, and path that hasn't expired. michael@0: bool michael@0: nsCookieService::FindCookie(const nsCookieKey &aKey, michael@0: const nsAFlatCString &aHost, michael@0: const nsAFlatCString &aName, michael@0: const nsAFlatCString &aPath, michael@0: nsListIter &aIter) michael@0: { michael@0: EnsureReadDomain(aKey); michael@0: michael@0: nsCookieEntry *entry = mDBState->hostTable.GetEntry(aKey); michael@0: if (!entry) michael@0: return false; michael@0: michael@0: const nsCookieEntry::ArrayType &cookies = entry->GetCookies(); michael@0: for (nsCookieEntry::IndexType i = 0; i < cookies.Length(); ++i) { michael@0: nsCookie *cookie = cookies[i]; michael@0: michael@0: if (aHost.Equals(cookie->Host()) && michael@0: aPath.Equals(cookie->Path()) && michael@0: aName.Equals(cookie->Name())) { michael@0: aIter = nsListIter(entry, i); michael@0: return true; michael@0: } michael@0: } michael@0: michael@0: return false; michael@0: } michael@0: michael@0: // remove a cookie from the hashtable, and update the iterator state. michael@0: void michael@0: nsCookieService::RemoveCookieFromList(const nsListIter &aIter, michael@0: mozIStorageBindingParamsArray *aParamsArray) michael@0: { michael@0: // if it's a non-session cookie, remove it from the db michael@0: if (!aIter.Cookie()->IsSession() && mDBState->dbConn) { michael@0: // Use the asynchronous binding methods to ensure that we do not acquire michael@0: // the database lock. michael@0: mozIStorageAsyncStatement *stmt = mDBState->stmtDelete; michael@0: nsCOMPtr paramsArray(aParamsArray); michael@0: if (!paramsArray) { michael@0: stmt->NewBindingParamsArray(getter_AddRefs(paramsArray)); michael@0: } michael@0: michael@0: nsCOMPtr params; michael@0: paramsArray->NewBindingParams(getter_AddRefs(params)); michael@0: michael@0: DebugOnly rv = michael@0: params->BindUTF8StringByName(NS_LITERAL_CSTRING("name"), michael@0: aIter.Cookie()->Name()); michael@0: NS_ASSERT_SUCCESS(rv); michael@0: michael@0: rv = params->BindUTF8StringByName(NS_LITERAL_CSTRING("host"), michael@0: aIter.Cookie()->Host()); michael@0: NS_ASSERT_SUCCESS(rv); michael@0: michael@0: rv = params->BindUTF8StringByName(NS_LITERAL_CSTRING("path"), michael@0: aIter.Cookie()->Path()); michael@0: NS_ASSERT_SUCCESS(rv); michael@0: michael@0: rv = paramsArray->AddParams(params); michael@0: NS_ASSERT_SUCCESS(rv); michael@0: michael@0: // If we weren't given a params array, we'll need to remove it ourselves. michael@0: if (!aParamsArray) { michael@0: rv = stmt->BindParameters(paramsArray); michael@0: NS_ASSERT_SUCCESS(rv); michael@0: nsCOMPtr handle; michael@0: rv = stmt->ExecuteAsync(mDBState->removeListener, getter_AddRefs(handle)); michael@0: NS_ASSERT_SUCCESS(rv); michael@0: } michael@0: } michael@0: michael@0: if (aIter.entry->GetCookies().Length() == 1) { michael@0: // we're removing the last element in the array - so just remove the entry michael@0: // from the hash. note that the entryclass' dtor will take care of michael@0: // releasing this last element for us! michael@0: mDBState->hostTable.RawRemoveEntry(aIter.entry); michael@0: michael@0: } else { michael@0: // just remove the element from the list michael@0: aIter.entry->GetCookies().RemoveElementAt(aIter.index); michael@0: } michael@0: michael@0: --mDBState->cookieCount; michael@0: } michael@0: michael@0: void michael@0: bindCookieParameters(mozIStorageBindingParamsArray *aParamsArray, michael@0: const nsCookieKey &aKey, michael@0: const nsCookie *aCookie) michael@0: { michael@0: NS_ASSERTION(aParamsArray, "Null params array passed to bindCookieParameters!"); michael@0: NS_ASSERTION(aCookie, "Null cookie passed to bindCookieParameters!"); michael@0: michael@0: // Use the asynchronous binding methods to ensure that we do not acquire the michael@0: // database lock. michael@0: nsCOMPtr params; michael@0: DebugOnly rv = michael@0: aParamsArray->NewBindingParams(getter_AddRefs(params)); michael@0: NS_ASSERT_SUCCESS(rv); michael@0: michael@0: // Bind our values to params michael@0: rv = params->BindUTF8StringByName(NS_LITERAL_CSTRING("baseDomain"), michael@0: aKey.mBaseDomain); michael@0: NS_ASSERT_SUCCESS(rv); michael@0: michael@0: rv = params->BindInt32ByName(NS_LITERAL_CSTRING("appId"), michael@0: aKey.mAppId); michael@0: NS_ASSERT_SUCCESS(rv); michael@0: michael@0: rv = params->BindInt32ByName(NS_LITERAL_CSTRING("inBrowserElement"), michael@0: aKey.mInBrowserElement ? 1 : 0); michael@0: NS_ASSERT_SUCCESS(rv); michael@0: michael@0: rv = params->BindUTF8StringByName(NS_LITERAL_CSTRING("name"), michael@0: aCookie->Name()); michael@0: NS_ASSERT_SUCCESS(rv); michael@0: michael@0: rv = params->BindUTF8StringByName(NS_LITERAL_CSTRING("value"), michael@0: aCookie->Value()); michael@0: NS_ASSERT_SUCCESS(rv); michael@0: michael@0: rv = params->BindUTF8StringByName(NS_LITERAL_CSTRING("host"), michael@0: aCookie->Host()); michael@0: NS_ASSERT_SUCCESS(rv); michael@0: michael@0: rv = params->BindUTF8StringByName(NS_LITERAL_CSTRING("path"), michael@0: aCookie->Path()); michael@0: NS_ASSERT_SUCCESS(rv); michael@0: michael@0: rv = params->BindInt64ByName(NS_LITERAL_CSTRING("expiry"), michael@0: aCookie->Expiry()); michael@0: NS_ASSERT_SUCCESS(rv); michael@0: michael@0: rv = params->BindInt64ByName(NS_LITERAL_CSTRING("lastAccessed"), michael@0: aCookie->LastAccessed()); michael@0: NS_ASSERT_SUCCESS(rv); michael@0: michael@0: rv = params->BindInt64ByName(NS_LITERAL_CSTRING("creationTime"), michael@0: aCookie->CreationTime()); michael@0: NS_ASSERT_SUCCESS(rv); michael@0: michael@0: rv = params->BindInt32ByName(NS_LITERAL_CSTRING("isSecure"), michael@0: aCookie->IsSecure()); michael@0: NS_ASSERT_SUCCESS(rv); michael@0: michael@0: rv = params->BindInt32ByName(NS_LITERAL_CSTRING("isHttpOnly"), michael@0: aCookie->IsHttpOnly()); michael@0: NS_ASSERT_SUCCESS(rv); michael@0: michael@0: // Bind the params to the array. michael@0: rv = aParamsArray->AddParams(params); michael@0: NS_ASSERT_SUCCESS(rv); michael@0: } michael@0: michael@0: void michael@0: nsCookieService::AddCookieToList(const nsCookieKey &aKey, michael@0: nsCookie *aCookie, michael@0: DBState *aDBState, michael@0: mozIStorageBindingParamsArray *aParamsArray, michael@0: bool aWriteToDB) michael@0: { michael@0: NS_ASSERTION(!(aDBState->dbConn && !aWriteToDB && aParamsArray), michael@0: "Not writing to the DB but have a params array?"); michael@0: NS_ASSERTION(!(!aDBState->dbConn && aParamsArray), michael@0: "Do not have a DB connection but have a params array?"); michael@0: michael@0: nsCookieEntry *entry = aDBState->hostTable.PutEntry(aKey); michael@0: NS_ASSERTION(entry, "can't insert element into a null entry!"); michael@0: michael@0: entry->GetCookies().AppendElement(aCookie); michael@0: ++aDBState->cookieCount; michael@0: michael@0: // keep track of the oldest cookie, for when it comes time to purge michael@0: if (aCookie->LastAccessed() < aDBState->cookieOldestTime) michael@0: aDBState->cookieOldestTime = aCookie->LastAccessed(); michael@0: michael@0: // if it's a non-session cookie and hasn't just been read from the db, write it out. michael@0: if (aWriteToDB && !aCookie->IsSession() && aDBState->dbConn) { michael@0: mozIStorageAsyncStatement *stmt = aDBState->stmtInsert; michael@0: nsCOMPtr paramsArray(aParamsArray); michael@0: if (!paramsArray) { michael@0: stmt->NewBindingParamsArray(getter_AddRefs(paramsArray)); michael@0: } michael@0: bindCookieParameters(paramsArray, aKey, aCookie); michael@0: michael@0: // If we were supplied an array to store parameters, we shouldn't call michael@0: // executeAsync - someone up the stack will do this for us. michael@0: if (!aParamsArray) { michael@0: DebugOnly rv = stmt->BindParameters(paramsArray); michael@0: NS_ASSERT_SUCCESS(rv); michael@0: nsCOMPtr handle; michael@0: rv = stmt->ExecuteAsync(mDBState->insertListener, getter_AddRefs(handle)); michael@0: NS_ASSERT_SUCCESS(rv); michael@0: } michael@0: } michael@0: } michael@0: michael@0: void michael@0: nsCookieService::UpdateCookieInList(nsCookie *aCookie, michael@0: int64_t aLastAccessed, michael@0: mozIStorageBindingParamsArray *aParamsArray) michael@0: { michael@0: NS_ASSERTION(aCookie, "Passing a null cookie to UpdateCookieInList!"); michael@0: michael@0: // udpate the lastAccessed timestamp michael@0: aCookie->SetLastAccessed(aLastAccessed); michael@0: michael@0: // if it's a non-session cookie, update it in the db too michael@0: if (!aCookie->IsSession() && aParamsArray) { michael@0: // Create our params holder. michael@0: nsCOMPtr params; michael@0: aParamsArray->NewBindingParams(getter_AddRefs(params)); michael@0: michael@0: // Bind our parameters. michael@0: DebugOnly rv = michael@0: params->BindInt64ByName(NS_LITERAL_CSTRING("lastAccessed"), michael@0: aLastAccessed); michael@0: NS_ASSERT_SUCCESS(rv); michael@0: michael@0: rv = params->BindUTF8StringByName(NS_LITERAL_CSTRING("name"), michael@0: aCookie->Name()); michael@0: NS_ASSERT_SUCCESS(rv); michael@0: michael@0: rv = params->BindUTF8StringByName(NS_LITERAL_CSTRING("host"), michael@0: aCookie->Host()); michael@0: NS_ASSERT_SUCCESS(rv); michael@0: michael@0: rv = params->BindUTF8StringByName(NS_LITERAL_CSTRING("path"), michael@0: aCookie->Path()); michael@0: NS_ASSERT_SUCCESS(rv); michael@0: michael@0: // Add our bound parameters to the array. michael@0: rv = aParamsArray->AddParams(params); michael@0: NS_ASSERT_SUCCESS(rv); michael@0: } michael@0: } michael@0: michael@0: size_t michael@0: nsCookieService::SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const michael@0: { michael@0: size_t n = aMallocSizeOf(this); michael@0: michael@0: if (mDefaultDBState) { michael@0: n += mDefaultDBState->SizeOfIncludingThis(aMallocSizeOf); michael@0: } michael@0: if (mPrivateDBState) { michael@0: n += mPrivateDBState->SizeOfIncludingThis(aMallocSizeOf); michael@0: } michael@0: michael@0: return n; michael@0: } michael@0: michael@0: MOZ_DEFINE_MALLOC_SIZE_OF(CookieServiceMallocSizeOf) michael@0: michael@0: NS_IMETHODIMP michael@0: nsCookieService::CollectReports(nsIHandleReportCallback* aHandleReport, michael@0: nsISupports* aData) michael@0: { michael@0: return MOZ_COLLECT_REPORT( michael@0: "explicit/cookie-service", KIND_HEAP, UNITS_BYTES, michael@0: SizeOfIncludingThis(CookieServiceMallocSizeOf), michael@0: "Memory used by the cookie service."); michael@0: }